Is a string parameter sent to PowerShell not really a string? - powershell

I am bit confused of the behaviour of the script below:
Test.ps1:
param(
[array]$Value = $(throw "Give me a value")
)
Write-Host $Value
$Value | Get-Member -MemberType Method
$Value.ToUpper()
Running the script:
PS C:\Temp> .\weird.ps1 test
TypeName: System.String
Name MemberType Definition
—- ———- ———-
…
ToUpper Method string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
…
Method invocation failed because [System.Object[]] doesn’t contain a method named ‘ToUpper’.
At C:\Temp\weird.ps1:6 char:15
+ $Value.ToUpper <<<< ()
+ CategoryInfo : InvalidOperation: (ToUpper:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Why do I get a MethodNotFound exception? Get-Member clearly says it is a string.

What's happening here is that the variable $value is typed to Object[] in the script. The call to Get-Member works because you are piping the value into the function. Hence instead of seeing the array it sees the values in the array which are indeed typed to String. This can be viewed by using the following Get-Member call without piping
Get-Member -MemberType Method -InputObject $value
This is also why ToUpper correctly fails (it's an array not a String).

When you pipe a collection of anything in powershell, it gets "unrolled" and sent one by one to the right hand side of the bar (pipe) character. This means that the contents of the array get sent to get-member. Get-member only accepts the first item sent to it, so it shows you the members of the string. Your parameter is of type [array], so the parameter binder sets $value to an array of length 1, containing your string "test."
Example:
ps> #(1,"hello",3) | gm
... shows members of int32
ps> #("hello", 1, 3) | gm
... shows members of string
In order to see the members of an array when you pipe it, you should wrap it in another array, so it becomes the unrolled item:
ps> ,#("hello", 1, 3) | gm
... shows members of array
The leading comma "," creates a wrapping array.
-Oisin

$Value is actually an array, because that is how you declare the parameter in your param block.

Related

How do I access values in an ordered PowerShell hash table using integer keys?

My requirement is to store integer keys and access hash table values using those integer keys in an ordered hash table.
What works
When I use string keys, no problem:
cls
$foo=[ordered]#{}
$foo.add("12",1)
$foo.add("24",2)
write-host ("first item=" + $foo.Item("12"))
write-host ("second item=" + $foo.Item("24"))
Output:
first item=1
second item=2
Using Brackets Fails
When I use brackets, the program doesn't throw an exception, but it returns nothing:
$fooInt=[ordered]#{}
$fooInt.add(12,1)
$fooInt.add(24,2)
write-host ("first item=" + $fooInt[12])
write-host ("second item=" + $fooInt[24])
Output:
first item=
second item=
Using the Item method Fails
When I use the Item method and integer keys, PowerShell interprets the integer key as an index and not a key:
$fooInt=[ordered]#{}
$fooInt.add(12,1)
$fooInt.add(24,2)
write-host ("first item=" + $fooInt.Item(12))
write-host ("second item=" + $fooInt.Item(24))
Output:
Exception getting "Item": "Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index"
At line:8 char:1
+ write-host ("first item=" + $fooInt.Item(12))
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], GetValueInvocationException
+ FullyQualifiedErrorId : ExceptionWhenGetting
Exception getting "Item": "Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index"
At line:9 char:1
+ write-host ("second item=" + $fooInt.Item(24))
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], GetValueInvocationException
+ FullyQualifiedErrorId : ExceptionWhenGetting
How do I access values in a PowerShell hashtable using an integer key?
They keys in a hashtable are objects, not strings. When you're attempting to access the key "12" with the integer 12, it cannot find that entry because the keys don't match.
HOWEVER, you're not using a standard hashtable, you're using an ordered hashtable which has a different Item method on it since it can work by key or index. If you want to access the integer key with an ordered hashtable, you need to use a different syntax:
$hash.12
If you use the array accessor syntax:
$hash[12]
it will try to return the 13th item in the list.
You can observe the difference between these objects by using Get-Member:
$orderedHash | Get-Member Item
TypeName: System.Collections.Specialized.OrderedDictionary
Name MemberType Definition
---- ---------- ----------
Item ParameterizedProperty System.Object Item(int index) {get;set;}, System.Object Item(System.Object key) {get;set;}
$hash | Get-Member Item
TypeName: System.Collections.Hashtable
Name MemberType Definition
---- ---------- ----------
Item ParameterizedProperty System.Object Item(System.Object key) {get;set;}
After some more experimentation, this is only the case on the int32 type. If you define and access it with a different type, it will work since it's no longer matching the overloaded int signature:
$hash = [ordered]#{
([uint32]12) = 24
}
$hash[[uint32]12]
> 24
Summary
$fooInt.Item([object]12) or $fooInt[[object]12]
Reasoning
As seen in TheIncorrigible1's answer, .Item has overloads; it is backed by the method get_Item:
PS C:\> $fooInt.get_Item
OverloadDefinitions
-------------------
System.Object get_Item(int index)
System.Object get_Item(System.Object key)
System.Object IOrderedDictionary.get_Item(int index)
System.Object IDictionary.get_Item(System.Object key)
The version which takes an integer and does indexing comes from the IOrderedDictionary interface, the normal IDictionary key lookup takes a [System.Object]. When you try to use it with an integer argument, PowerShell binds the version which takes an [int] because it's a better match, and runs that one.
Earlier, I made a comment of how you could use reflection to pick out the overload you want, and invoke that, but it's ugly:
$fooInt.GetType().GetMethods().where{
$_.Name -eq 'get_Item' -and $_.GetParameters().Name -eq 'Key'
}.Invoke($fooInt, 'Public', $null, 12, $null)
^ your parameter
Thinking on it, [int] is a value type, not a reference type, and that means .Net has to box it into an object to put it in a Hashtable. So maybe if you also box your integer parameter into an object when doing a lookup, PowerShell might bind to the overload you want and do a key lookup and still match the correct key .. what do you know, it works:
PS C:\> $fooInt=[ordered]#{}
PS C:\> $fooInt.add(12,1)
PS C:\> $fooInt.add(24,2)
PS C:\> write-host ("first item=" + $fooInt.Item([object]12))
first item=1
And it works for indexing, too:
PS C:\> write-host ("first item=" + $fooInt[[object]12])
first item=1
Which is very close to TheIncorrigible1's experiment, except you don't need to define the dictionary with the key typed as something else and then cast your lookups to a matching type, you only need to access it with casting to object, because that's happening internally already for the keys you define.

