Why is the Export-Csv not working with PSCustomObject? [duplicate] - powershell

pracl is a sysinternal command that can be used to list the ACLs of a directory. I have a list of shares and I want to create a csv file such that for each ACL entry, I want the share path in one column and share permission in the next. I was trying to do that by using the following code
$inputfile = "share.txt"
$outputFile = "out.csv"
foreach( $path in Get-Content $inputfile)
{
$results=.\pracl.exe $path
{
foreach ($result in $results) {write-host $path,$line}
}
$objResult = [pscustomobject]#{
Path = $Path
Permission = $line
}
$outputArray += $objResult
$objresult
}
$outputArray | Export-Csv -Path $outputfile -NoTypeInformation
It failed with the following error :-
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
At C:\Users\re07393\1\sample.ps1:14 char:1
+ $outputArray += $objResult
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Any suggestions ?

You're trying to create an array of [pscustomobject]s in your $outputArray variable iteratively, using +=, but you're not initializing $outputArray as an array - see the bottom section for an explanation of the resulting behavior.
Thus, the immediate solution to your problem is to do just that:
# Do this before your `foreach` loop, then `+=` will work for appending elements.
$outputArray = #()
However, using += to add to arrays is inefficient, because in reality a new array instance must be created every time, because arrays are immutable data structures. That is, every time += is used, PowerShell creates a new array instance behind the scenes to which the existing elements as well as the new element are copied.
A simpler and much more efficient approach is to let PowerShell create an array for you, by using the foreach loop as an expression and assigning it to a variable as a whole:
That is, whatever is output in every iteration of the loop is automatically collected by PowerShell:
A simplified example:
# Create an array of 10 custom objects
[array] $outputArray = foreach ($i in 1..10) {
# Create and implicitly output a custom object in each iteration.
[pscustomobject] #{
Number = $i
}
}
Note the use of type constraint [array] to the left of $outputArray, which ensures that the variable value is always an array, even if the loop happens to produce just one output object (in which case PowerShell would otherwise just store that object itself, and not wrap it in an array).
Note that you can similarly use for, if, do / while / switch statements as expressions.
In all cases, however, these statements can only serve as expressions by themselves; regrettably, using them as the first segment of a pipeline or embedding them in larger expressions does not work - see GitHub issue #6817.
As for what you tried:
$outputArray += $objResult
Since you didn't initialize $outputArray before the loop, the variable is implicitly created in the loop's first iteration:
If the LHS variable doesn't exist yet, += is effectively the same as =: that is, the RHS is stored as-is in the LHS variable, so that $outputArray now contains a [pscustomobject] instance.
In the second iteration, because $outputArray now has a value, += now tries to perform a type-appropriate + operation (such as numeric addition for numbers, and concatenation for strings), but no + (op_Addition()) operation is defined for type [pscustomobject], so the operation fails with the error message you saw.

Related

Trying to use += for Get-DistributionGroupMember results in empty variable [duplicate]

pracl is a sysinternal command that can be used to list the ACLs of a directory. I have a list of shares and I want to create a csv file such that for each ACL entry, I want the share path in one column and share permission in the next. I was trying to do that by using the following code
$inputfile = "share.txt"
$outputFile = "out.csv"
foreach( $path in Get-Content $inputfile)
{
$results=.\pracl.exe $path
{
foreach ($result in $results) {write-host $path,$line}
}
$objResult = [pscustomobject]#{
Path = $Path
Permission = $line
}
$outputArray += $objResult
$objresult
}
$outputArray | Export-Csv -Path $outputfile -NoTypeInformation
It failed with the following error :-
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
At C:\Users\re07393\1\sample.ps1:14 char:1
+ $outputArray += $objResult
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Any suggestions ?
You're trying to create an array of [pscustomobject]s in your $outputArray variable iteratively, using +=, but you're not initializing $outputArray as an array - see the bottom section for an explanation of the resulting behavior.
Thus, the immediate solution to your problem is to do just that:
# Do this before your `foreach` loop, then `+=` will work for appending elements.
$outputArray = #()
However, using += to add to arrays is inefficient, because in reality a new array instance must be created every time, because arrays are immutable data structures. That is, every time += is used, PowerShell creates a new array instance behind the scenes to which the existing elements as well as the new element are copied.
A simpler and much more efficient approach is to let PowerShell create an array for you, by using the foreach loop as an expression and assigning it to a variable as a whole:
That is, whatever is output in every iteration of the loop is automatically collected by PowerShell:
A simplified example:
# Create an array of 10 custom objects
[array] $outputArray = foreach ($i in 1..10) {
# Create and implicitly output a custom object in each iteration.
[pscustomobject] #{
Number = $i
}
}
Note the use of type constraint [array] to the left of $outputArray, which ensures that the variable value is always an array, even if the loop happens to produce just one output object (in which case PowerShell would otherwise just store that object itself, and not wrap it in an array).
Note that you can similarly use for, if, do / while / switch statements as expressions.
In all cases, however, these statements can only serve as expressions by themselves; regrettably, using them as the first segment of a pipeline or embedding them in larger expressions does not work - see GitHub issue #6817.
As for what you tried:
$outputArray += $objResult
Since you didn't initialize $outputArray before the loop, the variable is implicitly created in the loop's first iteration:
If the LHS variable doesn't exist yet, += is effectively the same as =: that is, the RHS is stored as-is in the LHS variable, so that $outputArray now contains a [pscustomobject] instance.
In the second iteration, because $outputArray now has a value, += now tries to perform a type-appropriate + operation (such as numeric addition for numbers, and concatenation for strings), but no + (op_Addition()) operation is defined for type [pscustomobject], so the operation fails with the error message you saw.

