PowerShell array getting new entries - arrays

In this snippet I have a function (FDiskScan) that gets a computer name as an input and should return an array of objects.
function FDiskScan ([String] $name)
{
$outarray = [System.Collections.ArrayList]#()
$diskscan = Get-WmiObject Win32_logicaldisk -ComputerName $name
foreach ($diskobj in $diskscan)
{
if($diskobj.VolumeName -ne $null )
{
$max = $diskobj.Size/1024/1024/1024
$free = $diskobj.FreeSpace/1024/1024/1024
$full = $max - $free
$obj = #{
'ID' = $diskobj.deviceid
'Name' = $diskobj.VolumeName
'TotalSpace' = $max
'FreeSpace' = $free
'OccupiedSpace' = $full }
$TMP = New-Object psobject -Property $obj
$outarray.Add($TMP)
}
}
return $outarray
}
$pc = "INSERT PC NAME HERE"
$diskdata = [System.Collections.ArrayList]#()
$diskdata = FDiskScan($pc)
foreach ($disk in $diskdata)
{
Write-Host "Disco: " $disk.ID
Write-Host "Espaço Total: " ([math]::Round($disk.TotalSpace, 2)) "GB"
Write-Host "Espaço Ocupado: " ([math]::Round($disk.OccupiedSpace, 2)) "GB"
Write-Host "Espaço Livre" ([math]::Round($disk.FreeSpace, 2)) "GB" "`n"
}
Within the function with debugging and going into the variables I can see that everything is alright, and when the array gets out of the function and into the script scope it adds 2 more entries.
While in debug mode it tells me that $outarry within FDiskScan has the two disks that I have on my system organised as they should be.
However on:
$diskdata = FDiskScan($pc)
It says that it has an entry of value 0 on index 0 and of value 1 on index 1, then the disks follow suit, first disk C: in index 3 and disk D in index 4.
The expected behaviour was for index 0 and 1 having disks C and D respectively not a phantom 0 and 1 entries.

When adding an object to an array list in PowerShell (i.e. $outarray.Add($TMP)) the index, the object was added at, gets returned. As you don't assign the return value to a variable the function returns a System.Array containing the indexes and the entries of the array list returned by return $outarray. That's the reason why your functions return value contains 4 elements. Furthermore your functions return value in this case is not of type System.Collections.ArrayList but of type System.Array.
To avoid that behaviour do the following.
$null = $outarray.Add($TMP);

You are seeing 0, 1 because of this line - $outarray.Add($TMP). Change it to $outarray.Add($TMP) | Out-Null. I think PowerShell is printing the index when adding to the array.

Related

Appending objects to arrays in Powershell

