Array of variables in PowerShell has null members - arrays

I have a PowerShell script, where I want to make sure certain variables have value before proceeding.
So I have the following:
$dataRow = $sheet.Cells.Find($country).Row
$serverCol = $sheet.Cells.Find($serverString).Column
$databaseCol = $sheet.Cells.Find($databaseString).Column
$userCol = $sheet.Cells.Find($userString).Column
$passwordCol = $sheet.Cells.Find($passString).Column
$partnerCol = $sheet.Cells.Find($partnerString).Column
#All variables in this array are required. If one is empty - the script cannot continue
$requiredVars = #($dataRow, $serverCol, $databaseCol, $userCol, $passwordCol, $partnerCol)
But when I foreach over the array like so:
foreach ($var in $requiredVars)
{
Write-Host DataRow = ($dataRow -eq $var)
Write-Host ServerCol = ($serverCol -eq $var)
Write-Host DatabaseCol = ($databaseCol -eq $var)
Write-Host UserCol = ($userCol -eq $var)
Write-Host PasswordCol = ($passwordCol -eq $var)
Write-Host PartnerCol = ($partnerCol -eq $var)
if ($var -eq $null)
{
[System.Windows.Forms.MessageBox]::Show("No data found for given string!")
$excel.Quit()
return
}
}
I always get the MessageBox. I added the "Write-Host" part to see the value of each variable, then changed it to see which variable was null but all variables have values in them and all the checks you see here return "False".
I'd like to know what I'm doing wrong and if the $requiredVars array only copies values, not references or something.

Instead of using separate variables, you may consider using a Hashtable to store them all.
This makes checking the individual items a lot simpler:
# get the data from Excel and store everything in a Hashtable
# to use any of the items, use syntax like $excelData.passwordCol or $excelData['passwordCol']
$excelData = #{
'dataRow' = $sheet.Cells.Find($country).Row
'serverCol' = $sheet.Cells.Find($serverString).Column
'databaseCol' = $sheet.Cells.Find($databaseString).Column
'userCol' = $sheet.Cells.Find($userString).Column
'passwordCol' = $sheet.Cells.Find($passString).Column
'partnerCol' = $sheet.Cells.Find($partnerString).Column
}
# check all items in the hash. If any item is $null then exit
foreach ($item in $excelData.Keys) {
# or use: if ($null -eq $excelData[$item])
if (-not $excelData[$item]) {
[System.Windows.Forms.MessageBox]::Show("No data found for item $item!")
$excel.Quit()
# IMPORTANT: clean-up used COM objects from memory when done with them
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
# Your code doesn't show this, but you'll have a $workbook object in there too
# [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return
}
}

One way to directly solve your question is this:
$a = "foo"
$b = "bar"
$c = $null
$requiredVariables = $a, $b, $c
# How many total entries in array?
($requiredVariables).Count
# How many of them have a value?
($requiredVariables | Where-Object {$_}).Count
# So one option for a single check would be:
if (($requiredVariables.Count) -ne ($requiredVariables | Where-Object {$_}).Count) {
Write-Warning "Not all values provided"
}
However an alternative [and better] approach is to make your code in to a function that includes parameter validation
function YourCustomFunction {
Param (
[ValidateNotNullOrEmpty()]
$a
,
[ValidateNotNullOrEmpty()]
$b
,
[ValidateNotNullOrEmpty()]
$c
)
Process {
Write-Output "Your function code goes here..."
}
}
# Call your function with the params
YourCustomFunction -a $a -b $b -c $c
Example output:
Test-YourCustomFunction: Cannot validate argument on parameter 'c'. The argument is null or empty. Provide an argument that is not null or empty, and
then try the command again.
At line:39 char:48

Related

Powershell filling array with function calling itself to loop through

