Powershell add-member. Add a member that's an ArrayList? - arrays

The Powershell "add-member" command is very useful. I use it to add properties to custom objects. Sometimes I set a member as an array to hold multiple objects. Is it possible to add an ArrayList as a member on a custom object?
Imagine a list of articles has properties "index", "title", and "keywords." In Powershell, you could put this code in a loop:
for($i = 0; $i -lt 100; $i++) {
$a = new-object -TypeName PSObject
$a | Add-Member -MemberType NoteProperty -Name index -Value $i
$a | Add-Member -MemberType NoteProperty -Name title -Value "Article $i"
$a | Add-Member -MemberType NoteProperty -Name keywords -Value #()
$articles += $a
}
You'd end up with an array, $articles, of Article objects, each with members index, title, and keywords. Furthermore, the keywords member is an array that can have multiple entries:
$articles[2].keywords += "Stack Exchange", "Powershell", "ArrayLists"
$articles[2].keywords[2]
Powershell
This meets most of my needs, but I just don't like dealing with arrays. ArrayLists are just easier to work with, if only because
$arrayList1.remove("value")
is so much more intuitive than
$array1 = $array1 |? {$_ new "value"}
Is there a way with Add-Member to add an ArrayList as a member? Or am I stuck with arrays? If Powershell doesn't support thi snatively, could I pop in some C# code to make a new class with an ArrayList as a member?

$arr = #("one","two","three")
$arr.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
$a = new-object -TypeName PSObject
[System.Collections.ArrayList]$arrList=$arr
$a | Add-Member -MemberType NoteProperty -Name ArrayList -value $arrlist
$a.ArrayList
one
two
three
$a.ArrayList.remove("one")
$a.ArrayList
two
three
To add a blank ArrayList to your custom object just use
$a | Add-Member -MemberType NoteProperty -Name ArrayList -value (New-object System.Collections.Arraylist)

I find all this Add-Member stuff to be confusing for data only. In powershell 3, you can just make an object from a hashtable, and use a little thing I learned from a blog about using Invoke to get a Collection typed object:
$myObject = [PSCustomObject]#{
index = $idx;
title = $title;
keywords = {#()}.Invoke()
}
$myObject.keywords.Add("foo")
$myObject.keywords.Add("bar")
Write-Host "Original"
$myObject.keywords
Write-Host
Write-Host "New:"
[void]$myObject.keywords.Remove("foo")
$myObject.keywords
Write-Host
Original
foo
bar
New:
bar

Related

Adding array elements to a PSObject

