Add One Object from an Array to another Array - arrays

I can't figure out what I'm doing wrong and hope someone can point me in the right direction.
I'm trying to iterate through an array of objects and testing on each object and when something is true, I want to take that object and add it to it's own array, as a single object (just like it was in the original array of objects). I seem to be adding the information to the new array, but when I reference the new array by doing newarray[0] it gives me the first item of the object, not the entire object itself.
The issue appears to be with this line:
$global:MachinesInAD += $global:MachineObject
The data in the csv file is a machine hostname, the machines IP address, an error code, and an agent ID.
e.g. MACHINENAME, 10.10.10.10, ERROR101, 123456FF
Function ReadExcelReport (){
$global:Report = "C:\TEMP\Tools\Scripts\agents.csv"
$Unresponsive = import-csv $global:Report | Where-Object {($_.State -eq "QUEUED" -or $_.State -eq "FAILED")} #Add items to array from spreadsheet where the state is equal to queued or failed
$global:UnresponsiveMachineInfo = #()
foreach ($item in $Unresponsive){
$global:UnresponsiveMachineInfo += ,#($item.'Hostname', $item.'IP Address',$item.'Error',$item.'Agent Cert ID') #Build the object - Add the following columns hostname, ip address, error, agent cert id
}
ADCheck
}
Function ADCheck (){
$Machine = $null
$global:MachinesInAD = #()
$global:MachinesNotInAD = #()
$global:MachineObject = New-Object system.object
$global:MachineObject = $Null
$global:MachinesInAD = $null
$global:UnresponsiveMachineInfo | foreach-object { #Iterate through each object in the array
$global:MachineObject = $_
$Machine = $_[0] #Set Machine to the hostname AKA the first element in the array for the current ($_) object (objects defined above)
try{
write-host "Checking A.D. for: $Machine"
if (Get-ADComputer $Machine){ #Check to see if the machine is in A.D.
write-host "Found $Machine in A.D." -ForegroundColor Green
$global:MachinesInAD += $global:MachineObject
}
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { #If the machine was NOT in A.D. catch the error it creates and...
write-warning -message "Machine $Machine not found in A.D."
$global:MachinesNotInAd += $MachineObject
}
}
}

This is happening because what you're calling an object, is just an array (which.. is an object, but your properties are elements, not properties).
Anyway, when you do this:
$global:MachinesInAD += $global:MachineObject
You end up concatenating the arrays.
#(1,2,3) + #(4,5,6)
That results in an array of 6 elements, not 3 numbers and an array.
You should use either a [hashtable] or a [PSObject] instead of an array; or as you did when you built the original one, you'll need to force it into a one elements array, something like:
$global:MachinesInAD += ,#($global:MachineObject)

Related

values from a foreach loop in a function into an array

I have a function that replaces PackageID in a SCCM task sequence, I would like to capture all those package IDs into a variable, so I would be able to create a report based on that.
The problem is that I already have a foreach loop doing the work, and I can't figure out how to not overwrite the values.
$Driver.PackageID comes from a foreach loop based on $Drivers, which contains
If I run the code I get this as I have Write-Output defined:
Updated code:
function Set-Drivers{
foreach ($Driver in $Drivers) {
Write-Output "Driver Name: $($Driver.Name)"
Write-Output "DriverPackageID: $($Driver.PackageID)"
}
}
$array = #()
$array = Set-Drivers
$hash = [ordered]#{
'DriverName' = $Driver.Name
'DriverID' = $Driver.PackageID
}
$array += New-Object -Typename PSObject -Property $hash
Can someone explain, why I only get the first result in my $array? I can see the values are being overwritten if I run it in debug mode.
Your code is not iterating over the results, but instead only using one of them. This what you intended.
$array = $drivers | foreach {
[ordered]#{
DriverName = $_.Name
DriverID = $_.PackageID
}
}
Your function doesn't return anything. It only writes lines to the console. Then after the function is finished, you create a single object and add that to your array.
Try something like
function Set-Drivers{
$result = foreach ($Driver in $Drivers) {
[PsCustomObject]#{
'DriverName' = $Driver.Name
'DriverID' = $Driver.PackageID
}
}
# output the result
# the comma wraps the result in a single element array, even if it has only one element.
# PowerShell 'flattens' that upon return from the function, leaving the actual resulting array.
,$result
}
$array = Set-Drivers
# show what you've got
$array