I have the following code:
$DataType = "X,Y,Z"
$Data = "1,2,3"
$Table = #()
for ($i = 0; $i -le ($DataType.Count-1); $i++)
{
$Properties = #{$DataType[$i]=$Data[$i]}
$Object = New-Object -TypeName PSCustomObject -Property $Properties
$Table += $Object
}
$Table | Format-Table -AutoSize
I get this output:
X
-
1
What I would like to get is:
X Y Z
- - -
1 2 3
Thanks for your help!
Cutting a long story short:
$DataType, $Data | ConvertFrom-Csv
X Y Z
- - -
1 2 3
Ok, it needs a little explanation:
PowerShell will automatically unroll the array of strings ($DataType, $Data) and supply it as individual line items to the pipeline. The ConvertFrom-Csv cmdlet supports supplying the input table through the pipeline as separate lines (strings).
You can do the following instead:
$DataType = "X","Y","Z"
$Data = 1,2,3
$hash = [ordered]#{}
for ($i = 0; $i -lt $DataType.Count; $i++) {
$hash.Add($DataType[$i],$Data[$i])
}
$table = [pscustomobject]$hash
Explanation:
The code creates two collections, $DataType and $Data, of three items. $hash is an ordered hash table. [ordered] is used to preserve the order at which key-value pairs are added to the hash table. Since $hash is the object type hashtable, it contains the .Add(key,value) method for adding key-value pairs.
Since the [pscustomobject] type accelerator can be cast on a hash table, we can simply use the syntax [pscustomobject]$hash to create a new object.
If we consider your attempt, your variables are actually single strings rather than collections. Surrounding a value with quotes causes PowerShell to expand the inner contents as a string. When you index a string rather than a collection, you index the characters in the string rather than the entire item. You need to quote the individual elements between the commas so that the , acts as a separator rather than part of the string. You can see this behavior in the code below:
# DataType as a string
$DataType = "X,Y,Z"
$DataType[1]
,
# DataType as an array or collection
$DataType = "X","Y","Z"
$DataType[1]
Y
If you receive your data from another output in the current format, you can manipulate using $DataType = $DataType.Split(',') in order to create a collection. Alternatively you can treat the data as comma-separated and use the Import-Csv or ConvertFrom-Csv commands as in iRon's answer provided you order your strings properly.
Inside of your loop, you are adding three new objects to your collection $table rather than creating one object with three properties. $table += $Object creates an array called $table that appends a new item to the previous list from $table. If this was your original intention, you can view your collection by running $table | Format-List once you fix your $DataType and $Data variables.
When a collection is enumerated, the default table view displays the properties of the first object in a collection. Any succeeding objects will only display values for the first object's matching properties. So if object1 has properties X and Y and object2 has properties Y and Z, the console will only display values for properties X and Y for both objects. Format-List overrides this view and displays all properties of all objects. See below for an example of this behavior:
$obj1
X Y
- -
1 2
$obj2
Y Z
- -
3 4
$array = $obj1,$obj2
# Table View
$array
X Y
- -
1 2
3
# List View
$array | Format-List
X : 1
Y : 2
Y : 3
Z : 4
It seems that you want to create a single object with a property for each value in the arrays $DataType/$Data, but the problems are...
Neither $DataType nor $Data are arrays.
By creating your object inside the for loop you will create one object per iteration.
Since $DataType is a scalar variable $DataType.Count returns 1. Ordinarily, testing for $DataType.Count-1 would mean the loop never gets entered, but by the grace of using -le (so 0 -le 0 returns $true) instead of -lt, it does for exactly one iteration. Thus, you do get your single result object, but with only the first property created.
To fix this, let's create $DataType and $Data as arrays, as well as creating one set of properties before the loop to be used to create one result object after the loop...
...
$DataType = "X,Y,Z" -split ','
$Data = "1,2,3" -split ','
$Properties = #{}
for ($i = 0; $i -lt $DataType.Count; $i++)
{
$Properties[$DataType[$i]] = $Data[$i]
}
New-Object -TypeName PSCustomObject -Property $Properties | Format-Table -AutoSize
You'll also notice that $i -le ($DataType.Count-1) has been simplified to $i -lt $DataType.Count. On my system the above code outputs...
Y Z X
- - -
2 3 1
The properties are correct, but the order is not what you wanted. This is because Hashtable instances, such as $Properties, have no ordering among their keys. To ensure that the properties are in the order you specified in the question, on PowerShell 3.0 and above you can use this to preserve insertion order...
$Properties = [Ordered] #{}
What if you initialized $Table as an appendable like so:
$Table = New-Object System.Collections.ArrayList
for ($i = 0; $i -le ($DataType.Count-1); $i++)
{
$Properties = #{$DataType[$i]=$Data[$i]}
$Object = New-Object -TypeName PSCustomObject -Property $Properties
$Table.Add ( $Object )
}
Reformat your logic as needed.
One solution to this problem (if the inputs were two separate arrays):
$DataType = #( 'X','Y','Z' )
$Data = #( '1','2','3' )
$Table = New-Object psobject
for ($i = 0; $i -le ( $DataType.Count-1 ); $i++)
{
$Table | Add-Member -Name "$( $DataType[$i] )" -Value ( $Data[$i] ) -MemberType NoteProperty
}
$Table

Check for empty value in Array