Preface: I haven't any formal training with script writing. So I'm sure many of you will be banging you head against the table wondering what I'm doing.
I am working to gather information on our current catalog of Teams sites, and respective site owners. I have a PS script to gather the list of sites, then it loops through each site to get list of owners. After I gather that owner list (stored in a var) I loop through that var to get the user name and store each user name in an array as a new element. That element is then used later when adding a new member to a PSObject so I can export the results to a CSV. Here's a bit of the code;
#Var's
$Count = 1
$TSO = New-Object PSObject
$SiteOwner = #("SiteOwner0","SiteOwner1","SiteOwner2","SiteOwner3","SiteOwner4","SiteOwner5","SiteOwner6","SiteOwner7") #Array to create column headers in the CSV
#STEP 1: Get a list of the sites
$Sites = get-team | sort-object DisplayName
#STEP 2: Get a list of the owners of the different sites
FOREACH ($i in $Sites){
$h = Get-TeamUser -GroupID $i.GroupID | Where {$_.role -eq "Owner"}
$Count += 1
Write-Host . -ForeGroundColor "Cyan" -NoNewLine
#Add the new columns to $TSO
$TSO | Add-Member -MemberType NoteProperty -Name "SiteName" -Value $i.DisplayName -Force
$TSO | Add-Member -MemberType NoteProperty -Name "GroupOwner" -Value $i.Description -Force
$TSO | Add-Member -MemberType NoteProperty -Name "Visibility" -Value $i.Visibility -Force
$TSO | Add-Member -MemberType NoteProperty -Name "Archived" -Value $i.Archived -Force
$TSO | Add-Member -MemberType NoteProperty -Name "SiteEmail" -Value $i.MailNickname -Force
#loop through each member to discover the assigned role
FOREACH ($o in $h){
Write-Host . -ForeGroundColor "Cyan" -NoNewLine
#Build the dynamics of the owners before passing to $TSO
$OHolder += $o.user
} #END NESTED FOREACH
#Handle BLANK elements in the $OHolder array
#The number 8 is the number of array elements we need a value for
$Comp = 0
While($Comp -lt 8){
If($OHolder[$Comp] -ne $null){
$TSO | Add-Member -MemberType NoteProperty -Name $SiteOwner[$Comp] -Value $OHolder[$Comp] -Force
}
ELSEIF(-not ($OHolder[$Comp])){
$OHolder[$Comp] = "NA"
$TSO | Add-Member -MemberType NoteProperty -Name $SiteOwner[$Comp] -Value $OHolder[$Comp] -Force
}
#Increment the $Comp
$Comp += 1
}#END WHILE
#Resetting the $Comp outside the loop
$Comp = 0
#************* END STEP 2 *************************
#Squirt out the info to a CSV
$TSO | Export-CSV -Path $OPathOut -NoTypeInformation -Append -Force
#Reset the $OHOLDER Array
$OHolder = #()
} #END FOREACH
My problem is this; When adding the site owner to the PSObject for siteowner ($TSO | Add-Member -MemberType NoteProperty -Name $SiteOwner[$Comp] -Value $OHolder[$Comp] -Force) it's counting each character of the value as an element.
For example: $OHolder will have two elements, $OHolder[0] is supposed to equal "doe#domain.com" and $OHolder[1] is suppose to equal "john#domain.com". What actually happens is the length of the array becomes 29 (for each character) rather than 2.
How can I add the users to the PSObject as intended? Right now the output will have:
SiteOwner0 | SiteOwner1 | SiteOwner2
d | o | e
# |d |o
Do not use += to add items to an array. It is inefficient if the arrays become large. A new array is created each time after the current array contents are read into memory. You can simply output each item inside of the foreach and assign the array variable to the foreach output.
$OHolder = #(FOREACH ($o in $h){
Write-Host . -ForeGroundColor "Cyan" -NoNewLine
#Build the dynamics of the owners before passing to $TSO
$o.user # Output
})
In the simplistic example above, a loop is likely not even necessary as $OHolder = ,$h.user should suffice in PowerShell v3+.
You are not defining $OHolder variable before using += operator.
By default powershell seems to define a string variable in this case.
$undefVar = $null
$undefVar += "test"
$undefVar += "some"
# content of $undefVar is now testsome
Try to move $OHolder = #() before the loop FOREACH ($o in $h){

Update different arrays via a common function