Convert a SharePoint Online list into JSON using arrays

I'm trying to convert a set of SharePoint list items (and associated data) into a JSON object. To do this I'm trying to create a multi-dimensional array and then iterate over my SharePoint objects to populate it.
This is the relevant code so far:
#Lookup Source Address
$rootWeb = $Context.Web
$List = $rootWeb.lists.getByTitle($ListName)
$fields = $List.Fields;
$ListItems = $List.GetItems([Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery())
#Load the List
$Context.Load($rootWeb)
$Context.Load($List)
$Context.Load($ListItems)
$context.Load($fields)
$Context.ExecuteQuery()
$listArray = #()
$listArray["DisplayTitle"] = #()
$listArray["Description"] = #()
$listArray["Setting"] = #()
$listArray["HealthAreas"] = #()
$listArray["ResourceType"] = #()
$listArray["ExternalURL"] = #()
$listArray["Active"] = #()
Write-Host "List items are"
foreach ($item in $ListItems)
{
$listArray["DisplayTitle"].Add($item["Title"])
$listArray["Description"].Add($item["File Description"])
$listArray["Setting"].Add($item["Setting"])
$listArray["HealthAreas"].Add($item["Health_x0020_Area"])
$listArray["ResourceType"].Add($item["Resource_x0020_Type"])
$listArray["ExternalURL"].Add($item["External_x0020_file_x0020_path"])
$listArray["Active"].Add($item["Currently_x0020_active_x003f_"])
}
Write-Host "############################"
Write-Host $listArray | ConvertTo-Json
I know there's a gap in my thinking here (maybe I need a hashtable) but just can't see it. The error I'm receiving is:
You cannot call a method on a null-valued expression.
However I can't see where my null variable may be originating from as I've confirmed each item in the loop does contain data (by writing to console).
The error that you receive is not related to SharePoint but to PowerShell. You created the PowerShell array and tried to access its elements like it was associative array/hashtable.
Please try this code (I've tested it with my own list with different column names and it works fine):
#Lookup Source Address
$rootWeb = $Context.Web
$List = $rootWeb.lists.getByTitle($ListName)
$fields = $List.Fields;
$ListItems = $List.GetItems([Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery())
#Load the List
$Context.Load($rootWeb)
$Context.Load($List)
$Context.Load($ListItems)
$context.Load($fields)
$Context.ExecuteQuery()
$listArray = New-Object System.Collections.Generic.List[System.Object]
Write-Host "List items are"
foreach ($item in $ListItems)
{
$listArray.Add([hashtable]#{
DisplayTitle=$item["Title"];
Description= $item["File Description"];
Setting= $item["Setting"];
HealthAreas= $item["Health_x0020_Area"];
ResourceType= $item["Resource_x0020_Type"];
ExternalURL= $item["External_x0020_file_x0020_path"];
Active= $item["Currently_x0020_active_x003f_"];
}
)
}
Write-Host "############################"
$json = $listArray | ConvertTo-Json
Write-Host $json

PowerShell: modify elements of array

My cmdlet get-objects returns an array of MyObject with public properties:
public class MyObject{
public string testString = "test";
}
I want users without programming skills to be able to modify public properties (like testString in this example) from all objects of the array.
Then feed the modified array to my second cmdlet which saves the object to the database.
That means the syntax of the "editing code" must be as simple as possible.
It should look somewhat like this:
> get-objects | foreach{$_.testString = "newValue"} | set-objects
I know that this is not possible, because $_ just returns a copy of the element from the array.
So you'd need to acces the elements by index in a loop and then modify the property.This gets really quickly really complicated for people that are not familiar with programming.
Is there any "user-friendly" built-in way of doing this? It shouldn't be more "complex" than a simple foreach {property = value}
I know that this is not possible, because $_ just returns a copy of the element from the array (https://social.technet.microsoft.com/forums/scriptcenter/en-US/a0a92149-d257-4751-8c2c-4c1622e78aa2/powershell-modifying-array-elements)
I think you're mis-intepreting the answer in that thread.
$_ is indeed a local copy of the value returned by whatever enumerator you're currently iterating over - but you can still return your modified copy of that value (as pointed out in the comments):
Get-Objects | ForEach-Object {
# modify the current item
$_.propertyname = "value"
# drop the modified object back into the pipeline
$_
} | Set-Objects
In (allegedly impossible) situations where you need to modify a stored array of objects, you can use the same technique to overwrite the array with the new values:
PS C:\> $myArray = 1,2,3,4,5
PS C:\> $myArray = $myArray |ForEach-Object {
>>> $_ *= 10
>>> $_
>>>}
>>>
PS C:\> $myArray
10
20
30
40
50
That means the syntax of the "editing code" must be as simple as possible.
Thankfully, PowerShell is very powerful in terms of introspection. You could implement a wrapper function that adds the $_; statement to the end of the loop body, in case the user forgets:
function Add-PsItem
{
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline,ValueFromRemainingArguments)]
[psobject[]]$InputObject,
[Parameter(Mandatory)]
[scriptblock]$Process
)
begin {
$InputArray = #()
# fetch the last statement in the scriptblock
$EndBlock = $Process.Ast.EndBlock
$LastStatement = $EndBlock.Statements[-1].Extent.Text.Trim()
# check if the last statement is `$_`
if($LastStatement -ne '$_'){
# if not, add it
$Process = [scriptblock]::Create('{0};$_' -f $Process.ToString())
}
}
process {
# collect all the input
$InputArray += $InputObject
}
end {
# pipe input to foreach-object with the new scriptblock
$InputArray | ForEach-Object -Process $Process
}
}
Now the users can do:
Get-Objects | Add-PsItem {$_.testString = "newValue"} | Set-Objects
The ValueFromRemainingArguments attribute also lets users supply input as unbounded parameter values:
PS C:\> Add-PsItem { $_ *= 10 } 1 2 3
10
20
30
This might be helpful if the user is not used to working with the pipeline
Here's a more general approach, arguably easier to understand, and less fragile:
# $dataSource would be get-object in the OP
# $dataUpdater is the script the user supplies to modify properties
# $dataSink would be set-object in the OP
function Update-Data {
param(
[scriptblock] $dataSource,
[scriptblock] $dataUpdater,
[scriptblock] $dataSink
)
& $dataSource |
% {
$updaterOutput = & $dataUpdater
# This "if" allows $dataUpdater to create an entirely new object, or
# modify the properties of an existing object
if ($updaterOutput -eq $null) {
$_
} else {
$updaterOutput
}
} |
% $dataSink
}
Here are a couple of examples of use. The first example isn't applicable to the OP, but it's being used to create a data set that is applicable (a set of objects with properties).
# Use updata-data to create a set of data with properties
#
$theDataSource = #() # will be filled in by first update-data
update-data {
# data source
0..4
} {
# data updater: creates a new object with properties
New-Object psobject |
# add-member uses hash table created on the fly to add properties
# to a psobject
add-member -passthru -NotePropertyMembers #{
room = #('living','dining','kitchen','bed')[$_];
size = #(320, 200, 250, 424 )[$_]}
} {
# data sink
$global:theDataSource += $_
}
$theDataSource | ft -AutoSize
# Now use updata-data to modify properties in data set
# this $dataUpdater updates the 'size' property
#
$theDataSink = #()
update-data { $theDataSource } { $_.size *= 2} { $global:theDataSink += $_}
$theDataSink | ft -AutoSize
And then the output:
room size
---- ----
living 320
dining 200
kitchen 250
bed 424
room size
---- ----
living 640
dining 400
kitchen 500
bed 848
As described above update-data relies on a "streaming" data source and sink. There is no notion of whether the first or fifteenth element is being modified. Or if the data source uses a key (rather than an index) to access each element, the data sink wouldn't have access to the key. To handle this case a "context" (for example an index or a key) could be passed through the pipeline along with the data item. The $dataUpdater wouldn't (necessarily) need to see the context. Here's a revised version with this concept added:
# $dataSource and $dataSink scripts need to be changed to output/input an
# object that contains both the object to modify, as well as the context.
# To keep it simple, $dataSource will output an array with two elements:
# the value and the context. And $dataSink will accept an array (via $_)
# containing the value and the context.
function Update-Data {
param(
[scriptblock] $dataSource,
[scriptblock] $dataUpdater,
[scriptblock] $dataSink
)
% $dataSource |
% {
$saved_ = $_
# Set $_ to the data object
$_ = $_[0]
$updaterOutput = & $dataUpdater
if ($updaterOutput -eq $null) { $updaterOutput = $_}
$_ = $updaterOutput, $saved_[1]
} |
% $dataSink
}