Method invocation failed because [System.Management.Automation.PSObject] doesn't contain a method named 'op_Addition'

I am trying to export some data from sharepoint list to csv and I got this error:
$ListItemCollection | Export-CSV "D:\LX.csv" -NoTypeInformation
Method invocation failed because [System.Management.Automation.PSObject] doesn't contain a method named 'op_Addition'.
At line:20 char:2
+ $ListItemCollection += $ExportItem
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
the code is really easy
$URL = "https://mysite"
$List = "Page Contents"
$Web = Get-SPWeb $URL
$web
$spList = $Web.Lists[$List]
$Items = $spList.GetItems()
$listItems = $spList.Items
foreach($item in $listItems) {
$ExportItem = New-Object PSObject
$ExportItem | Add-Member -MemberType NoteProperty -name "PageID" -value $item["PageID"]
$ExportItem | Add-Member -MemberType NoteProperty -Name "Html Component" -value $item["Html Component"]
#Add the object with property to an Array
$ListItemCollection += $ExportItem
}
tl;dr:
$ListItemCollection is of type [System.Management.Automation.PSObject], not an array.
Make sure that it is an array (e.g., $ListItemCollection = #()) for += to work as intended, i.e., for += to append an element[1].
Note that commands that typically output multiple items - which are then collected in a regular [object[]] array, if assigned to a variable - output only a scalar if the command situationally happens to return only one item; in other words: a single-item output array is automatically unwrapped.
Therefore, if there's a chance that a command situationally returns only a single object, yet you need the result to always be an array, use #(...), the array-subexpression operator; e.g.:
# #(...) ensures that $file is an array, even if just 1 file matches
$files = #(Get-ChildItem *.txt)
The error message implies that $ListItemCollection is of type [System.Management.Automation.PSObject] and not an array.
Since type [pscustomobject] ([System.Management.Automation.PSObject]) does not have a static op_Addition method, you cannot use the + operator with an instance of it as the LHS.
(Type-specific operators are implemented as static op_* methods).
You can verify this as follows:
PS> (New-Object System.Management.Automation.PSObject) + 1 # !! Breaks
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'
If you want to inspect a given type for operator support, use a command such as the following, using the [datetime] type as an example:
PS> [datetime] | Get-Member -Force -Static -Type Method op_*
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
op_Addition Method static datetime op_Addition(datetime d, timespan t)
op_Equality Method static bool op_Equality(datetime d1, datetime d2)
op_GreaterThan Method static bool op_GreaterThan(datetime t1, datetime t2)
op_GreaterThanOrEqual Method static bool op_GreaterThanOrEqual(datetime t1, datetime t2)
op_Inequality Method static bool op_Inequality(datetime d1, datetime d2)
op_LessThan Method static bool op_LessThan(datetime t1, datetime t2)
op_LessThanOrEqual Method static bool op_LessThanOrEqual(datetime t1, datetime t2)
op_Subtraction Method static datetime op_Subtraction(datetime d, timespan t), static timespan op_Subtraction(datetime d1, datetime d2)
Note:
The "primitive" .NET data types do not have such methods, because operator support for them is built in.
Similarly, it is PowerShell itself that implements + for arrays and collections ([object[]], [System.Collections.Generic.List[object]], ...), though note that:
a new instance is invariably constructed, and
the result is always of type [object[]] (unless you use a type-constrained variable that converts the array back to a different collection type).
-Force is needed, because Get-Member hides the op_* methods by default.
[1] Technically, a new array is created behind the scenes, because arrays are immutable. In loops this can be a performance concern; if so, use a mutable collection type such as [System.Collections.Generic.List[object]] and append to it with its .Add() method.

What is the logic behind flags and methods?

For example:
> "a,b,c" -split ","
a
b
c
> "a,b,c".split(",")
a
b
c
> "a,b,c".length
5
> "a,b,c" -length
At line:1 char:9
+ "a,b,c" -length
+ ~~~~~~~
Unexpected token '-length' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnexpectedToken
So not every method can be expressed as an flag/argument list. I'm not even sure that .split and -split are the same thing or whether this is by chance.
When should I expect to use a flag and when a method? How do I discover all flags available (for a string, a number, etc.
Another thing is that ls -? returns a help text, but "foo" -? doesn't. So while it accepts flags, it is not really treated as a command
It boils down to this.
This ... "a,b,c" -length
About Operators
An operator is a language element that you can use in a command or expression.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-5.1
Because there are several operator types, the above is not a single source doc reference.
Vs this... "a,b,c".length
About methods
Describes how to use methods to perform actions on objects in PowerShell.
Methods allow you to examine, compare and format many properties of a PowerShell Object, perform an action.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_methods?view=powershell-5.1
From your example :
String.Split Method
Returns a string array that contains the substrings in this instance that are delimited by elements of a specified string or Unicode character array
https://msdn.microsoft.com/en-us/library/system.string.split(v=vs.110).aspx
About Split
The Split operator splits one or more strings into substrings.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_split?view=powershell-5.1
String.Length Property
The Length property returns the number of Char objects in this instance, not the number of Unicode characters.
You'll note if you open this in the PowerShell_ISE.exe or VSCode, you'll see right away, the item 4 is immediately shown as an syntax error even before you run. This is denoted by the red squiggle. That marker means it will never work, so no real reason to try it.
Just because you can type it, does not make it right. If you type a '-' after a space for anything you get a list of what is expected. Well, if you are in the PowerShel_ISE or Visual Studio Code. If you are in the PowerShell console host, you have to hit the tab key to tab through the list or use CRTL + Spacebar to see the full list, then tab or arrow to what you want to use.
('a,b,c').length # this is an array, and this is returning the count of the elements in the array
5
('a,b,c','d,e,f').length # note the element count difference
2
('a,b,c').Length # property use of a .Net class
5
('a,b,c') -length # attempted unknown / invalid switch (PowerShell operator)
To know what you can and cannot do to an object or how, you have to what it supports. That is what Get-Member is for.
So the Array allows this..
('a,b,c') | Get-Member
and this..
('abc') | Get-Member
Most common listed
Name MemberType Definition
---- ---------- ----------
...
Split Method string[] Split(Params char[] separator), string[] Split(char[] separator, int count), string[] Sp...
...
Substring Method string Substring(int startIndex), string Substring(int startIndex, int length)
...
ToLower Method string ToLower(), string ToLower(cultureinfo culture)
...
ToString Method string ToString(), string ToString(System.IFormatProvider provider), string IConvertible.ToString...
...
ToUpper Method string ToUpper(), string ToUpper(cultureinfo culture)
...
Trim Method string Trim(Params char[] trimChars), string Trim()
TrimEnd Method string TrimEnd(Params char[] trimChars)
TrimStart Method string TrimStart(Params char[] trimChars)
Chars ParameterizedProperty char Chars(int index) {get;}
Length Property int Length {get;}
As for this...
When should I expect to use a flag and when a method?
How do I discover all flags available (for a string, a number, etc.
You have to read the help file on the cmdlet you are trying to use, or at minimum it's examples.
Get-Help -Name Get-ItemProperty -Full
Get-Help -Name Get-ItemProperty -Examples
Then info on the cmdlet / function you are trying to use
(Get-Command -Name Get-ItemProperty).Parameters
switches (flags) which will expect a value to the right of it or not, see the property values line below
Then what you can use the cmdlet/function on.
Get-ItemProperty -Path D:\Temp | Format-Table -AutoSize -Wrap
Get-ItemProperty -Path D:\Temp | Format-List
Get-ItemProperty -Path D:\Temp | Format-List -Force
Get-ItemProperty -Path D:\Temp | Select-Object -Property * # property values
(Get-ItemProperty -Path D:\Temp) | Get-Member
As for this...
Another thing is that ls -? returns a help text, but "foo" -? doesn't.
So while it accepts flags, it is not really treated as a command
Foo is not a valid name of anything in PowerShell, unless you created a foo function or module. So, it should hot return anything.
Again, just because you can type it, does not make it correct.
In most cases, if you are doing the above and you do not get automatic intellisense, then what you are doing is probably wrong.
To see all the cmdlets, functions, etc you have on your system for use. You do this.
Get-Command

Where-Object filter unexpectedly matches the whole input collection, Get-Member shows collection type instead of element type

My Get-AzureStorSimpleAccessControlRecord call returns 2 objects, with
Name property values servertest4 and servertest3.
However,
Get-AzureStorSimpleAccessControlRecord | where {$_.Name -like '*servertest4*'}
is unexpectedly returning both objects.
If I pass it through a for-each loop, the where statement is working to capture that property.
Get-AzureStorSimpleAccessControlRecord | % {$_.Name} | where {$_ -like 'servertest4'}
servertest4
Passing the original command to Get-Member unexpectedly doesn't show the Name property. Is that the problem?
If so, why does it have it as a column and why does it work when I pass it through the for-each loop? This is all it returned for properties (I stripped the methods out):
Get-AzureStorSimpleAccessControlRecord | gm
TypeName: System.Collections.Generic.List`1[[Microsoft.WindowsAzure.Management.StorSimple.Models.AccessControlRecord, Microsoft.WindowsAzure.Management.StorSimple,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]
Name MemberType Definition
Item ParameterizedProperty
Microsoft.WindowsAzure.Management.StorSimple.Models.AccessControlRecord Item(int index) {get;set;}
Capacity Property int Capacity {get;set;}
Count Property int Count {get;}
IsFixedSize Property bool IsFixedSize {get;}
IsReadOnly Property bool IsReadOnly {get;}
IsSynchronized Property bool IsSynchronized {get;}
SyncRoot Property System.Object SyncRoot {get;}
What am I missing?
It seems that Get-AzureStorSimpleAccessControlRecord is ill-behaved and sends its output collection as a whole through the pipeline, as a single object, instead of element by element.
You should be able work around that by enclosing the call in (...):
(Get-AzureStorSimpleAccessControlRecord) |
Where-Object { $_.Name -like '*servertest4*' }
Enclosing the call in (...), the grouping operator makes PowerShell enumerate the collection, i.e. it sends its elements one by one through the pipeline, as is normally expected.
Optional reading: What happens without the workaround?
The following simplified example simulates Get-AzureStorSimpleAccessControlRecord's undesired behavior by creating an [int[]] array that is artificially wrapped in another array by way of the unary form of the array-construction operator, ,.
Sending this wrapped collection through the pipeline causes it to be sent as a whole instead of item by item.
$wrappedColl = , [int[]] (1, 2) # create artificially wrapped collection
Now, Get-Member reports the wrapped collection type instead of the type of the individual items, as normally happens (the pipeline unwrapped the outer array and sent its only element - the inner [int[]] array - as-is):
PS> $wrappedColl | Get-Member
TypeName: System.Int32[] # !! Type of the wrapped array as a whole
...
The array being passed as a whole also gets in the way when applying a Where-Object filter, which is what prompted the question:
# Trying to extract element 2 outputs the whole array(!).
PS> $wrappedColl | Where-Object { $_ -eq 2 }
1
2
Since the [int[]] array is passed as a whole, that's what $_ is bound to inside the Where-Object script block.
Applying -eq to a LHS that is an array performs array filtering rather than returning a simple [bool] value, so that [int[]] (1, 2) -eq 2 returns the array of matching items, which in this case is [object[]] 2 (note the result array is alway a regular PS array, of type [object[]]).
The resulting (single-item) array is then interpreted as a Boolean in the context of the Where-Object cmdlet, and any non-empty array evaluates to $true when coerced to a Boolean.
Therefore, since the script block passed to Where-Object evaluated to $true, the input object is passed through, which in this case is, as stated, the whole [int[]] array.

Why and how are these two $null values different?

Apparently, in PowerShell (ver. 3) not all $null's are the same:
>function emptyArray() { #() }
>$l_t = #() ; $l_t.Count
0
>$l_t1 = #(); $l_t1 -eq $null; $l_t1.count; $l_t1.gettype()
0
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
>$l_t += $l_t1; $l_t.Count
0
>$l_t += emptyArray; $l_t.Count
0
>$l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype()
True
0
You cannot call a method on a null-valued expression.
At line:1 char:38
+ $l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype()
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
>$l_t += $l_t2; $l_t.Count
0
>$l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype()
True
You cannot call a method on a null-valued expression.
At line:1 char:32
+ $l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype()
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
>$l_t += $l_t3; $l_t.count
1
>function addToArray($l_a, $l_b) { $l_a += $l_b; $l_a.count }
>$l_t = #(); $l_t.Count
0
>addToArray $l_t $l_t1
0
>addToArray $l_t $l_t2
1
So how and why is $l_t2 different from $l_t3? In particular, is $l_t2 really $null or not? Note that $l_t2 is NOT an empty array ($l_t1 is, and $l_t1 -eq $null returns nothing, as expected), but neither is it truly $null, like $l_t3. In particular, $l_t2.count returns 0 rather than an error, and furthermore, adding $l_t2 to $l_t behaves like adding an empty array, not like adding $null. And why does $l_t2 suddenly seem to become "more $null" when it gets passed in the the function addToArray as a parameter???????
Can anyone explain this behaviour, or point me to documentation that would explain it?
Edit:
The answer by PetSerAl below is correct. I have also found this stackOverflow post on the same issue.
Powershell version info:
>$PSVersionTable
Name Value
---- -----
WSManStackVersion 3.0
PSCompatibleVersions {1.0, 2.0, 3.0}
SerializationVersion 1.1.0.1
BuildVersion 6.2.9200.16481
PSVersion 3.0
CLRVersion 4.0.30319.1026
PSRemotingProtocolVersion 2.2
In particular, is $l_t2 really $null or not?
$l_t2 is not $null, but a [System.Management.Automation.Internal.AutomationNull]::Value. It is a special instance of PSObject. It is returned when a pipeline returns zero objects. That is how you can check it:
$a=&{} #shortest, I know, pipeline, that returns zero objects
$b=[System.Management.Automation.Internal.AutomationNull]::Value
$ReferenceEquals=[Object].GetMethod('ReferenceEquals')
$ReferenceEquals.Invoke($null,($a,$null)) #returns False
$ReferenceEquals.Invoke($null,($a,$b)) #returns True
I call ReferenceEquals thru Reflection to prevent conversion from AutomationNull to $null by PowerShell.
$l_t1 -eq $null returns nothing
For me it returns an empty array, as I expect from it.
$l_t2.count returns 0
It is a new feature of PowerShell v3:
You can now use Count or Length on any object, even if it didn’t have the property. If the object didn’t have a Count or Length property, it will will return 1 (or 0 for $null). Objects that have Count or Length properties will continue to work as they always have.
PS> $a = 42
PS> $a.Count
1
 
And why does $l_t2 suddenly seem to become "more $null" when it gets passed in the the function addToArray as a parameter???????
It seems that PowerShell converts AutomationNull to $null in some cases, like calling .NET methods. In PowerShell v2, even when saving AutomationNull to a variable it gets converted to $null.
To complement PetSerAl's great answer with a pragmatic summary:
Commands that happen to produce no output do not return $null, but the [System.Management.Automation.Internal.AutomationNull]::Value singleton,
which can be thought of as an "array-valued $null" or, to coin a term, null enumeration. It is sometimes also called "AutomationNull", for its type name.
Note that, due to PowerShell's automatic enumeration of collections, even a command that explicitly outputs an empty collection object such as #() has no output (unless enumeration is explicitly prevented, such as with Write-Output -NoEnumerate).
In short, this special value behaves like $null in scalar contexts, and like an empty array in enumeration contexts, notably in the pipeline, as the examples below demonstrate.
Given that $null and the null enumeration situationally behave differently, distinguishing between the two via reflection may be necessary, which is currently far from trivial; GitHub issue #13465 proposes implementing a test that would allow you to use $someValue -is [AutomationNull].
As of PowerShell 7.3.0, the following, obscure test is required:
$null -eq $someValue -and $someValue -is [psobject]
Caveats:
Passing [System.Management.Automation.Internal.AutomationNull]::Value as a cmdlet / function parameter value invariably converts it to $null.
See GitHub issue #9150.
In PSv3+, even an actual (scalar) $null is not enumerated in a foreach loop; it is enumerated in a pipeline, however - see bottom.
In PSv2-, saving a null enumeration in a variable quietly converted it to $null and $null was enumerated in a foreach loop as well (not just in a pipeline) - see bottom.
# A true $null value:
$trueNull = $null
# An operation with no output returns
# the [System.Management.Automation.Internal.AutomationNull]::Value singleton,
# which is treated like $null in a scalar expression context,
# but behaves like an empty array in a pipeline or array expression context.
$automationNull = & {} # calling (&) an empty script block ({}) produces no output
# In a *scalar expression*, [System.Management.Automation.Internal.AutomationNull]::Value
# is implicitly converted to $null, which is why all of the following commands
# return $true.
$null -eq $automationNull
$trueNull -eq $automationNull
$null -eq [System.Management.Automation.Internal.AutomationNull]::Value
& { param($param) $null -eq $param } $automationNull
# By contrast, in a *pipeline*, $null and
# [System.Management.Automation.Internal.AutomationNull]::Value
# are NOT the same:
# Actual $null *is* sent as data through the pipeline:
# The (implied) -Process block executes once.
$trueNull | % { 'input received' } # -> 'input received'
# [System.Management.Automation.Internal.AutomationNull]::Value is *not* sent
# as data through the pipeline, it behaves like an empty array:
# The (implied) -Process block does *not* execute (but -Begin and -End blocks would).
$automationNull | % { 'input received' } # -> NO output; effectively like: #() | % { 'input received' }
# Similarly, in an *array expression* context
# [System.Management.Automation.Internal.AutomationNull]::Value also behaves
# like an empty array:
(#() + $automationNull).Count # -> 0 - contrast with (#() + $trueNull).Count, which returns 1.
# CAVEAT: Passing [System.Management.Automation.Internal.AutomationNull]::Value to
# *any parameter* converts it to actual $null, whether that parameter is an
# array parameter or not.
# Passing [System.Management.Automation.Internal.AutomationNull]::Value is equivalent
# to passing true $null or omitting the parameter (by contrast,
# passing #() would result in an actual, empty array instance).
& { param([object[]] $param)
[Object].GetMethod('ReferenceEquals').Invoke($null, #($null, $param))
} $automationNull # -> $true; would be the same with $trueNull or no argument at all.
The [System.Management.Automation.Internal.AutomationNull]::Value documentation states:
Any operation that returns no actual value should return AutomationNull.Value.
Any component that evaluates a Windows PowerShell expression should be prepared to deal with receiving and discarding this result. When received in an evaluation where a value is required, it should be replaced with null.
PSv2 vs. PSv3+, and general inconsistencies:
PSv2 offered no distinction between [System.Management.Automation.Internal.AutomationNull]::Value and $null for values stored in variables:
Using a no-output command directly in a foreach statement / pipeline did work as expected - nothing was sent through the pipeline / the foreach loop wasn't entered:
Get-ChildItem nosuchfiles* | ForEach-Object { 'hi' }
foreach ($f in (Get-ChildItem nosuchfiles*)) { 'hi' }
By contrast, if a no-output commands was saved in a variable or an explicit $null was used, the behavior was different:
# Store the output from a no-output command in a variable.
$result = Get-ChildItem nosuchfiles* # PSv2-: quiet conversion to $null happens here
# Enumerate the variable.
$result | ForEach-Object { 'hi1' }
foreach ($f in $result) { 'hi2' }
# Enumerate a $null literal.
$null | ForEach-Object { 'hi3' }
foreach ($f in $null) { 'hi4' }
PSv2: all of the above commands output a string starting with hi, because $null is sent through the pipeline / being enumerated by foreach:
Unlike in PSv3+, [System.Management.Automation.Internal.AutomationNull]::Value is converted to $null on assigning to a variable, and $null is always enumerated in PSv2.
PSv3+: The behavior changed in PSv3, both for better and worse:
Better: Nothing is sent through the pipeline for the commands that enumerate $result: The foreach loop is not entered, because the [System.Management.Automation.Internal.AutomationNull]::Value is preserved when assigning to a variable, unlike in PSv2.
Possibly Worse: foreach no longer enumerates $null (whether specified as a literal or stored in a variable), so that foreach ($f in $null) { 'hi4' } perhaps surprisingly produces no output.
On the plus side, the new behavior no longer enumerates uninitialized variables, which evaluate to $null (unless prevented altogether with Set-StrictMode).
Generally, however, not enumerating $null would have been more justified in PSv2, given its inability to store the null-collection value in a variable.
In summary, the PSv3+ behavior:
takes away the ability to distinguish between $null and [System.Management.Automation.Internal.AutomationNull]::Value in the context of a foreach statement
thereby introduces an inconsistency with pipeline behavior, where this distinction is respected.
For the sake of backward compatibility, the current behavior cannot be changed. This comment on GitHub proposes a way to resolve these inconsistencies for a (hypothetical) potential future PowerShell version that needn't be backward-compatible.
When you return a collection from a PowerShell function, by default PowerShell determines the data type of the return value as follows:
If the collection has more than one element, the return result is an array. Note that the data type of the return result is System.Array even if the object being returned is a collection of a different type.
If the collection has a single element, the return result is the value of that element, rather than a collection of one element, and the data type of the return result is the data type of that element.
If the collection is empty, the return result is $null
$l_t = #() assigns an empty array to $l_t.
$l_t2 = emptyArray assigns $null to $l_t2, because the function emptyArray returns an empty collection, and therefore the return result is $null.
$l_t2 and $l_t3 are both null, and they behave the same way. Since you've pre-declared $l_t as an empty array, when you add either $l_t2 or $l_t3 to it, either with the += operator or the addToArray function, an element whose value is **$null* is added to the array.
If you want to force the function to preserve the data type of the collection object you're returning, use the comma operator:
PS> function emptyArray {,#()}
PS> $l_t2 = emptyArray
PS> $l_t2.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS> $l_t2.Count
0
Note: The empty parentheses after emtpyArray in the function declaration is superfluous. You only need parentheses after the function name if you're using them to declare parameters.
An interesting point to be aware of is that the comma operator doesn't necessarily make the return value an array.
Recall that as I mentioned in the first bullet point, by default the data type of the return result of a collection with more than one element is System.Array regardless of the actual data type of the collection. For example:
PS> $list = New-Object -TypeName System.Collections.Generic.List[int]
PS> $list.Add(1)
PS> $list.Add(2)
PS> $list.Count
2
PS> $list.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
Note that the data type of this collection is List`1, not System.Array.
However, if you return it from a function, within the function the data type of $list is List`1, but it's returned as a System.Array containing the same elements.
PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return $list}
PS> $l = Get-List
PS> $l.Count
2
PS> $l.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
If you want the return result to be a collection of the same data type as the one within the function that you're returning, the comma operator will accomplish that:
PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return ,$list}
PS> $l = Get-List
PS> $l.Count
2
PS> $l.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
This isn't limited to array-like collection objects. As far as I've seen, any time PowerShell changes the data type of the object you're returning, and you want the return value to preserve the object's original data type, you can do that by preceding the object being returned with a comma. I first encountered this issue when writing a function that queried a database and returned a DataTable object. The return result was an array of hashtables instead of a DataTable. Changing return $my_datatable_object to return ,$my_datatable_object made the function return an actual DataTable object.