tldr; I need to fill an array, which is populated in a function, within constricted language. Until now i found only ways, which are not do able in constricted language.
So basicly i want to loop through the AD and identify looping groups and where users are placed looping wise.
To Achive this i wrote a function which calls itslef. The function returns 4 diffrent objects. These objects are needed to handle the loop.
But the function scope needs to return the value to the script scope ("top most") as otherwise the script will loop infinitly on the first object already.
Unfortunatly this is in constrained language, which means the most common resolves wont work.
Shortend Code Sample
$ReturnValue1 = #()
$ReturnValue2 = #()
$ReturnValue3 = #()
$ReturnValue4 = #()
Function Get-ADInfos
{
Param(
$Entitys
)
foreach($Entity in $Entitys){
$Object = New-Object -TypeName PSObject
if($Entity.objectClass -eq "user"){
if($ReturnValue2.User.distinguishedName -contains $Entity)
#Do Something
$ReturnValue1 += $Object
Write-Host "$Entity is already scanned"
}else{
#Do something
$ReturnValue2 += $didsomething
Get-ADInfos $Values #looping
}
}elseif($Entity.objectClass -eq "group"){
if($ReturnValue4.Group.distinguishedName -contains $Entity){
#Do Something
$ReturnValue3 += $didsomething
}else{
#Do Something
$ReturnValue4 += $didsomething
Get-ADInfos $Values
}
}else{
write-host "finished"
}
}
Full Code for Repro (Older) #Note: To use constrained language for testing.
$User = #()
$Gruppen = #()
$LoopUser = #()
$LoopGroup = #()
Function Get-ADInfos
{
Param(
$Entry
)
#$Entry = Get-ADGroup "Domain Users"
if($Entry.objectClass -eq "user"){
$Entitys = Get-ADPrincipalGroupMembership $Entry
}elseif($Entry.objectClass -eq "group"){
$Entitys = Get-ADGroupMember $Entry
}else{}
foreach($Entity in $Entitys){
if($Entity.objectClass -eq "user"){
if($User.user -contains $Entity){
$Row = "" | Select User, Group
$Row.User = $Entity
$Row.Group = $Entry
$LoopUser += $Row #return to "master" scope
Write-Host "$Entity is already scanned"
}else{
$Row = "" | Select User, Group
$Row.User = $Entity
$Row.Group = $Entry
$User += $Row #return to "master" scope
Write-Host "$Entity is in $group"
Get-ADInfos $Entity
}
}elseif($Entity.objectClass -eq "group"){
if($Groups.group -contains $Entity){
$Row = "" | Select ScannedGroup, ParentGroup
$Row.ScannedGroup = $Entity
$Row.ParentGroup = $Entry
$LoopGroup += $Row #return to "master" scope
Write-Host "$Entity is already scanned"
}else{
$Row = "" | Select Group
$Row.Group = $Entity
$Groups += $Row #return to "master" scope
Write-Host "$Entity scanned"
Get-ADInfos $Entity
}
}else{
write-host "finished"
}
}
}
Get-ADGroup "Domain Users" | Get-ADInfos
PowerShell Arrays are immutable (fixed size collections):
$User = #()
$User.add('item')
MethodInvocationException: Exception calling "Add" with "1" argument(s): "Collection was of a fixed size."
This means that -besides the inefficient use of the increase assignment operator (+=)- it will create a new copy of the of array in each child scope when you try to change it:
$User = #()
function Test {
$User += 'Item'
Write-Host 'Child scope:' $User
}
Test
Write-Host 'Parent scope:' $User
Yields:
Child scope: Item
Parent scope:
Instead, I recommend you to use List<T> Class knowing that you can use the List<T>.Add(T) Method and the fact that objects are referenced by default:
$User = [Collections.Generic.List[object]]::new()
function Test { $User.Add('item') }
Test
$User # Yields: item
Addendum
As your script appears to run under constrained language mode, you will not be allowed to use the List<T> type or anything similar you might consider to use native PowerShell HashTables instead. See also: Mutable lists in Constrained Language Mode.
To apply this to your script:
Change all the arrays (that have a shared scope) to hashtables.
e.g.: $User = #() → $User = #{}
Change your assignments.
e.g.: $User += $Row → $User[$User.Count] = $Row
Change the conditions.
e.g.: $User.user -contains $Entity → $User.Values.user -contains $Entity

PowerShell: Passing an ArrayList of Objects into another Script as an Argument