How to create and populate an array in Powershell based on a dynamic variable?

I've been struggling with this for a couple of days, and I'm not sure how to conquer it. I need to do the following:
Import a csv of users with the following values:
ID, Name, Region
Create an array based on the Region values that I can then use to populate with ID's and Names with that region, ie.
Array_SEA
AA_SCOM, Adam Andrews, SEA
Array_OAK
BB_SCOM, Bob Barker, OAK
Here's the code I've got right now:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist =#()
foreach ($vitem in $list2)
{
$arraylist += New-Object PsObject -Property #{'Array' = "Array_" + $vitem.bu}
}
foreach ($varray in $arraylist)
{
$arr = new-variable -Name $varray
$arr.value += $varray.array
$arr
}
This produces the following error for records with a duplicate regions:
New-Variable: A variable with name '#{Array=Array_SCA}' already exists.
I'm also getting the following when it tries to add values:
Property 'value' cannot be found on this object; make sure it exists and is settable.
I get that I'm not actually creating arrays in the second section, but I'm not sure how to pass the output of the variable to an array name without turning the variable declaration into the array name, if that makes sense.
I've tried the following with hash tables, and it gets closer:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist =#{}
foreach ($vitem in $list2){$arraylist[$vitem.bu] = #()}
foreach ($record in $list2)
{
$arraylist[$vitem.bu] += ($record.SCOMID,$record.Name,$record.BU)
Write-host "Array: "
$arraylist[$vitem.bu]
write-host ""
}
The output on this shows no errors, but it just keeps showing the added fields for all of the records for each iteration of the list, so I don't think that it's actually assigning each unique BU to the array name.
I like the hashtable-approach, but I would finetune it a little. Try:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist = #{}
foreach ($vitem in $list2){
if($arraylist.ContainsKey($vitem.BU)) {
#Array exists, add item
$arraylist[($vitem.BU)] += $vitem
} else {
#Array not found, creating it
$arraylist[($vitem.BU)] = #($vitem)
}
}
#TEST: List arrays and number of entries
$arraylist.GetEnumerator() | % {
"Array '$($_.Key)' has $($_.Value.Count) items"
}
You could also use Group-Object like:
$list2 = ipcsv .\TSE_Contact_List.csv | Group-Object BU
#TEST: List groups(regions) and number of entries
$list2 | % {
"Region '$($_.Name)' has $(#($_.Group).Count) items"
}

Powershell: Why is a certain string array is being formatted as "System.Int32"?

I'm trying to add string members to an array called $htmlbodytest that I can then email at the end of the script (after all the loops close).
Why am I getting the following error message when I try to populate an array with string members (and only string members)?:
Cannot convert value "test server" to type "System.Int32". Error: "Input string was not in a correct format."
I have several other jagged arrays in the script (within the loops) that store strings with no issue.
I have not declared anything anywhere with regard to data type.
If I add a test array member after the loops in the email section with just "test" it still fails.
If I copy this into it's own script, I can populate the array with strings just fine.
More detail:
# before loops begin, array is declared as:
$htmlbodytest = #()
[...]
foreach ($server_name in $server_names) {
Start-Job -Scriptblock {
#If server fails a ping test, skip it and add to array to email
if(!(Test-Connection -ComputerName $using:server_name -Quiet)) {
$htmlbodytest += "$using:server_name"
continue
} else {
[...]
} # after work of loop is finished, send email
$smtpServer = "10.10.10.10"
$smtpFrom = "from#co.com"
$smtpTo = "to#co.com"
$messageSubject = "Servers that didn't record uptime this month due to no ping"
$message = New-Object System.Net.Mail.MailMessage $smtpfrom, $smtpto
$message.Subject = $messageSubject
$message.IsBodyHTML = $true
$message.Body = $htmlbodytest | ForEach-Object {Add-Member -InputObject $_ -Type NoteProperty -Name Servers -Value $_; $_} | ConvertTo-Html -Head $a -Property Server
$smtp = New-Object Net.Mail.SmtpClient($smtpServer)
$smtp.Send($message)
Closing ISE and reopening this resolved this. I was using this same variable name in a different tab, but was always populating with string types. I'm certainly not marking this as answered out of glory, but to prevent wasting others' time on this flaky issue.

Resources