How do I check for empty values in an hashtable, and list the item name as well ?
I could do if ($Vars.ContainsValue($null)) but this does not get me what item that has a $null value
$Vars = #{
1 = "CustomerID";
2 = "DepartmentID";
3 = "Environment";
4 = "JoinDomain";
5 = ""
}
if I do a foreach ($var in $vars) I get the whole hashtable?
First of all this is not an array, because those are written as #('element1', 'element2'). This concerns a HashTable which is indicated as #{} and is enumerated by the GetEnumerator() method.
After that method it's simply a matter of filtering out what you need with the key and/or the value property.
$Vars = #{
1 = "CustomerID";
2 = "DepartmentID";
3 = "Environment";
4 = "JoinDomain";
5 = ""
}
$VerbosePreference = 'Continue'
$Vars.GetEnumerator() | Where-Object {
-not $_.Value
} | ForEach-Object {
Write-Verbose "The key '$($_.Key)' has no value"
# Other code for handling the key with no value
}

Array of variables in PowerShell has null members

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

Using Get-Random after using Where-Object creates a null valued expression

I'm trying to create a 10 character password that includes a mix of numbers, letters (uppercase and lowercase), and symbols.
Below is the script I am using in the function:
Function Get-TempPassword {
$TempPassword = $null
$ascii = $NULL;For ($a = 33;$a –le 126;$a++) {$ascii +=, ([char][byte]$a | Where-Object {$_ -notin "'",'`','|','_',"`;",'"',','})}
Do {$TempPassword += $ascii | Get-Random; $loop++}
Until ($loop -eq 11)
return $TempPassword
}
If I remove the following section:
| Where-Object {$_ -notin "'",'`','|','_',";",'"',','}
The creation of the password works fine albeit including the symbols I don't want included.
Having the Where-Object function causes the Get-Random function to only use the first 5 characters in the array, and therefore I don't get letters of any case type, or numbers, or any of the other symbols.
I've found that if I use $ascii[26] (being the 25th character in the array) I get a null value, however I would think this would allow any character up to this character to be used, or none at all, not just the first 5. The 25th character just so happens to be a ; (ascii value number 59). I tried adding the symbol to the Where-Object exclusion, and it was removed from the array, but the 25th character still showed as a null value.
I performed a reverse lookup of the ascii value [int[]][char[]] of each character either side of where the ; symbol would appear and it returned values 58 and 60, leading me to believe it was value 59 that was offending, but the symbol at this point should have been excluded.
Adding characters to the 'where-object' exclusion list should be removing them from the array, and it appears to, however running $ascii.Count shows 49 characters, regardless of whether I add or remove characters to the Where-Object exclusion list.
I have looked for information on the web and can't seem to find any, although it may be the search terms I'm using, as it's a bit of a complex case that not many would be reporting on.
Any help is appreciated.
I didn't write this and i can't remember where i got it but i have built this into any scripts to create random secure Windows passwords, you can specify the length of the password returned by the param [int]$PasswordLength ( i have already set it to 10 ).
function New-SWRandomPassword {
[CmdletBinding(DefaultParameterSetName='FixedLength',ConfirmImpact='None')]
[OutputType([String])]
Param
(
# Specifies minimum password length
[Parameter(Mandatory=$false,
ParameterSetName='RandomLength')]
[ValidateScript({$_ -gt 0})]
[Alias('Min')]
[int]$MinPasswordLength = 8,
# Specifies maximum password length
[Parameter(Mandatory=$false,
ParameterSetName='RandomLength')]
[ValidateScript({
if($_ -ge $MinPasswordLength){$true}
else{Throw 'Max value cannot be lesser than min value.'}})]
[Alias('Max')]
[int]$MaxPasswordLength = 11,
# Specifies a fixed password length
[Parameter(Mandatory=$false,
ParameterSetName='FixedLength')]
[ValidateRange(1,2147483647)]
[int]$PasswordLength = 10,
# Specifies an array of strings containing charactergroups from which the password will be generated.
# At least one char from each group (string) will be used.
[String[]]$InputStrings = #('abcdefghijkmnpqrstuvwxyz', 'ABCEFGHJKLMNPQRSTUVWXYZ', '23456789', '!"#%&'),
# Specifies a string containing a character group from which the first character in the password will be generated.
# Useful for systems which requires first char in password to be alphabetic.
[String] $FirstChar,
# Specifies number of passwords to generate.
[ValidateRange(1,2147483647)]
[int]$Count = 1
)
Begin {
Function Get-Seed{
# Generate a seed for randomization
$RandomBytes = New-Object -TypeName 'System.Byte[]' 4
$Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider'
$Random.GetBytes($RandomBytes)
[BitConverter]::ToUInt32($RandomBytes, 0)
}
}
Process {
For($iteration = 1;$iteration -le $Count; $iteration++){
$Password = #{}
# Create char arrays containing groups of possible chars
[char[][]]$CharGroups = $InputStrings
# Create char array containing all chars
$AllChars = $CharGroups | ForEach-Object {[Char[]]$_}
# Set password length
if($PSCmdlet.ParameterSetName -eq 'RandomLength')
{
if($MinPasswordLength -eq $MaxPasswordLength) {
# If password length is set, use set length
$PasswordLength = $MinPasswordLength
}
else {
# Otherwise randomize password length
$PasswordLength = ((Get-Seed) % ($MaxPasswordLength + 1 - $MinPasswordLength)) + $MinPasswordLength
}
}
# If FirstChar is defined, randomize first char in password from that string.
if($PSBoundParameters.ContainsKey('FirstChar')){
$Password.Add(0,$FirstChar[((Get-Seed) % $FirstChar.Length)])
}
# Randomize one char from each group
Foreach($Group in $CharGroups) {
if($Password.Count -lt $PasswordLength) {
$Index = Get-Seed
While ($Password.ContainsKey($Index)){
$Index = Get-Seed
}
$Password.Add($Index,$Group[((Get-Seed) % $Group.Count)])
}
}
# Fill out with chars from $AllChars
for($i=$Password.Count;$i -lt $PasswordLength;$i++) {
$Index = Get-Seed
While ($Password.ContainsKey($Index)){
$Index = Get-Seed
}
$Password.Add($Index,$AllChars[((Get-Seed) % $AllChars.Count)])
}
Return $(-join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value))
}
}
}
New-SWRandomPassword
EDIT:::
https://gallery.technet.microsoft.com/scriptcenter/Generate-a-random-and-5c879ed5
The script can be found here.
Short version (best method for me):
$possible=36..38 + 40..43 + 45..58 + 60..94 + 97..123 + 125..126 + 33
(get-random -count 10 -input $possible | % {[char]$_}) -join ''
Using the following script seems to have worked exactly how I want.
I removed the comma (,) from after += on this line:
$ascii = $NULL;For ($a = 33;$a –le 126;$a++) {$ascii +=, ([char][byte]$a | Where-Object {$_ -notin "'",'`','|','_',";",'"',','})}
I created a blank array before the array is added to:
$ascii = #()
The full code block is below:
Function Get-TempPassword {
$TempPassword = $null
$ascii = #()
For ($a = 33;$a –le 126; $a++) { $ascii += ([char][byte]$a | Where-Object { $_ -notin "'",'`','|','_',";",'"',',' }) }
Do {$TempPassword += $ascii | Get-Random; $loop++}
Until ($loop -eq 11)
return $TempPassword
}
Recursive method :
Function random-password ($length = 10)
{
$Assembly = Add-Type -AssemblyName System.Web
$password = [System.Web.Security.Membership]::GeneratePassword($length, 2)
$desablechar = "[``'|_;,`"]"
if ($password -match $desablechar )
{
random-password $length
}
else
{
$password
}
}
random-password
try Something like this :
Function random-password ($length = 10)
{
$possible=36..38 + 40..43 + 45..58 + 60..94 + 97..123 + 125..126 + 33
$password = get-random -count $length -input $possible |
% -begin { $aa = $null } -process {$aa += [char]$_} -end {$aa}
return $password
}
random-password
I have rectified my others propositions, but i propose an other method :)
Function random-password2 ($length = 10)
{
$Assembly = Add-Type -AssemblyName System.Web
$password = [System.Web.Security.Membership]::GeneratePassword(50, 2)
$possible=36..38 + 40..43 + 45..58 + 60..94 + 97..123 + 125..126 + 33
$newchar=[char](get-random -count 1 -input $possible)
$password=$password -replace "[`'|_;,]", $newchar
$password.Substring(0, $length)
}
random-password2

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.

Resources