Array Union functionality [duplicate]

I am using Get-ChildItem to fetch locally installed certs. Then I'd like to delete these certs from my local certificate store. If I execute the script below it will work when there is more than one item returned in both the Get-ChildItem queries. But if only one item is returned in either of the queries the addition of the two collections will fail. Simply because with only one object returned the implementation only returns one instance and not a collection of one. What is the right approach here? I won't know if the result(s) returned from my queries will result in 0, 1 or multiple certs.
$myCerts = Get-ChildItem cert:\CurrentUser\My | Where-Object { $_.Subject -like "CN=$certName" }
$trustedPeopleCerts = Get-ChildItem cert:\CurrentUser\TrustedPeople | Where-Object { $_.Subject -like "CN=$certName" }
$allCerts = $myCerts + $trustedPeopleCerts
foreach ($cert in $allCerts) {
$store = Get-Item $cert.PSParentPath
$store.Open('ReadWrite')
$store.Remove($cert)
$store.Close()
}
If $myCerts might just be a single item, use the array subexpression operator #( ):
$allCerts = #($myCerts) + $trustedPeopleCerts
Another solution that accomplishes the same thing is to use a unary comma operator to ensure that $myCerts is always an array. Consider the following examples that we are using for one element and 0 elements.
$myCerts = Get-Item C:\temp
$myCerts.GetType().FullName
$myCerts = $null
$myCerts.GetType().FullName
The above would generate System.IO.DirectoryInfo and an error for "call[ing] a method on a null-valued expression"
Now lets try the same thing with unary comma operator.
$myCerts = ,(Get-Item C:\temp)
$myCerts = ,($null)
The answer in both cases is System.Object[]
I forgot about one thing this is doing. #() might be preferential in this case as it is an array sub expression. The comma operator I think here is creating a multidimentional array. When used in a pipe it is unrolled and you still get the data you are looking for but it is an important difference to note.
I found a way to do it using a pipeline in case arrays aren't playing nicely. This will write each item to the local pipeline inside the ScriptBlock, which gets captured to the variable.
$merged = & {
$foo
$bar
}

In PowerShell, everytime I execute the script the array gets bigger