I'm currently making a Powershell script that will analyze multiple log files from a mail server to gather various statistics that will be stored in a number of different arrays.
I have the following code snippet as an example of updating one of the arrays.
#Update arrays
#Overall array
$objNewValue = New-Object -TypeName PSObject
$objNewValue = $PSOSSLOverall | Where-Object {($_.Version -contains $strVersion -and $_.Cipher -contains $strCipher -and $_.Bits -contains $strBits)}
If ($objNewValue -ne $null) {
try {
Write-Verbose "$strVersion $strCipher $strBits is already in the array, so we'll update TimeSeen value"
$objNewValue.TimesSeen++
$objNewValue.LastSeen = $dtTimestamp
} #try
catch {
Write-Host "Something whent wrong while attempting to update an existing object in the overall array" -BackgroundColor DarkRed
Write-Host "Current line: $strtemp[$i]"
Write-Host "Current values: $dtTimestamp <-> $strVersion <-> $strCipher <-> $strBits"
Write-Host "Current array:"
$PSOSSLOverall | Sort-Object -Property Version, Cipher -Descending | Format-Table -AutoSize
Write-Host "Exception object:"
$_
} #catch
} #If Check for existence in Overall array
Else {
try {
Write-Verbose "$strVersion $strCipher $strBits is not in the array, so it will be added "
$objNewValue = New-Object -TypeName PSObject
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Version' -Value $strVersion
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Cipher' -Value $strCipher
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Bits' -Value $strBits
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'TimesSeen' -Value 1
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Percentage' -Value 0
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'FirstSeen' -Value $dtTimestamp
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'LastSeen' -Value $dtTimestamp
$PSOSSLOverall += $objNewValue
} #try
catch {
Write-Host "Something whent wrong while attempting to add a new object to the overall array"
Write-Host "Current line: $strtemp[$i]"
Write-Host "Current values: $dtTimestamp <-> $strVersion <-> $strCipher <-> $strBits"
Write-Host "Exception object:"
$_
} #catch
} #Else Check for existence in Overall array
However, when I have up to 10 or more arrays that I need to update, the result will be a lot of similar code as there's only relatively few lines that change each time - like the array being updated, the where clause, the variables used and number of columns in the arrays.
Would it be possible to create a function that can handle updating the different arrays?
Thanks in advance.
-Update-
To explain the code snippet above: All the variables are already set before this part of the script is run. $strtemp[$i] is actually where all the data comes from as that the current line in the log file from which I then extract the needed data and place it in various variables.
First I search the array in question, which in this case is $PSOSSLOverall, to see if the data from the current line is already in the array. If $objNewValue is not null, then the data is already there, and I then increment a counter and update a date stamp for that "row" of data. If $objNewValue is null, then the data is not already there, and then we added a now object (row) to the array with the data from various variables.
Each attempt is equipped with try/catch section for error handling.
The end result will be an array that looks like this (the percentage column is calculated elsewhere):
The other arrays have various number of columns, which I what I guess makes it difficult to make a common function to update them.
I have found myself in a similar situation once or twice. You'll have to refactor your code, converting inflexible static definitions into parameter variables. The idea is to separate the data from program logic so you can do something like pass a different set of attribute names and values into the same function for different circumstances.
Here were a couple places I found to improve flexibility:
Parameterize the Where-Object expressions used to find matching records so you don't write the same code for each combination of columns and values. See hasPropertyValues below. All it does is perform an -and concatenated -contains operation on each name-value pair you pass to it.
Parameterize the data you want changed for each eventuality. Your code does something when it finds matching records and when it finds no matching records. Pull those actions out of the script body and into an input parameter that can change when you're done working with the encryption dataset and have to move onto another. The UpdateRecords function takes hashtable parameters defining the shape of the data when new records are added and when matching records are found.
See below for the example. I think you can adapt some of the ideas in here to your code.
# this is a convenience function allowing us to test multiple name-value pairs against an object
function hasPropertyValues {
Param(
[object] $inputObject,
[hashtable] $properties
)
$result = $true
foreach($name in $properties.Keys){
$result = $result -and ($inputObject.$name -contains $properties[$name])
}
Write-Output $result
}
# this function evaluates each object in $inputDataset
# if an object matches the name-values defined in $matchProperties
# it updates the records according to $updateRecordProperties
# if no records are found which match $matchProperties
# a new object is created with the properties in both $matchProperties
# and $newRecordProperties
# All results are written to the pipeline, including unmodified objects
function UpdateRecords{
Param (
[object[]] $inputDataset,
[hashtable] $matchProperties,
[hashtable] $updateRecordProperties,
[hashtable] $newRecordProperties
)
$numberOfMatchingRecords = 0
foreach ($record in $inputDataset){
if ( hasPropertyValues -inputObject $record -properties $matchProperties) {
# a record with matching property values found.
# Update required attributes
$numberOfMatchingRecords++
foreach($name in $updateRecordProperties.Keys){
if ($updateRecordProperties[$name] -is 'ScriptBlock'){
# if the update is a scriptblock, we invoke the scriptblock
# passing the record as input. The result of the invocation
# will be set as the new attribute value
$newValue = & $updateRecordProperties[$name] $record
} else {
$newValue = $updateRecordProperties[$name]
}
$record | Add-Member -Force -MemberType NoteProperty -Name $name -Value $newValue
}
}
Write-Output $record
}
if ($numberOfMatchingRecords -eq 0) {
# no records found with the search parameters
$newRecord = New-Object -TypeName psobject -Property $newRecordProperties
foreach($key in $matchProperties.Keys){
$newRecord | Add-Member -MemberType NoteProperty -Name $key -Value $matchProperties[$key] -Force
}
Write-Output $newRecord
}
}
[object[]] $TestDataset= #(New-Object psobject -Property #{
version='TLSv1.2'
cipher='ECDHE-RSA-AES256-GCM-SHA384'
Bits=256
TimesSeen = 1833
Percentage = 87578
FirstSeen = [DateTime]::Now
LastSeen = [DateTime]::Now
})
function TestUpdateRecords{
$encryptionNewRecordDefaults = #{
TimesSeen = 1
Percentage = 0
FirstSeen = [DateTime]::Now
LastSeen = [DateTime]::Now
}
$encryptionUpdateAttributes = #{
LastSeen = [DateTime]::Now
TimesSeen = {$ARGS[0].TimesSeen + 1}
}
# test adding a new record
UpdateRecords -inputDataset $TestDataset `
-matchProperties #{ Version='TLSv1.0';cipher='unbreakable';bits=720} `
-updateRecordProperties $encryptionUpdateAttributes `
-newRecordProperties $encryptionNewRecordDefaults
# test modifying a matching record
UpdateRecords -inputDataset $things `
-matchProperties #{Version='TLSv1.2';cipher='ECDHE-RSA-AES256-GCM-SHA384';bits=256} `
-updateRecordProperties $encryptionUpdateAttributes `
-newRecordProperties $encryptionNewRecordDefaults
}
TestUpdateRecords
There are a lot of different ways to implement this kind of refactoring. You could, for instance, extract dataset-specific logic into scriptblocks and pass these to your main loop function.
Another possibility is to dig into the object-oriented features of PowerShell and try to build classes around each of your datasets. This could encapsulate the 'Update' and 'New' actions in a pleasant way. I'm not yet literate enough in powershell OO features to try.

How to add a custom property to a PowerShell array?

Say I have a PowerShell array $Sessions = #() which I am going to fill with PSCustomObjects. How can I add a custom property to the array itself? E.g. so I can have $Sessions.Count which is built-in and $Sessions.Active which I want to set to the active session count.
I know that I can add properties to PSCustomObjects (in a dirty way) with
$MyCustomObject = "" | Select-Object Machine, UserName, SessionTime
but though doing so on an array would not result in the property being added.
So how can I achieve my goal? Is there any way to create a custom array?
The answer to your question as stated would be to just use Add-Member on the array object.
Add-Member -InputObject $sessions -MemberType NoteProperty -Name "State" -Value "Fabulous"
Adding a property to each element after you created the object is similar.
$sessions | ForEach-Object{
$_ | Add-Member -MemberType NoteProperty -Name "State" -Value "Fabulous"
}
This of course comes with a warning (that I forgot about). From comments
Beware, though, that appending to that array ($sessions += ...) will replace the array, thus removing the additional property.
Ansgar Wiechers
Depending on your use case there are other options to get you want you want. You can save array elements into distinct variables:
# Check the current object state
$state = $object.Property .....
# Add to the appropriate array.
if($state -eq "Active"){
$activeSessions += $object
} else {
$inactiveSessions += $object
}
Or you could still store your state property and post process with Where-Object as required:
# Process each inactive session
$sessions | Where-Object{$_.State -eq "Active"} | ForEach-Object{}
To avoid the destroying / recreating array issue, which can be a performance hog, you could also use an array list instead.
$myArray = New-Object System.Collections.ArrayList
Add-Member -InputObject $myArray -MemberType ScriptMethod -Name "NeverTellMeTheOdds" -Value {
$this | Where-Object{$_ % 2 -ne 0}
}
$myArray.AddRange(1..10)
$myArray.NeverTellMeTheOdds()
Notice that the array had its member added then we added its elements.
As Matt commented, you can use the Add-Member on an enumerable type by supplying it as a positional argument to the -InputObject parameter.
To allow for resizing after adding the new property, use a generic List instead of #():
$list = [System.Collections.Generic.List[psobject]]::new()
$list.AddRange(#(
[pscustomobject]#{SessionId = 1; Active = $true}
[pscustomobject]#{SessionId = 2; Active = $false}
[pscustomobject]#{SessionId = 3; Active = $true}
) -as [psobject[]])
Add-Member -InputObject $list -MemberType ScriptProperty -Name ActiveSessionCount -Value {
return #($this |? Active -eq $true).Count
}
Now you can retrieve the active session count easily:
PS C:\> $list.ActiveSessionCount
2

Remove one or many members from Object in powershell

I have created a custom object called $info and moving it to an array $arr ,
How is it possible to remove one member along with its all properties ?
My script:
Get-Process | ForEach-Object{
$info = New-Object -TypeName PSObject
$info | Add-Member -Type NoteProperty -Name Process -Value $_.processname
$info | Add-Member -Type NoteProperty -Name ID -Value $_.id
$arr += $info
}
$arr | ft -AutoSize
The result looks like this :
Process ID
------- --
ApplicationFrameHost 38556
AppVShNotify 9792
armsvc 2336
atieclxx 6944
atiesrxx 1844
audiodg 59432
CcmExec 3988
chrome 46068
How can I remove one particular member for example "audiodg 59432" gets removed
audiodg 59432
Your terminology is a bit incorrect here. A member is on an individual object. When you use Add-Member above you're adding properties to each individual object, then you're returning an array of objects.
You're asking how to remove an individual object from the array.
In PowerShell you cannot remove an item from an array. You could instead filter the array based on some criteria and create a new one:
$newArr = $arr | Where-Object { $_.Name -ne 'audiodg' }
# or
$newArr = $arr | Where-Object { $_.ID -ne 59432 }

Create Powershell Object From Loop Containing Multiple Users

I need to create a Powershell object, array or hash table to store a list of users and assorted details, these are extracted from a CSV file and located using Get-ADUser. As below:
$userList = Import-CSV $CSVInputFile
$users = #{}
Foreach ($csvUser in $userList)
{
$userSearchString = $csvUser | Select -ExpandProperty SamAccountName
$currentUser = (Get-ADUser -Filter {SamAccountName -eq $userSearchString} `
-Properties PasswordExpired,PasswordLastSet,EmailAddress |
Where {$_.Enabled -eq "True"})
If ($currentUser.EmailAddress -ne $null)
{
$currentUserEmailString = $csvUser | Select -ExpandProperty EmailAddress
$currentUserEmailString = ($currentUserEmailString -as [string])
$currentUser.EmailAddress = $currentUserEmailString
}
$Users = New-Object PSObject -Property #{
DistinguishedName = $currentUser.DistinguishedName
EmailAddress = $currentUser.EmailAddress
Enabled = $currentUser.Enabled
GivenName = $currentUser.GivenName
Name = $currentUser.Name
PasswordExpired = $currentUser.PasswordExpired
PasswordLastSet = $currentUser.PasswordLastSet
SamAccountName = $currentUser.SamAccountName
Surname = $currentUser.Surname
}
$Users
}
How can I add the details of each user for each iteration of the loop to the object.
I want to end up with an object containing the details of a number of users, same as the output directly from Get-ADUser:
Name SamAccountName EmailAddress
---- -------------- ------------
User1 user1 user1#domain.com
User2 user2 user2#domain.com
User3 user3 user3#domain.com
Any help would be greatly appreciated!
Not sure if I'm missing the point on this but I see you are building a custom object right in your loop. The only issue I do see is you are not keeping the results after each loop. Rather you are destroying the objects history.
I would change the declaration of $users to an array $users = #() and instead of populating a user hashtable into users add the current object into the array. You will then have an array of hashtables:
$Users += New-Object PSObject -Property #{...
Then you could the $Users output line outside the loop and you will have the whole thing. Then you could just output to a Select to get the output you desire.
$Users | Select-Object name,SamAccountName,EmailAddress
There is a potential major drawback of this approach though. When using += on arrays a new array is created and resized for the new element and the old array is discarded. This has huge performance implications for larger arrays.
An even better way to approach this would be to leverage the pipeline. This would be a performance boost when you have larger user groups.
Import-CSV $CSVInputFile | ForEach-Object{
$userSearchString = $_.SamAccountName
$currentUser = Get-ADUser -Filter {SamAccountName -eq $userSearchString} `
-Properties PasswordExpired,PasswordLastSet,EmailAddress |
Where {$_.Enabled -eq "True"}
If ($currentUser.EmailAddress -ne $null){
$currentUser.EmailAddress = $_.EmailAddress
}
[pscustomobject][ordered]#{
DistinguishedName = $currentUser.DistinguishedName
# ..... truncated
Surname = $currentUser.Surname
}
}
Now you could send that to something like Export-CSV or just save it into a variable. Your options are open now. [pscustomobject][ordered] are type accelerators available in PowerShell v3.0+
Define an $users as Array
$users = #()
and append the New-Object into $Users.
$Users += New-Object
Can't believe both of you guys got in before me! Oh well.
Hope this helps anyway.
$userList = Import-CSV $CSVInputFile
$users = #()
Foreach ($csvUser in $userList)
{
$userSearchString = $csvUser | Select -ExpandProperty SamAccountName
$currentUser = (Get-ADUser -Filter {SamAccountName -eq $userSearchString} `
-Properties PasswordExpired,PasswordLastSet,EmailAddress |
Where {$_.Enabled -eq "True"})
If ($currentUser.EmailAddress -ne $null)
{
$currentUserEmailString = $csvUser | Select -ExpandProperty EmailAddress
$currentUserEmailString = ($currentUserEmailString -as [string])
$currentUser.EmailAddress = $currentUserEmailString
}
#clears the properties of the previous object and starts collecting properties
$UserObj = New-Object PSObject
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "DistinguishedName" -Value $($currentUser.DistinguishedName)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "EmailAddress" -Value $($currentUser.EmailAddress)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "Enabled" -Value $($currentUser.Enabled)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "GivenName" -Value $($currentUser.GivenName)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "UserName" -Value $($currentUser.Name)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "PasswordExpired" -Value $($currentUser.PasswordExpired)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "PasswordLastSet" -Value $($currentUser.PasswordLastSet)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "SamAccountName" -Value $($currentUser.SamAccountName)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "Surname" -Value $($currentUser.Surname)
#saves the properties in an array that exists outside of the loop to preserve information beyond one interation
$users += $UserObj
}
$users | Format-Table -Property UserName,SamAccountName,EmailAddress

Resources