I am trying to pass an ArrayList that contains objects into another PowerShell script to execute something further.
The error message that I am receiving is:
"Cannot process argument transformation on parameter 'al'. Cannot convert the "System.Collections.Hashable System.Collections.Hashable System.Collections.Hashable" value of type "System.String" to type "System.Collections.ArrayList""
In script1.ps1:
$al = New-Object System.Collections.ArrayList
...
$obj = #{"var1"="apple"; "var2"="banana"; "var3"="carrot";}
$al.Add($obj)
...
foreach ($i in $al) {
$temp = $($i.var1)
write-host "$temp" #outputs "apple" correctly
}
invoke-expression -Command "script2.ps1 -al '$al'"
In script2.ps1:
param ([System.Collections.ArrayList]$al)
...
foreach ($i in $al) {
$temp = $($i.var1)
write-host "$temp" #error message
}
For a reason that I'm not familiar with, Invoke-Expression is converting your ArrayList to a HashTable. If you really need an ArrayList in script2.ps1, you can make $al a global variable (see below).
Updated script1.ps1
$al = New-Object System.Collections.ArrayList
$obj = #{"var1" = "apple"; "var2" = "banana"; "var3" = "carrot"; }
$al.Add($obj)
foreach ($i in $al) {
$temp = $($i.var1)
write-host "$temp"
}
$Global:al = $al
invoke-expression -Command "$PSScriptRoot\script2.ps1"
Updated script2.ps1
param()
$Global:al.GetType().FullName
foreach ($i in $Global:al) {
$temp = $($i.var1)
write-host "$temp"
}

Expand property in hashtable

I'm having a simple issue with a script, where I want to run a GCI against a remote server, issue is, the value is combined with another hashtable property, so the GCI fails.
The script reads entries from a two-column .csv, the headers are "server" and "platform"
Here's what I've got:
$ShortDate = (Get-Date).ToString('MM/dd/yyyy')
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI \\$_.Server\c$\log\Completed_Summary_*.html -EA 0 | where {$.LastWriteTime -ge "$ShortDate"}) {
Write-Host "FOUND"
} # end of IF GCI
} # end of For-Each
$serverObjects += New-Object -TypeName PSObject -Property $CheckServer
The problem is that the entry for $_.Server should be SERVER1, SERVER2, SERVER3, etc, all the entries in the servers.csv, instead, the values for both $_.Server and $_.Platform are combined. Such as:
Write-Host "Checking" \\#{Server=SERVER1; Platform=PLATFORM_1}.Server\c$\log\Completed_Summary_*.html
it should show as follows:
Write-Host "Checking" \\SERVER1\log\Completed_Summary_*.html
How do I un-combine them so that the GCI command works?
PowerShell only does simple variable expansion inside strings. For more complex expressions like index operations or accessing object properties/methods it would insert the stringified value of the array or object variable and leave the rest of the operation untouched.
Demonstration:
PS C:\> $array = 23, 42
PS C:\> Write-Host "some $array[1] or other"
some 23 42[1] or other
PS C:\> $object = New-Object -Type PSObject -Property #{Foo=23; Bar=42}
PS C:\> Write-Host "some $object.Foo or other"
some #{Bar=42; Foo=23}.Foo or other
To avoid this you need to either:
assign the resulting value to a variable first and use that variable in the string:
$value = $array[5]
Write-Host "some $value or other"
$value = $object.Foo
Write-Host "some $value or other"
use a subexpression ($(...)):
Write-Host "some $($array[5]) or other"
Write-Host "some $($object.Foo) or other"
use the format operator (-f):
Write-Host "some {0} or other" -f $array[5]
Write-Host "some {0} or other" -f $object.Foo
modify like it
$ShortDate = Get-Date -Hour 0 -Minute 0 -Second 0
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
$Dir="C:\temp"
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI "\\$($_.Server)\c$\log\Completed_Summary_*.html" -EA 0 | where {$_.LastWriteTime -ge $ShortDate})
{
Write-Host "FOUND"
}
}

Using intermediate variable to work with array (reference type)