So I'm starting with PowerShell and this is odd to me. I put a breakpoint in the last line and when I run the script $newArray is "abc" then it pauses in Write-Output and I stop the debugger. If I run it again, $newArray is "abcabc" and so on. For one, I think I'm doing something wrong as this behavior is so weird to me, like it stores in memory despite of stopping the debugger. And secondly I would expect $newArray to be an array and not a single string with the values concatenated. Any clues?
$array = "a", "b", "c";
$newArray;
foreach ($item in $array )
{
$newArray += $item
}
Write-Output "Just before ending script";
tl;dr
In order to use += to iteratively build up an array, you must (re)initialize the target variable as an empty array: $newArray = #(); see the bottom section for what happens if you don't.
Re repeated debugging passes: unfortunately, both the obsolescent Windows PowerShell ISE and the PowerShell extension for Visual Studio Code run in the same session, so that subsequent invocations can be affected by the state of previous ones. However, the Visual Studio PowerShell extension can be configured to use a new session every time - see this answer.
That said, it's usually better not to use +=, because it is inefficient: it requires creating a new array behind the scenes in every iteration, because .NET arrays are immutable data structures (in terms of element count).
In your simple case, you could simply use the following to create a (shallow) copy of $array, via #(), the array-subexpression operator, which implicitly clones an array:
$newArray = #($array)
If you need to perform an operation on each element of the source array, you can take advantage of the fact that you can use a foreach loop as an expression, which implicitly collects the iterations' outputs in an array - assuming that there's more than one output:
[array] $newArray = foreach ($item in $array) { $item + '!' }
Note the [array] type constraint, which ensures that $newArray receives an array even if the foreach loop happens to output just one object; alternatively, you could have wrapped the loop into #(...)
How += works:
In short: if the first value you add to an uninitialized variable with += is a string, that string is stored as-is, and all subsequent += operations perform string concatenation; specifically:
$newArray += $item is syntactic sugar for $newArray = $newArray + $item.
If $newArray has not been defined, its implied value is $null[1]
Since your $item values are strings ([string]), $newArray += $item in the first iteration amounts to $newArray = $null + "a", which assigns "a" - a single string - to $newArray (with a [string] RHS, $null on the LHS is treated like an empty string, performing string concatenation that effectively returns the RHS string in this case).
The second iteration amounts to $newArray = "a" + "b", which - given that the LHS is a string - again performs string concatenation, so that $newArray contains "ab" afterwards.
The third (and any hypothetical subsequent iterations) simply keep appending to the string stored in $newArray.
Generally speaking:
In the first += iteration of $initiallyUninitialized += <value>, $null + <value> is evaluated, which stores <value> in $initiallyUninitialized and effectively types it as whatever type <value> happens to be.
In subsequent += iterations, the then-current type of the $initiallyUninitialized value determines the result of the implied $initiallyUninitialized + <value> operation - which typically, but not necessarily, preserves that type:
The type may automatically widen in the case of a numeric type:
$a += [int]::MaxValue; $a += [int]::MaxValue - $a is now of type [double].
The operation may fail with a statement-terminating error if the + operation cannot be performed:
$a += Get-Date; $a += Get-Date - causes a statement-terminating error, because two [datetime] instances cannot be added (+).
[1] This applies by default (Set-StrictMode -Off); with Set-StrictMode -Version 1 or higher, you'd actually get a statement-terminating error.
Setting an array's value to #() will clear it
Example: Add $newArray = #(); to the end of the script
Do you want it to keep the full list in one of the arrays or do you want it cleared every time you run it?

Powershell - Iterate through variables dynamically

