I'm attempting to compare two arrays: one contains a list of usernames (dynamic, sometimes more usernames, sometimes less) and the other contains a list of file names (also dynamic). Every file name contains the username along with other text, e.g "Username report [date].xlsx". The goal is to match the elements between Array A and Array B.
Array A is just usernames.
Output of Array A, contained in $Username is just:
PersonA
PersonB
PersonC
etc...
Array B contains filepaths, but I can narrow it down to just filenames like so $ArrayB.Name (the full path would be $ArrayB.FullName). The naming format for Array B is "Username report [date].xlsx".
Output of Array B, contained within $LatestFiles.Name (for the file name) is:
PersonA Report 1-1-21.xlsx
PersonB Report 1-1-21.xlsx
PersonC Report 1-1-21.xlsx
After matching, the final piece would be if element in Array A matches element in Array B, attach ArrayB.FullName to the corresponding username + "#domain.com".
Unfortunately I can't even get the matching to work properly.
I've tried:
foreach ($elem in $UserName) { if ($LatestFiles.Name -contains $elem) { "there is a match" } }
and
foreach ($elem in $UserName) {
if($LatestFiles.Name -contains $elem) {
"There is a match"
} else {
"There is no match"
}
}
and a couple different variations, but I can't get them to output the matches. Any assistance is appreciated.
Short answer to why you can't get matches:
-Contains is meant for matching a value against a collection, not a String. You would be better off using -Like for your comparison operator. Or, at the very least, you may be trying to see if the name of the file, and not simply the part of the name that holds the user, is in the collection of user names.
I sounds like you are not simply comparing the arrays, but the more complicated matter of what you do with the two matching elements.
$LatestFiles |
ForEach-Object {
# first, let's take the formatted file name and make it more usable
$fileName = $_.BaseName -split ' ' # BaseName over Name so that it strips the extension
Write-Output #{
User = $fileName[0]
Date = $fileName[2]
File = $_
}
} -PipelineVariable FileData |
# next, only accept valid users
Where-Object User -In $UserName |
# note that we don't need the value from $UserName because we already have it in $FileData.User (what we matched)
ForEach-Object {
# finally do your thing with it.
$userWithDomain = "$($FileData.User)#domain.com"
Write-Output $userWithDomain # or whatever you want
}
Related
I currently have an array in Powershell giving me a list of users with certain objects (info). There are multiple instances of certain users but with different info attached. I want to add that info so when I search for a certain element/name in array it comes back with all the info.
So say for example I have three array.name all equal to John. But each instance has different addresses. I want to add those different address together so I have one array.name equal to John but it has the three different address attached.
I have tried a loop within a loop and creating a new array by trying to compare what I already have with the new array. But it doesn't work and spews out an endless list.
$arr3 += $arr2[0]
For ($a=0; $a -le ($arr2.length - 1); $a++) {
$temp3 = $arr2[$a].managedBy
if ($temp3 -eq $null){
$temp3 = "none"
}
For ($b=0; $b -le ($arr3.length - 1); $b++) {
if ($temp3 -eq $arr3[$b].managedBy) {
$arr3[$b].name += "`n"
$arr3[$b].name += $arr2[$a].name
$arr3[$b].description += "`n"
$arr3[$b].description += $arr2[$a].description
$arr3[$b].info += "`n"
$arr3[$b].info += $arr2[$a].info
} else {
$arr3 += $arr2[$a]
}
}
}
I should have arr3 with a list of managedBy that don't repeat but with extra info attached in some cases. This doesn't work and ends up spewing out a huge endless array.
Applying Lee_Daily's suggestions, you can do the following:
$arr3 = $arr2 | Group-Object ManagedBy |
select #{n='ManagedBy';e={$_.Name}},#{n='Name';e={$_.group.Name -join ";"}},
#{n='description';e={$_.group.description -join ";"}},
#{n='info';e={$_.group.info -join ";"}}
This code groups names that are identical into one line of output using Group-Object. The .Group property contains the remaining properties of the $arr2 objects. All of the array object properties are stored in the .group property of the Group-Object output, hence the need to use nested properties ($_.group.property) to access them. I excluded ManagedBy. I use the -join operator to join all values of a single property with a semi-colon delimiter.
To customize this code for your situation, you can change the following since you may need to swap properties to reach your desired effect:
Group-Object ManagedBy: You can use another property besides ManagedBy. This should be the property that contains duplicate values across your array that you want to reduce to one entry.
Select #{n=...;e={...}}: The property you are grouping by (ManagedBy in this case) is mapped to the .name property of the Group-Object output. The remaining headers and values in the hash table should be the multiple values you want to join in one line. The hash table values should exclude the grouped property.
-join ";": This is the delimiter that separates your common properties' values. Your info property value will be in the format Info 1;Info 2;Info 3. You can update this delimiter.
Noob here.
I'm trying to pare down a list of domains by eliminating all subdomains if the parent domain is present in the list. I've managed to cobble together a script that somewhat does this with PowerShell after some searching and reading. The output is not exactly what I want, but will work OK. The problem with my solution is that it takes so long to run because of the size of my initial list (tens of thousands of entries).
UPDATE: I've updated my example to clarify my question.
Example "parent.txt" list:
adk2.co
adk2.com
adobe.com
helpx.adobe.com
manage.com
list-manage.com
graph.facebook.com
Example output "repeats.txt" file:
adk2.com (different top level domain than adk2.co but that's ok)
helpx.adobe.com
list-manage.com (not subdomain of manage.com but that's ok)
I would then take and eliminate the repeats from the parent, leaving a list of "unique" subdomains and domains. I have this in a separate script.
Example final list with my current script:
adk2.co
adobe.com
manage.com
graph.facebook.com (it's not facebook.com because facebook.com wasn't in the original list.)
Ideal final list:
adk2.co
adk2.com (since adk2.co and adk2.com are actually distinct domains)
adobe.com
manage.com
graph.facebook.com
Below is my code:
I've taken my hosts list (parent.txt) and checked it against itself, and spit out any matches into a new file.
$parent = Get-Content("parent.txt")
$hosts = Get-Content("parent.txt")
$repeats =#()
$out_file = "$PSScriptRoot\repeats.txt"
$hosts | where {
$found = $FALSE
foreach($domains in $parent){
if($_.Contains($domains) -and $_ -ne $domains){
$found = $TRUE
$repeats += $_
}
if($found -eq $TRUE){
break
}
}
$found
}
$repeats = $repeats -join "`n"
[System.IO.File]::WriteAllText($out_file,$repeats)
This seems like a really inefficient way to do it since I'm going through each element of the array. Any suggestions on how to best optimize this? I have some ideas like putting more conditions on what elements to check and check against, but I feel like there's a drastically different approach that would be far better.
First, a solution based strictly on shared domain names (e.g., helpx.adobe.com and adobe.com are considered to belong to the same domain, but list-manage.com and manage.com are not).
This is not what you asked for, but perhaps more useful to future readers:
Get-Content parent.txt | Sort-Object -Unique { ($_ -split '\.')[-2,-1] -join '.' }
Assuming list.manage.com rather than list-manage.com in your sample input, the above command yields:
adk2.co
adk2.com
adobe.com
graph.facebook.com
manage.com
{ ($_ -split '\.')[-2,-1] -join '.' } sorts the input lines by the last 2 domain components (e.g., adobe.com):
-Unique discards duplicates.
A shared-suffix solution, as requested:
# Helper function for (naively) reversing a string.
# Note: Does not work properly with Unicode combining characters
# and surrogate pairs.
function reverse($str) { $a = $str.ToCharArray(); [Array]::Reverse($a); -join $a }
# * Sort the reversed input lines, which effectively groups them by shared suffix
# with the shortest entry first (e.g., the reverse of 'manage.com' before the
# reverse of 'list-manage.com').
# * It is then sufficient to output only the first entry in each group, using
# wildcard matching with -notlike to determine group boundaries.
# * Finally, sort the re-reversed results.
Get-Content parent.txt | ForEach-Object { reverse $_ } | Sort-Object |
ForEach-Object { $prev = $null } {
if ($null -eq $prev -or $_ -notlike "$prev*" ) {
reverse $_
$prev = $_
}
} | Sort-Object
One approach is to use a hash table to store all your parent values, then for each repeat, remove it from the table. The value 1 when adding to the hash table does not matter since we only test for existence of the key.
$parent = #(
'adk2.co',
'adk2.com',
'adobe.com',
'helpx.adobe.com',
'manage.com',
'list-manage.com'
)
$repeats = (
'adk2.com',
'helpx.adobe.com',
'list-manage.com'
)
$domains = #{}
$parent | % {$domains.Add($_, 1)}
$repeats | % {if ($domains.ContainsKey($_)) {$domains.Remove($_)}}
$domains.Keys | Sort
I have an array $vhdlist with contents similar to the following filenames:
UVHD-S-1-5-21-8746256374-654813465-374012747-4533.vhdx
UVHD-S-1-5-21-8746256374-654813465-374012747-6175.vhdx
UVHD-S-1-5-21-8746256374-654813465-374012747-8147.vhdx
UVHD-template.vhdx
I want to use a regex and be left with an array containing only SID portion of the filenames.
I am using the following:
$sids = foreach ($file in $vhdlist)
{
[regex]::split($file, '^UVHD-(?:([(\d)(\w)-]+)).vhdx$')
}
There are 2 problems with this: in the resulting array there are 3 blank lines for every SID; and the "template" filename matches (the resulting line in the output is just "template"). How can I get an array of SIDs as the output and not include the "template" line?
You seem to want to filter the list down to those filenames that contain an SID. Filtering is done with Where-Object (where for short); you don't need a loop.
An SID could be described as "S- and then a bunch of digits and dashes" for this simple case. That leaves us with ^UVHD-S-[\d-]*\.vhdx$ for the filename.
In combination we get:
$vhdlist | where { $_ -Match "^UVHD-S-[\d-]*\.vhdx$" }
When you don't really have an array of strings, but actually an array of files, use them directly.
dir C:\some\folder | where { $_.Name -Match "^UVHD-S-[\d-]*\.vhdx$" }
Or, possibly you can even make it as simple as:
dir C:\some\folder\UVHD-S-*.vhdx
EDIT
Extracting the SIDs from a list of strings can be thought as a combined transformation (for each element, extract the SID) and filter (remove non-matches) operation.
PowerShell's ForEach-Object cmdlet (foreach for short) works like map() in other languages. It takes every input element and returns a new value. In effect it transforms a list of input elements into output elements. Together with the -replace operator you can extract SIDs this way.
$vhdlist | foreach { $_ -replace ^(?:UVHD-(S-[\d-]*)\.vhdx|.*)$,"`$1" } | where { $_ -gt "" }
The regex back-reference for .NET languages is $1. The $ is a special character in PowerShell strings, so it needs to be escaped, except when there is no ambiguity. The backtick is the PS escape character. You can escape the $ in the regex as well, but there it's not necessary.
As a final step we use where to remove empty strings (i.e. non-matches). Doing it this way around means we only need to apply the regex once, instead of two times when filtering first and replacing second.
PowerShell operators can also work on lists directly. So the above could even be shortened:
$vhdlist -replace "^UVHD-(S-[\d-]*)\.vhdx$","`$1" | where { $_ -gt "" }
The shorter version only works on lists of actual strings or objects that produce the right thing when .ToString() is called on them.
Regex breakdown:
^ # start-of-string anchor
(?: # begin non-capturing group (either...)
UVHD- # 'UVHD-'
( # begin group 1
S-[\d-]* # 'S-' and however many digits and dashes
) # end group 1
\.vhdx # '.vhdx'
| # ...or...
.* # anything else
) # end non-capturing group
$ # end-of-string anchor
I have an array of Objects. I want to search the array to determine if the a specific value exists in $obj.NewName. If the value is unique, then put the value in $obj.NewName. If it does exist, increment value and search again until the value is unique.
Function CreateObject ($origionalName)
{
$newName = $null
$obj = $null
$obj = New-Object PSObject
$obj | Add-Member -type NoteProperty -Name OrigionalName -Value $origionalName
$obj | Add-Member -type NoteProperty -Name NewName -Value $newName
Return ($obj)
}
Function Get-NewPatchName ($patch)
{
$patch.OrigionalPatchName.split("-") | Foreach {
## Pipelined string is $_. Check if it starts with "KB".
If ($_.substring(0,2) -like "kb" )
{
## This create the new name based on the substring containing "KB...", and adds the existing file extension.
$tempName = $_
## If there is a version number in the original file name, include that in the new name
If ($patch.OrigionalPatchName -like "*-v*")
{
$intPosition = ($patch.OrigionalPatchName.ToUpper()).IndexOf("-V")
$tempName += ($patch.OrigionalPatchName.substring($intPosition+1,2))
}
## Determine if name is unique
{
## STUCK HERE!!!!!!!!!!
}
}
}
}
$patchList = #()
$nameList = "AMD64-all-windows6.0-kb2900986-x64_63", `
"AMD64-all-windows6.0-kb3124275-x64_57", `
"AMD64-all-windows6.0-kb2900986-x64_63"
Foreach ($name in $nameList){
$patchList += CreateObject $name }
Foreach ($patch in $patchList) {
Get-NewPatchName $patch }
Foreach ($patch in $patchList) {
Write-Host "Origional Patch Name: " $patch.OrigionalName
Write-Host "New Patch Name : " $patch.NewName
}
Expected results:
Origional Patch Name: AMD64-all-windows6.0-kb2900986-x64_63
New Patch Name : kb2900986
Origional Patch Name: AMD64-all-windows6.0-kb3124275-x64_57
New Patch Name : kb3124275
Origional Patch Name: AMD64-all-windows6.0-kb2900986-x64_63
New Patch Name : kb2900986a
I would process $origionalName down to it's kb#, Example:
$tempName = "kb2900986"
Then I want to see if $tempName already exists in patchList under any $patch.newName. If it does, then I would increment the $tempName to
$tempName = "kb2900986a"
and run the search again. Once $tempName is unique
$patch.newName = $tempName
I already figured out how to process and increment the $tempName. I'm just stuck on how to search the $patchList to determine if the $tempName currently exists for any $patch.newName.
I'm working under the assumption you are using a newer version of PS, this WILL NOT work in PS2 as it makes use of a somewhat sloppy feature of newer versions of the shell that makes life much easier for the task you are tackling. code below would go in the area you've identified with the comments.
if($patchlist.newname -contains $tempname){
*do whatever you need to do if the list already contains the name*
} else {
*do whatever you need to do if the list doesn't contain the name*
}
This makes use of a powershell feature that will automatically access the .newname property of each item in the $patchlist array and present it as it's own array of values when calling $patchlist.newname its an interesting array feature that was introduced in PS3 I believe. I'd also make sure your function stays atomic by passing in patchlist(preferably with a different name) but that's not really necessary.
The PS2 Version
$templist = foreach($name in $patchlist){
$name.NewName
}
if($templist -contains $tempname){
*do whatever you need to do if the list already contains the name*
} else {
*do whatever you need to do if the list doesn't contain the name*
}
That will work for any version of powershell, it's not exactly optimized(creates the $templist array for each iteration) but it works and you can make changes as needed if you need additional speed
Mike Garuccio's helpful answer contains important pointers to the solution.
Here's an idiomatic PowerShell v2+ solution that also streamlines the code in the question:
# Convert an array of patch names to custom objects containing
# an .OriginalName property with the input patch name, and a yet-to-be-assigned
# .NewName property.
Function CreateObjects ([string[]] $originalNames)
{
foreach ($name in $originalNames) {
[pscustomobject] #{ OriginalName = $name; NewName = $null }
}
}
# Assign a unique name to the .NewName property of all input objects.
Function Set-NewPatchNames ([pscustomobject[]] $patches)
{
# Initialize an aux. hashtable in which we'll keep track of the unique
# new names assigned so far.
$htNewNames = #{}
foreach ($patch in $patches) {
# Extract the KB number from the original name.
# Note: For brevity, the "-v" version-number extraction mentioned in the
# question is skipped.
$newNameCandidate = $patch.OriginalName -replace '.*-kb(\d+).*', '$1'
# Is the candidate for the new name unique?
if ($htNewNames.Contains($newNameCandidate)) { # Name already used.
# Find a unique variation of the name.
$rootName = $newNameCandidate
# Loop and append 'a', 'b', ... until a unique name is found.
# Note: With more than 26 duplicates, you'll run out of letters,
# at which point seemingly random Unicode chars. will be used.
$suffixCharCode = ([int] [char] 'a') - 1
do {
++$suffixCharCode
$newNameCandidate = $rootName + [char] $suffixCharCode
} while ($htNewNames.Contains($newNameCandidate))
}
# Assign the new name and record it in the hashtable of names used so far.
$patch.NewName = $newNameCandidate
$htNewNames.Add($newNameCandidate, $null)
}
}
# The input list of patch names.
$nameList = "AMD64-all-windows6.0-kb2900986-x64_63",
"AMD64-all-windows6.0-kb3124275-x64_57",
"AMD64-all-windows6.0-kb2900986-x64_63"
# Create a collection of custom objects with the original patch name
# stored in .OriginalName, and a yet-to-be-filled .NewName property.
$patchList = CreateObjects $nameList
# Fill the .NewName properties with unique names.
Set-NewPatchNames $patchList
# Output the resulting objects.
$patchList
In PSv5.1, this yields (the code does work correctly in PSv2, but yields slightly less readable output):
OriginalName NewName
------------ -------
AMD64-all-windows6.0-kb2900986-x64_63 2900986
AMD64-all-windows6.0-kb3124275-x64_57 3124275
AMD64-all-windows6.0-kb2900986-x64_63 2900986a
So if I create an array from CSV file
name,age,height,fruit
Jon,34,5,orange
Jane,23,4,apple
Dave,27,6,pear
I can read this in using
$list = Import-CSV .....
but now i have the question. "tell me about Jane"
So I could say
foreach ($val in $list)
{
if ($val.name = "jane" ) {$currentuser = $val}
}
write-host $currentuser.name
write-host $currentuser.age
write-host $currentuser.hight
write-host $currentuser.fruit
Is there a better way to do this rather than stepping through? In my actually case i have a list of staff from HR and a separate one from Active directory.
I want to step through the HR list to find the user in AD, set this user as a variable/object. and then update the user using information from HR.
The above method will work but seems very inefficient to be loping through two lists of several thousand users.
Given the array created from the CSV, I want a method that by inputting the string "jane" it will return jane's info to a object i can use.
If you have two lists, both with distinct keys by which the two can be correlated, the best way is to store one list in a lookup table (any type of dictionary will do, including a hashtable in PowerShell), and then loop sequentially through the other list:
$HRList = #'
Name,Position,Department
John,Manager,Sales
Bob,Worker,Production
Sally,Accountant,Finance
'# |ConvertFrom-Csv
$ADList = #'
Name,Username
Sally,sallysalt
Bob,bobburrows
John,johnjames
'# |ConvertFrom-Csv
# Convert the AD list to a hash table
$ADLookupTable = #{}
foreach($ADUser in $ADList)
{
$ADLookupTable[$ADUser.Name] = $ADUser
}
# Go through HR list
foreach($HRUser in $HRList)
{
# Now you can find the relevant object in the other list
$ADUser = $ADLookupTable[$HRUser.Name]
Set-ADUser -Identity $ADUser.Username -Title $ADUser.Position -Department $ADUser.Department
}