I am trying to use $a variable in this script for working with intermediate steps so that I don't have to use $array[$array.Count-1] repeatedly. Similarly for $prop as well . However, values are being overwritten by last value in loop.
$guests = Import-Csv -Path C:\Users\shant_000\Desktop\UploadGuest_test.csv
$output = gc '.\Sample Json.json' | ConvertFrom-Json
$array = New-Object System.Collections.ArrayList;
foreach ($g in $guests) {
$array.Add($output);
$a = $array[$array.Count-1];
$a.Username = $g.'EmailAddress';
$a.DisplayName = $g.'FirstName' + ' ' + $g.'LastName';
$a.Password = $g.'LastName' + '123';
$a.Email = $g.'EmailAddress';
foreach ($i in $a.ProfileProperties.Count) {
$j = $i - 1;
$prop = $a.ProfileProperties[$j];
if ($prop.PropertyName -eq "FirstName") {
$prop.PropertyValue = $g.'FirstName';
} elseif ($prop.PropertyName -eq "LastName") {
$prop.PropertyValue = $g.'LastName';
}
$a.ProfileProperties[$j] = $prop;
}
$array[$array.Count-1] = $a;
}
$array;
All array elements are referencing one actual variable: $output.
Create an entirely new object each time by repeating JSON-parsing:
$jsontext = gc '.\Sample Json.json'
..........
foreach ($g in $guests) {
$a = $jsontext | ConvertFrom-Json
# process $a
# ............
$array.Add($a) >$null
}
In case the JSON file is very big and you change only a few parts of it you can use a faster cloning technique on the changed parts (and their entire parent chain) via .PSObject.Copy():
foreach ($g in $guests) {
$a = $output.PSObject.Copy()
# ............
$a.ProfileProperties = $a.ProfileProperties.PSObject.Copy()
# ............
foreach ($i in $a.ProfileProperties.Count) {
# ............
$prop = $a.ProfileProperties[$j].PSObject.Copy();
# ............
}
$array.Add($a) >$null
}
As others have pointed out, appending $object appends a references to the same single object, so you keep changing the values for all elements in the list. Unfortunately the approach #wOxxOm suggested (which I thought would work at first too) doesn't work if your JSON datastructure has nested objects, because Copy() only clones the topmost object while the nested objects remain references to their original.
Demonstration:
PS C:\> $o = '{"foo":{"bar":42},"baz":23}' | ConvertFrom-Json
PS C:\> $o | Format-Custom *
class PSCustomObject
{
foo =
class PSCustomObject
{
bar = 42
}
baz = 23
}
PS C:\> $o1 = $o
PS C:\> $o2 = $o.PSObject.Copy()
If you change the nested property bar on both $o1 and $o2 it has on both objects the value that was last set to any of them:
PS C:\> $o1.foo.bar = 23
PS C:\> $o2.foo.bar = 24
PS C:\> $o1.foo.bar
24
PS C:\> $o2.foo.bar
24
Only if you change a property of the topmost object you'll get a difference between $o1 and $o2:
PS C:\> $o1.baz = 5
PS C:\> $o.baz
5
PS C:\> $o1.baz
5
PS C:\> $o2.baz
23
While you could do a deep copy it's not as simple and straightforward as one would like to think. Usually it takes less effort (and simpler code) to just create the object multiple times as #PetSerAl suggested in the comments to your question.
I'd also recommend to avoid appending to an array (or arraylist) in a loop. You can simply echo your objects inside the loop and collect the entire output as a list/array by assigning the loop to a variable:
$json = Get-Content '.\Sample Json.json' -Raw
$array = foreach ($g in $guests) {
$a = $json | ConvertFrom-Json # create new object
$a.Username = $g.'EmailAddress'
...
$a # echo object, so it can be collected in $array
}
Use Get-Content -Raw on PowerShell v3 and newer (or Get-Content | Out-String on earlier versions) to avoid issues with multiline JSON data in the JSON file.

How to remove item from an array in PowerShell?