I am importing a CSV file with two records per line, "Name" and "Path".
$softwareList = Import-Csv C:\Scripts\NEW_INSTALLER\softwareList.csv
$count = 0..($softwareList.count -1)
foreach($i in $count){
Write-Host $softwareList[$i].Name,$softwareList[$i].Path
}
What I am trying to do is dynamically assign the Name and Path of each record to a WPFCheckbox variable based on the $i variable. The names for these checkboxes are named something such as WPFCheckbox0, WPFCheckbox1, WPFCheckbox2 and so on. These objects have two properties I planned on using, "Command" to store the $SoftwareList[$i].path and "Content" to store the $SoftwareList[$i].Name
I cannot think of a way to properly loop through these variables and assign the properties from the CSV to the properties on their respective WPFCheckboxes.
Any suggestions would be very appreciated.
Invoke-Expression is one way, though note Mathias' commented concerns on the overall approach.
Within your foreach loop, you can do something like:
invoke-expression "`$WPFCheckbox$i`.Command = $($SoftwareList[$i].Path)"
invoke-expression "`$WPFCheckbox$i`.Content= $($SoftwareList[$i].Name)"
The back-tick ` just before the $WPFCheckBox prevents what would be an undefined variable from being immediately evaluated (before the expression is invoked), but the $I is. This gives you a string with your $WPFCheckbox1, to which you then append the property names and values. The $SoftwareList values are immediately processed into the raw string.
The Invoke-Expression then evaluates and executes the entire string as if it were a regular statement.
Here's a stand-alone code snippet to play with:
1..3 |% {
invoke-expression "`$MyVariable$_` = New-Object PSObject"
invoke-expression "`$MyVariable$_` | add-member -NotePropertyName Command -NotePropertyValue [String]::Empty"
invoke-expression "`$MyVariable$_`.Command = 'Path #$_'"
}
$MyVariable1 | Out-String
$MyVariable2 | Out-String
$MyVariable3 | Out-String
As a side note (since I can't comment yet on your original question,) creating an array just to act as iterator through the lines of the file is really inefficient. There are definitely better ways to do that.

Is Arraylist passed to functions by reference in PowerShell

I found out today that an arraylist I passed to a function gets changed when I remove a value from the arraylist within the function. The code below seems to imply that passing is happening by reference. Why would that be? Is this by design or some kind of bug? (I am using v4 on Win 8.1)
function myfunction {
param (
[System.Collections.ArrayList]$local
)
"`$local: " + $local.count
"removing 1 from `$local"
$local.RemoveAt(0)
"`$local:" + $local.count
}
[System.Collections.ArrayList]$names=(Get-Content c:\temp\names.txt)
"`$names: " + $names.count
myfunction -local $names
"`$names: " + $names.count
RESULT:
$names: 16
$local: 16
removing 1 from $local
$local:15
$names: 15
This is by design, and is not a bug. Arrays, collections and hash tables are passed by ref. The reason this behaves differently than adding or removing from an array is that operation creates a new array inside the function scope. Any time you create a new variable inside the function, it is scoped to the function. $local.RemoveAt(0) doesn't create a new $local, it just calls a function of the existing $local in the parent script. If you want the function to operate on it's own $local, you need to explicitly create a new one inside the function.
Because it's by ref, this won't work:
$local = $local
You'll still be referencing $local in the parent scope.
But you can use the clone() method to create a new copy of it
function testlocal {
param ([collections.arraylist]$local)
$local = $local.Clone()
$local.RemoveAt(0)
$local
}
$local = [collections.arraylist](1,2,3)
'Testing function arraylist'
testlocal $local
''
'Testing local arraylist'
$local
Testing function arraylist
2
3
Testing local arraylist
1
2
3
mjolinor's helpful answer provides the crucial pointer: To have the function operate on a copy of the input ArrayList, it must be cloned via .Clone() first.
Unfortunately, the explanation offered there for why this is required is not correct:[1]
No PowerShell-specific variable behavior comes into play; the behavior is fundamental to the .NET framework itself, which underlies PowerShell:
Variables are technically passed by value (by default[2]), but what that means depends on the variable value's type:
For value types, for which variables contain the data directly, a copy of the actual data is made.
For reference types, for which variables only contain a reference to the data, a copy of the reference is made, resulting in effective by-reference passing.
Therefore, in the case at hand, because [System.Collections.ArrayList] is a reference type (verify with -not [System.Collections.ArrayList].IsValueType), parameter $local by design points to the very same ArrayList instance as variable $names in the calling scope.
Unfortunately, PowerShell can obscure what's happening by cloning objects behind the scenes with certain operations:
Using += to append to an array ([System.Object[]]):
$a = 1, 2, 3 # creates an instance of reference type [Object[]]
$b = $a # $b and $a now point to the SAME array
$a += 4 # creates a NEW instance; $a now points to a DIFFERENT array.
Using += to append to a [System.Collections.ArrayList] instance:
While in the case of an array ([System.Object[]) a new instance must be created - because arrays are by definition of fixed size - PowerShell unfortunately quietly converts a [System.Collections.ArrayList] instance to an array when using += and therefore obviously also creates a new object, even though [System.Collections.ArrayList] can be grown, namely with the .Add() method.
$al = [Collections.ArrayList] #(1, 2, 3) # creates an ArrayList
$b = $al # $b and $al now point to the SAME ArrayList
$al += 4 # !! creates a NEW object of type [Object[]]
# By contrast, this would NOT happen with: $al.Add(4)
Destructuring an array:
$a = 1, 2, 3 # creates an instance of reference type [Object[]]
$first, $a = $a # creates a NEW instance
[1] mjolinor's misconception is around inheriting / shadowing of variables from the parent (ancestral) scope: A parameter declaration is implicitly a local variable declaration. That is, on entering testlocal() $local is already a local variable containing whatever was passed as the parameter - it never sees an ancestral variable of the same name. The following snippet demonstrates this: function foo([string] $local) { "`$local inside foo: $local" }; $local = 'hi'; "`$local in calling scope: $local"; foo; foo 'bar' - foo() never sees the calling scope's definition of $local.
[2] Note that some .NET languages (e.g., ref in C#) and even PowerShell itself ([ref]) also allow passing a variable by reference, so that the local parameter is effectively just an alias for the calling scope's variable, but this feature is unrelated to the value/reference-type dichotomy.
If you pass myfunction -local $names, then $names = $local and it looks like your function is to delete $local # position 1 (affecting $names, remember). The next time you read the variable it has been modified, so fix is to double grep $names variable returning the count to 16.
[System.Collections.ArrayList]$names=(Get-Content c:\temp\names.txt)
"`$names: " + $names.count
myfunction -local $names
# $names variable is altered now, so re-run grep.
[System.Collections.ArrayList]$names=(Get-Content c:\temp\names.txt)
"`$names: " + $names.count
Or re-write / pipe your function as a skip instead of on-the-fly deletion ???
Select-Object -Skip 1
Here is some further proof - note the write-host commands will display both variables being edited at the same time by the function.
function myfunction {
param (
[System.Collections.ArrayList]$local
)
Write-Host $names
Write-Host $local
"`$local: " + $local.count
"removing 1 from `$local"
$local.RemoveAt(0)
"`$local: " + $local.count
Write-Host $names
Write-Host $local
}
[System.Collections.ArrayList]$names=(Get-Content c:\temp\names.txt)
"`$names: " + $names.count
myfunction -local $names
"`$names: minus 1 - bork bork bork " + $names.count
[System.Collections.ArrayList]$names=(Get-Content c:\temp\names.txt)
"`$names: " + $names.count