I have an object named $restoreItems that I would like to filter for duplicates property values and select the most recent one.
Please have a look at its members first to understand what I'm working with:
In order to be able to check for duplicates I need a combination of the 'DirName' + 'LeafName' properties as this combination builds a complete file path.
This is my first issue. I am not able to combine the two properties and add it to that object. I tried to add a new member like so:
$restoreItems | Add-Member -NotePropertyName fullPath -NotePropertyValue
($restoreItems.Dirname + '/' + $restoreItems.LeafName)
This command runs fine but upon calling
$restoreItems.fullPath
I get an error stating 'Array dimensions exceeded supported range.'
Is there another way to add $restoreItems.Dirname + '/' + $restoreItems.LeafName to the object? If so I could probably find a way to exclude duplicates according to their DeletedDate property.
Thanks for your help.
Edit: Here's .GetType() for the object:
Your statement
($restoreItems.Dirname + '/' + $restoreItems.LeafName)
builds an array of all dirnames, the string "\", and all leafnames.
What you actually want is something like this:
$restoreItems | ForEach-Object {
$fullname = Join-Path $_.DirName $_.LeafName
$_ | Add-Member -Type NoteProperty -Name FullName -Value $fullname
}
Related
I got a bunch of objects containing multiple string values. I want to manage the exceptions with a object/array consisting of multiple more and less known exceptions. Is there a simple way to get this to work? afraid my brain ain't made for this kind of problemsolving.
Example:
Exceptions =#{
Known1 = 'Minor Error'
Known2 = 'This Have We Seen Before'
Known3 = 'I Remember This Issue'
Special1 = 'Application malfunction'
Special2 = 'Application malfunction severe!'
}
$AllCases = Get-Cases
Foreach ($Case in $AllCases){
if ($Case.Name -match $Exceptions.Values.('^Known+[0-9]$')){'Do That again'}
Elseif ($Case.Name -match $Exceptions.Values.Special1){'OBS! Do This Fast'}
Elseif ($Case.Name -match $Exceptions.Values.Special2){'SEVERE OBS! do this faster!'}
}
Thanks :)
I guess you're trying to create a script to identify incidents where $case.Name whatever that could be (subject of the incident?) matches with your map of words that are commonly found, which in turn points to a specific Error Code or Error Index? If that's the case, It is probably better even though slower, to store a separate CSV file where you can add new Error Indexes and make your script read this CSV each time. In any case, I harcoded an example of how the CSV could look like and added some comments to guide you with the tought process.
$Exceptions = #"
Index,Error
Known1,Minor Error
Known2,This Have We Seen Before
Known3,I Remember This Issue
Special1,Application malfunction
Special2,Application malfunction severe!
"# | ConvertFrom-Csv
# I'll generate 500 random cases here mixing the words on Error
# I'll also added a IncidentNumber property becase I'm guessing you have something like that
$words = $Exceptions.Error -split '\s'
$AllCases = 0..500 | ForEach-Object {
$ran = Get-Random -Minimum 2 -Maximum 10
[pscustomobject]#{
IncidentNumber = Get-Random
Name = ($words | Get-Random -Count $ran) -join ' '
}
}
Example of the cases created
PS /> $AllCases | Get-Random -Count 5
IncidentNumber Name
-------------- ----
2043166219 Application This Remember Before
837011116 malfunction Error Seen Have malfunction Issue Minor severe!
2103904733 This malfunction We This I Have Seen Application
323914959 Minor This Issue Remember This Application Have Seen
1105202359 malfunction Seen We This I malfunction
Back to the script:
$result = foreach($Case in $AllCases)
{
if($Exceptions.Error -match $Case.Name)
{
# Note: I'm using -match here instead of -in or -contains
# because we want to partially match, considering that $Case.Name
# could be a much longer string
# $Exceptions array where the string in $Case.Name
# matches any element of $Exceptions.Error
$Exceptions.Where({$_.Error -match $Case.Name}) |
Select-Object #{n='IncidentNumber';e={$Case.IncidentNumber}},Index,#{n='Status';e={'FOUND'}}
continue
}
# If there are no matches you can use something like below, not sure
# you have some sort of incident number or something short you can use
# for those incidents not found :)
$Case | Select-Object IncidentNumber,#{n='Index';e={$null}},#{n='Status';e={'NOT FOUND'}}
}
Since this is all random, it is possible there will not be any matches but in this run I got these results
PS /> $result | Sort-Object Status | Select-Object -First 10
IncidentNumber Index Status
-------------- ----- ------
505966803 Special1 FOUND
235217253 Known3 FOUND
1034830172 Known3 FOUND
1047600080 Special2 FOUND
481579809 Known3 FOUND
2131683661 Known2 FOUND
505966803 Special2 FOUND
1424263573 NOT FOUND
249127953 NOT FOUND
489126244 NOT FOUND
I have the following script I wrote using PowerShell 5 that utilizes the Active Directory and Join-Object PowerShell modules to get a list of all AD Groups and their users (along with some additional properties per user like their manager and title):
$ADGroupsList = #(Get-ADGroup -Filter * -Properties * | Select-Object DistinguishedName,CN,GroupCategory,Description | Sort-Object CN)
#I'm using an ArrayList here so that later on I can use the .Add() method to avoid costly += operations.
$ADUsersList = New-Object -TypeName "System.Collections.ArrayList"
$ADUsersList = [System.Collections.ArrayList]#()
$Record = [ordered] #{
"Group Name" = ""
"Employee Name" = ""
"Title"= ""
"Manager" = ""
}
foreach ($Group in $ADGroupsList) {
$ArrayofMembers = #(Get-ADGroupMember -Identity $Group.DistinguishedName | Where-Object { $_.objectClass -eq "user" })
#Loop through each member in the list of members from above
foreach ($Member in $ArrayofMembers) {
#Get detailed user info about the current user like title and manager that aren't available from Get-ADGroupMember
$User = #(Get-ADUser -Identity $Member -Properties name,title,manager | Select-Object Name, Title, #{Label="Manager";Expression={(Get-ADUser (Get-ADUser $Member -Properties Manager).Manager).Name}})
#Specifies what values to apply to each property of the $Record object
$Record."Group Name" = $Group.CN
$Record."Employee Name" = $Member.Name
$Record."Title" = $User.Title
$Record."Manager" = $User.Manager
#Put all the stored information above in a 'copy' record
$objRecord = New-Object PSObject -property $Record
#Add that copy to the existing data in the ADUsersList object
[void]$ADUsersList.Add($objRecord)
}
#Using Join-Object here to enable me to use SQL-like JOINs
Join-Object -Left $ADUsersList -Right $ADGroupsList -LeftJoinProperty "Group Name" -RightJoinProperty "CN" -Type AllInLeft -LeftMultiMode DuplicateLines -RightMultiMode DuplicateLines -ExcludeRightProperties DistinguishedName | Export-Csv ("C:\ADReports\" + $Group.CN + " Report.csv") -NoTypeInformation
$ADUsersList.Clear()
}
Here's the output I expect (columns may be out of order, but column ordering isn't important):
My code works great for most groups, but for groups that have only one member (or none), I get an error:
Join-Object : Method invocation failed because [System.Management.Automation.PSCustomObject] does not contain a method named 'ForEach'.
At C:\GetADGroups&Users.ps1:54 char:5
+ Join-Object -Left $ADUsersList -Right $ADGroupsList -LeftJoinProp ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (ForEach:String) [Join-Object], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound,Join-Object
At first, I thought it was because I read arrays/arraylists with one entry get turned into scalars. But a knee-jerk wrapping of every object I can think of in #() didn't resolve the issue. In fact, if I wrap the $objRecord assignment (New-Object PSObject -property $Record) in #() to convert it to an array, it writes the Member Properties of $ADUsersList to the Join-Object line instead of the contents of $ADUsersList, resulting in this:
Is there somewhere I've missed an array/arraylist getting converted to a scalar? Why is the code above throwing an error for groups with <= 1 entries?
Compounding my curiosity, PowerShell 7 (possibly 6, too) doesn't seem to care about this issue; it doesn't throw the error at all (instead it just outputs the appropriate single-value/blank CSV). Normally I'd just wipe my hands and say PS 7 is required, but I'd like to get this working in PowerShell 5, or at least understand what is causing the issue.
Googling led me to several related articles & questions, including:
Method Invocation .Foreach failed, System.Object doesn't contain a method named 'foreach' this one's specific to PowerShell v2 (I'm running v5)
Method invocation failed because [System.Management.Automation.PSObject] doesn't contain a method named 'op_Addition' this one seems only tangentially related. Incidentally it's where I read that arrays with one item output as scalars, as I mentioned earlier.
It does appear that scalars lack the .ForEach() & .Where() methods in 5.1. The additional of the methods is probably just an enhancement newer version, certainly 7 not sure about 6. I'm sure that's documented somewhere.
I can't really test your code but it doesn't look like there's anywhere that could be flipping to a scalar. To help guarantee ArrayList collections through out you can type constrain the variables like [Collections.ArrayList]$Var = #() This may end up being more practical than hunting for an implementing #() throughout.
Something that stands out is the error seems to come from Join-Object I only found a single invocation of .ForEach() on line 820 of Join-Object.ps1 My guess is it's this line or similar elsewhere in the module combined with the 5.1 runtime environment.
If you can manually modify that to a traditional | ForEach-Object {...} might be telling. And/or you can wrap $result like #($Result) right before the .ForEach() is invoked.
Really interested to see what you come up with. I see you've already posted an issue with the author. Please post back if you get a reply. Thanks.
Description: I'm building a PowerShell-script that searches for files, then gives them unique names, copies them and then verifies them via hash-calculation - I chose to split the script in functions for each step, so it's easier to maintain the whole thing.
To get all values from one function to the other, I chose to use [hashtable]$FooBar - inside $FooBar, there are multiple arrays, such as FullName or OutputPath (which may change per file as they will be copied to subfolders named yyyy-mm-dd). All arrays are correlating with each other (meaning that index 1 contains all values of the first file, index 2 the values for the second file,...) and this works fine as of now.
A short simplified visualisation:
$FooBar = #{}
$FooBar.FullName = #()
$FooBar.Size = #()
$FooBar.Ext = #()
Get-ChildItem | ForEach-Object {
$FooBar.FullName += $_.FullName
$FooBar.Size += $_.Length
$FooBar.Ext += $_.Extension
}
However, I now need to sort them all by one value-set of one of the arrays, e.g. the size. Or, visualised again:
# From:
$FooBar
Name Value
---- -----
fullname {D:\AAA.XYZ, D:\BBB.ZYX, D:\CCC.YZX}
size {222, 111, 555}
extension {.XYZ, .ZYX, .YZX}
# To:
$FooBar = $FooBar | Sort-Object -Property Size -Descending
$FooBar
Name Value
---- -----
fullname {D:\CCC.YZX, D:\AAA.XYZ, D:\BBB.ZYX}
size {555, 222, 111}
extension {.YZX, .XYZ, .ZYX}
I tried $FooBar.GetEnumerator() | Sort-Object -Property Size, but this does not change anything. Google turned up suggestions on how to sort an array of hashtables, but in my case, it's the other way round, and I can't get my head around this because I don't even understand why this is a problem in the first place.
So my question is: is there any way to sort all arrays inside the hashtable by the value-set of one of the arrays? I can't get my head around this.
Disclaimer: I'm a PowerShell-autodidact with no reasonable background in scripting/programming, so it might well be that my "include everything in one hashtable"-solution isn't going to work at all or might be extremely inefficient - if so, please tell me.
The easiest way to go about what I believe you are trying to do is Select-Object
$fooBar = Get-ChildItem | Select-Object FullName, Size, Extension
This will create an array of new objects that only have the desired properties. The reason this works and your method doesn't is because Sort-Object works on properties and the property you are specifying is behind a few layers.
If you need more flexibility than just exact properties, you can create your own like this
$fooBar = Get-ChildItem | Select-Object #{Name = 'SizeMB'; Expression = {$_.Size / 1MB}}
Or manually create new properties with the [PSCustomObject] type accelerator:
$fooBar = Get-ChildItem | ForEach-Object {
[PSCustomObject]#{
FullName = $_.FullName
Extension = $_.Extension
Size = $_.Size
}
}
Update
If you need to add additional properties to the object after it's initially created you have a few options.
Add-Member
The most common method by far is by using the Add-Member cmdlet.
$object | Add-Member -MemberType NoteProperty -Name NewProperty -Value 'MyValue'
$object
Something important to keep in mind is that by default this cmdlet does not return anything. So if you place the above statement at the end of a function and do not separately return the object, your function won't return anything. Make sure you either use the -PassThru parameter (this is also useful for chaining Add-Member commands) or call the variable afterwards (like the example above)
Select-Object
You can select all previous properties when using calculated properties to add members. Keep in mind, because of how Select-Object works, all methods from the source object will not be carried over.
$fooBar | Select-Object *, #{Name = 'NewProperty'; Expression = {'MyValue'}}
psobject.Properties
This one is my personal favorite, but it's restricted to later versions of PowerShell and I haven't actually seen it used by anyone else yet.
$fooBar.psobject.Properties.Add([psnoteproperty]::new('NewProperty', 'MyValue'))
$fooBar
Each member type has it's own constructor. You can also add methods to $fooBar.psobject.Methods or either type to $fooBar.psobject.Members. I like this method because it feels more explicit, and something about adding members with members feels right.
Summary
The method you choose is mostly preference. I would recommend Add-Member if possible because it's the most used, therefore has better readability and more people who can answer questions about it.
I would also like to mention that it's usually best to avoid adding additional members if at all possible. A function's return value should ideally have a reliable form. If someone is using your function and they have to guess when a property or method will exist on your object it becomes very difficult to debug. Obviously this isn't a hard and fast rule, but if you need to add a member you should at least consider if it would be better to refactor instead.
For all practical purposes I'd strongly suggest you just store the objects you need in a single array, sort that once and then reference the individual properties of each object when needed:
$FooBar = Get-ChildItem |Sort-Object -Property Length
# Need the Extension property of the object at index 4?
$FooBar[4].Extension
To answer your actual question:
Array.Sort() has an overload that takes keys and values arrays separately. You could make a copy of the array you want to sort on for each other property you want to sort:
# Create hashtable of correlated arrays
$FooBar = #{}
$FooBar.FullName = #()
$FooBar.Size = #()
$FooBar.Ext = #()
# Types cast explicitly to avoid Array.Sort() calling .CompareTo() on the boxing object
Get-ChildItem | ForEach-Object {
$FooBar.FullName += [string]$_.FullName
$FooBar.Size += [int]$_.Length
$FooBar.Ext += [string]$_.Extension
}
# Define name of reference array property
$SortKey = 'Size'
# Sort all arrays except for the reference array
$FooBar.Keys |Where-Object {$_ -ne $SortKey} |ForEach-Object {
# Copy reference values to new array
$Keys = $FooBar[$SortKey].Clone()
# Sort values in target array based on reference values
[array]::Sort($Keys,$FooBar[$_])
}
# Finally sort the reference array
[array]::Sort($FooBar[$SortOn])
The above only works as long as the reference array is made up of value types
PowerShell makes working with objects ridiculously easy.
Try:
$FooBar = Get-Childitem
$FooBar | Get-Member
This will tell you that $Foobar actually contains objects of FileInfo and DirectoryInfo type, and show you the Properties available.
$FooBarSortedBySizeDesc = $FooBar | Sort-Object Length -Descending
$FooBarFullNamesOnly = $FooBar.FullName
I have a single dimensional array that I get from either a get-content command or from multi-line text box input. I want to assign a property to the entries in this array, then add more properties to use later in my script.
Something like:
$items = new-object psobject
$items | add-member -membertype NoteProperty –name Name –value NotSet
$items | add-member -membertype NoteProperty –name Percent –value NotSet
$names = #($textboxInputText.Lines)
$names | % { $items | Add-Member noteproperty $_.Name $temp.($_.Name) }
foreach ($item in $items)
{
$percent = {script block}
$item.percent = $percent
}
I know this is broken code, but I wanted to give an example of where I was headed. I've searched far and wide but haven't been able to find exactly what I was looking for.
EDIT:
Code Goal: Get input from a text box or text file (single line entries). Have those entries be assigned to the "name" property, then add a second property to the array (Percent) that will need to be filled in with another block of code.
EDIT 2:
Collection is being used in the following code:
foreach ($item in $collection) {
$psConsoleFile = "PATH TO FILE.pc1"
$variable1 = "something"
$variable2 = "something else"
$command = ".`"Command1 $item.name | Command2 -Switch $variable1 -Switch2 $variable2`""
$OutputScriptBlock = "powershell.exe -PSConsoleFile $psConsoleFile -command $command"
}
The output of this is as follows:
powershell.exe -PSConsoleFile "PATH TO FILE.psc1" -command ."Command1 #{Name=name1; Percentage=}.name | Command2 -Switch1 something"
Why is the code outputting the full row instead of the name?
Also, I'm using PS 4.0 for all implementations of this script.
Ok, I see the problem here. So you have an array of Strings that you got either from a multi-line text box form object, or from a text document with the Get-Content command, but what you really want is an array of PSObjects.
A string object can not have additional properties added to it like you want (well, not conventionally, let's just not go there because you won't be happy with where things end up, trust me on this one). Instead let's take that array of strings, and for each string create a PSObject for it like you want. You will want a ForEach-Object loop for this to be simple. Either way you will want to pipe your input (either the textbox or the get-content command) to a ForEach loop, and you can assign the whole thing to a variable that will collect all of the objects to be worked with later (to update the Percent property). Something like this should accomplish what you want:
[Array]$Collection = $textboxInputText.Lines | ForEach{
New-Object PSObject -Property #{
'Name' = $_
'Percentage' = $null
}
}
I specified $Collection as the type [Array] so that if you wanted to index into it later there wouldn't be any issues should your input only be a single item. Then if you want to update the percentages you can do that by running $Collection through a ForEach loop (either inline or not)
$Collection | ForEach{ $_.Percentage = {Script Block} }
or
ForEach($Item in $Collection){
$Item.Percentage = {Script Block}
}
Now, things to note here... You are not going to be able to just assign $Collection back to your textbox. You could probably assign $Collection.Name, but that may require a newer version of PS since I don't know how backwards compatible that is. If you use a Get-Content command instead of referencing the textbox, simply change $textboxInputText.Lines | ForEach{ to Get-Content "C:\Path\To\File.txt" | ForEach{ and you should be all set.
Edit: Ok, the problem you have now isn't with the object but with how you're trying to expand a property of it within a double quotes. To access the name you would have to create a sub expression within the double quotes by wrapping $Item.Name within $(). So that line for you would look like:
$command = ".`"Command1 $($item.name) | Command2 -Switch $variable1 -Switch2 $variable2`""
I would like to collect some information about hosts in the domain, so I am trying to write something like this:
# declare array for storing final data
$servers_list = #()
#start with a list of servers and go through collecting the info
$servers | ForEach-Object {
$sys = Get-WmiObject Win32_computersystem -ComputerName $_
# create new custom object to store information
$server_obj = New-Object –TypeName PSObject
$server_obj | Add-Member –MemberType NoteProperty –Name Domain –Value $sys.Domain
# .... add all other relevant info in the same manner
# Add server object to the array
$servers_list += $server_obj
}
The problem with this code is that I pass a reference to the object into array and not the actual object. So by the time my loop is finished I end up with an array that contains rows that look all the same :(
Any idea how to pass actual object into array and not just a reference to it?
Another thought is to dynamically declare new object instead of using $server_obj variable every time but I am not sure how to do this either...
Thanks!!!
You can build an array of objects and keep dynamically adding information to them like this:
#This will be your array of objects
#In which we will keep adding objects from each computer
$Result = #()
#start with a list of servers and go through collecting the info
$servers | ForEach-Object {
$sys = Get-WmiObject Win32_computersystem -ComputerName $_
# create new custom object to keep adding store information to it
$Result += New-Object –TypeName PSObject -Property #{Domain = $sys.Domain;
Name = $sys.Name;
SystemType = $sys.SystemType
}
}
# Get back the objects
$Result
Where Domain,Name and SystemType are the properties that you want to associate with the objects.
It sounds like it is passing a reference, but I don't think it's the object that's being passed as a reference, but the property values. There are discrete objects, but they all have the same reference for their property values, so they all look the same. If that's the case,
$server_obj | Add-Member –MemberType NoteProperty –Name Domain –Value "$($sys.Domain)"
should make the value a string, which is a value type and won't change.
You're making this a little harder than it should be. Pass the server names from a query, csv or list then iterate over them. Select what you want from the result.
$info = "server1", "server2" | ForEach-Object{Get-WmiObject -Class win32_computersystem -ComputerName $_ } | Select-Object Domain, Name, Systemtype
$info[1].Domain will output domain.com