I'm using Powershell 1.0 to remove an item from an Array. Here's my script:
param (
[string]$backupDir = $(throw "Please supply the directory to housekeep"),
[int]$maxAge = 30,
[switch]$NoRecurse,
[switch]$KeepDirectories
)
$days = $maxAge * -1
# do not delete directories with these values in the path
$exclusionList = Get-Content HousekeepBackupsExclusions.txt
if ($NoRecurse)
{
$filesToDelete = Get-ChildItem $backupDir | where-object {$_.PsIsContainer -ne $true -and $_.LastWriteTime -lt $(Get-Date).AddDays($days)}
}
else
{
$filesToDelete = Get-ChildItem $backupDir -Recurse | where-object {$_.PsIsContainer -ne $true -and $_.LastWriteTime -lt $(Get-Date).AddDays($days)}
}
foreach ($file in $filesToDelete)
{
# remove the file from the deleted list if it's an exclusion
foreach ($exclusion in $exclusionList)
{
"Testing to see if $exclusion is in " + $file.FullName
if ($file.FullName.Contains($exclusion)) {$filesToDelete.Remove($file); "FOUND ONE!"}
}
}
I realize that Get-ChildItem in powershell returns a System.Array type. I therefore get this error when trying to use the Remove method:
Method invocation failed because [System.Object[]] doesn't contain a method named 'Remove'.
What I'd like to do is convert $filesToDelete to an ArrayList and then remove items using ArrayList.Remove. Is this a good idea or should I directly manipulate $filesToDelete as a System.Array in some way?
Thanks
The best way to do this is to use Where-Object to perform the filtering and use the returned array.
You can also use #splat to pass multiple parameters to a command (new in V2). If you cannot upgrade (and you should if at all possible, then just collect the output from Get-ChildItems (only repeating that one CmdLet) and do all the filtering in common code).
The working part of your script becomes:
$moreArgs = #{}
if (-not $NoRecurse) {
$moreArgs["Recurse"] = $true
}
$filesToDelete = Get-ChildItem $BackupDir #moreArgs |
where-object {-not $_.PsIsContainer -and
$_.LastWriteTime -lt $(Get-Date).AddDays($days) -and
-not $_.FullName.Contains($exclusion)}
In PSH arrays are immutable, you cannot modify them, but it very easy to create a new one (operators like += on arrays actually create a new array and return that).
I agree with Richard, that Where-Object should be used here. However, it's harder to read.
What I would propose:
# get $filesToDelete and #exclusionList. In V2 use splatting as proposed by Richard.
$res = $filesToDelete | % {
$file = $_
$isExcluded = ($exclusionList | % { $file.FullName.Contains($_) } )
if (!$isExcluded) {
$file
}
}
#the files are in $res
Also note that generally it is not possible to iterate over a collection and change it. You would get an exception.
$a = New-Object System.Collections.ArrayList
$a.AddRange((1,2,3))
foreach($item in $a) { $a.Add($item*$item) }
An error occurred while enumerating through a collection:
At line:1 char:8
+ foreach <<<< ($item in $a) { $a.Add($item*$item) }
+ CategoryInfo : InvalidOperation: (System.Collecti...numeratorSimple:ArrayListEnumeratorSimple) [], RuntimeException
+ FullyQualifiedErrorId : BadEnumeration
This is ancient. But, I wrote these a while ago to add and remove from powershell lists using recursion. It leverages the ability of powershell to do multiple assignment . That is, you can do $a,$b,$c=#('a','b','c') to assign a b and c to their variables. Doing $a,$b=#('a','b','c') assigns 'a' to $a and #('b','c') to $b.
First is by item value. It'll remove the first occurrence.
function Remove-ItemFromList ($Item,[array]$List(throw"the item $item was not in the list"),[array]$chckd_list=#())
{
if ($list.length -lt 1 ) { throw "the item $item was not in the list" }
$check_item,$temp_list=$list
if ($check_item -eq $item )
{
$chckd_list+=$temp_list
return $chckd_list
}
else
{
$chckd_list+=$check_item
return (Remove-ItemFromList -item $item -chckd_list $chckd_list -list $temp_list )
}
}
This one removes by index. You can probably mess it up good by passing a value to count in the initial call.
function Remove-IndexFromList ([int]$Index,[array]$List,[array]$chckd_list=#(),[int]$count=0)
{
if (($list.length+$count-1) -lt $index )
{ throw "the index is out of range" }
$check_item,$temp_list=$list
if ($count -eq $index)
{
$chckd_list+=$temp_list
return $chckd_list
}
else
{
$chckd_list+=$check_item
return (Remove-IndexFromList -count ($count + 1) -index $index -chckd_list $chckd_list -list $temp_list )
}
}
This is a very old question, but the problem is still valid, but none of the answers fit my scenario, so I will suggest another solution.
I my case, I read in an xml configuration file and I want to remove an element from an array.
[xml]$content = get-content $file
$element = $content.PathToArray | Where-Object {$_.name -eq "ElementToRemove" }
$element.ParentNode.RemoveChild($element)
This is very simple and gets the job done.

Resources