Let's suppose I have a PowerShell function that returns a [string[]]:
function do_something() {
[string[]]$str_array = #("one", "two", "three")
return $str_array
}
$x = do_something()
# doesn't work
$y = #("something", $x)
Now I want to prepend a string to the front of the [string[]] list.
How would I go about doing that?
There are several ways to join two arrays. Regarding your question in the above comment:
That's interesting, I wasn't aware that a "," is different from a ";" when forming a list...
In PowerShell, the , is an own operator, where its behavior differs if used in a unary or binary statement.
From about_operators:
As a binary operator, the comma creates an array. As a unary operator, the comma creates an array with one member. Place the comma before the member.
In the code #("something", $x) you're using the operator in binary form. So a new array with two entries is created. The first entry is the string "something", the second entry contains an array of strings. Thats because PowerShell interprets it as: "Ok, I shall create a new array with two entries (="something", and a reference pointing to an array).
We can check that with the help or Get-Member:
C:\> $y | Get-Member
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Clone Method System.Object Clone(), System.Object ICloneable.Clone()
...
TypeName: System.Object[]
Name MemberType Definition
---- ---------- ----------
Add Method int IList.Add(System.Object value)
...
As can be seen, Get-Member shows use two TypeNames. The first is a string, the second is an array.
When changing the code to #("something"; $x) no ,-operator is used anymore. Instead, the code is now split up in about three "command", since the command-separator (=;) is used.
Cmd1: "something"
Cmd2: $x -> which dumps the array content
Cmd3: #() -> guarantees that the expressions inside the brackets are returned as an array.
You can also remove the command seperator and define $y over multiple lines:
PS C:\>$y = #("something"
>>> $x
>>> )
The be less ambiguous I'd recommend to merge two arrays in the following way:
PS C:\> $a = 1
PS C:\> $b = #(2,3,4)
PS C:\> $c = #()
PS C:\> $c += $a
PS C:\> $c += $b
PS C:\> $c
1
2
3
4
PS C:\> $c | gm
TypeName: System.Int32
Name MemberType Definition
...
Of course, it's "more" code, but its less ambiguous.
Hope that helps.
Related
I am using hashtables in powershell for multiple entries my hastable is working fine, but for the single entry its throwing the error by not picking the value it meant to be. Suppose I am having a Line
Aman;Indore;789858585;23
Raman;Delhi;785458545;35
for this two entry, my hashtable is working fine when i am giving command for example userinput.name[0] so it will pick Aman as the value and user.input.name[1] then it picks Raman as the value.
But the same code with single entry of
Aman;Indore;789858585;23
when I am giving userinput.name[0] so it is picking up A. The first letter of Aman it's picking instead of the complete name.
You might not even realise it, but you're using a PowerShell feature called Member Enumeration which was introduced in PowerShell 3.0.
Basically, if you attempt to access a named property on an array, if the property doesn't exist on the array object itself then PowerShell will look for that property on all the items in the array and return those values in a new array instead.
For example:
PS> $csv = #"
Aman;Indore;789858585;23
Raman;Delhi;785458545;35
"#
PS> $data = $csv | ConvertFrom-Csv -Delimiter ";" -Header #("Name", "Location", "Mobile", "Age");
PS> $data
Name Location Mobile Age
---- -------- ------ ---
Aman Indore 789858585 23
Raman Delhi 785458545 35
PS> $data.Name
Aman
Raman
# note this returns an array of objects thanks to member enumeration
PS> $data.Name.GetType().FullName
System.Object[]
PS> $data.Name[0]
Aman
In your case, $data.Name returns a new array containing the value of the Name property on all the items in $data - effectively #("Aman", "Raman"). So, when you use $data.Name[0], you're retrieving the first item in the array created by member enumeration - i.e. Aman - and all's well in the world...
Now, the catch is that if there's only one item in the array created by member enumeration it gets "unrolled" to be the value of the first item:
PS> $csv = #"
Aman;Indore;789858585;23
"#
PS> $data = $csv | ConvertFrom-Csv -Delimiter ";" -Header #("Name", "Location", "Mobile", "Age");
PS> $data
# Name Location Mobile Age
# ---- -------- ------ ---
# Aman Indore 789858585 23
# note this returns a single string because the "member enumeration" array
# gets unrolled if there's only one item
PS> $data.Name.GetType().FullName
System.String
PS> $data.Name
# Aman
PS> $data.Name[0]
# A
And in your case the $data.Name[0] is equivalent to "Aman"[0] which returns A.
To fix this, rather than inadvertently use member enumeration by doing $data.Name[0], where the result can vary based on the number of items in the array, you can use one of the following:
PS> $data[0].Name
Aman
PS> #($data.Name)[0]
Aman
The first option is probably more performant in the general case, but the second is a useful workaround in some cases where the first won't work (e.g. when dealing with return values from functions / where-object, etc).
I m trying to get the list of vm from nutanix with name, ipaddress,
The output I am recieving includes ipaddress with curly braces which gives output as System.String[]
I have taken all the values in a array by using a for loop, than have exported the values to csv
Script which i have written is as follows-
foreach ($vmachine in $vm){
$obj = "" | Select "vmName", "ipAddresses", "description", "protectionDomainName", "powerState"
$obj.vmName = $vmachine.vmName
$obj.ipAddresses = $vmachine.ipAddresses
$obj.description = $vmachine.description
$obj.protectionDomainName = $vmachine.protectionDomainName
$obj.powerState = $vmachine.powerState
$outArrayVM += $obj
$obj =$null
}
$outArrayVM | Export-Csv d:\z.csv
Expected output should be some ipaddress like 10.x.x.x, but m getting #{ipAddresses=System.String[]}
This happens because $vmachine.ipAddresses is a string array object. You want a string representation of that with controlled formatting. There are many ways to accomplish this. Here is one that will join multiple IPs (if they exist) using a ;. If there is only one IP, it will appear with no semi-colon:
$obj.ipAddresses = $vmachine.ipAddresses -join ";"
Here's an example of your scenario:
$ip = #("10.1.23.45")
$ip.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
$obj.name = "test"
$obj.ip = $ip
$obj
name ip
---- --
test {10.1.23.45}
$obj | convertto-csv
#TYPE Selected.System.String
"name","ip"
"test","System.Object[]"
Converting the ip property of $obj to string forces PowerShell to interpret the property as a string rather than a collection. Thus, the braces notation ({}) goes away.
$obj.ip = $ip -join ";"
$obj | convertto-csv
#TYPE Selected.System.String
"name","ip"
"test","10.1.23.45"
Here are some other alternatives to set the ip property value as a string:
$obj.ip = -join $ip # No join character here. Works best with only one IP.
$obj.ip = $ip[0] # Accesses first element of array $ip, which will be a string. Only works with one IP.
$obj.ip = [string]$ip # Uses string type accelerator to cast $ip as string. This will join multiple IPs with a space between each IP.
Explanation:
When a ConvertTo-Csv or Export-Csv is run, the input object property is converted using the ToString() method. If the reference type of that object property (System.Array in this case) does not have an override for the ToString() method, then that method will return the fully qualified type name of the property. In this instance, that FQTN is System.Object[]. This is predictable with a little digging.
Testing with [Int32], you would expect the string conversion to provide a string representation of the integer data because it does have an override:
$int = 1
$int.gettype().fullname
System.Int32
($int | Get-Member).where{$_.Name -eq "ToString"}
TypeName: System.Int32
Name MemberType Definition
---- ---------- ----------
ToString Method string ToString(), string ToString(string format), string ToString(System.IFormatProvider provid...
$int.ToString()
1
$int.ToString().gettype().fullname
System.String
Testing with [Array], you would not expect the string conversion to provide a string representation of the array data because it does not have an override:
$arr = [array]1
$arr.gettype().fullname
System.Object[]
([System.Object[]] | Get-Member -Static).where{$_.name -eq "ToString"}
$arr.toString()
System.Object[]
See Export-Csv and Object.ToString Method for supplemental explanations and examples.
I have a CSV file which contains multiline in some cells. I will use this data to compare the value got it from powershell.
This returns differences between the object, however the value is the same.
Expected Results should return nothing because both values are the same.
CSV content:
Value
System\CurrentControlSet\Control\ProductOptions
System\CurrentControlSet\Control\Server Applications
Software\Microsoft\Windows NT\CurrentVersion
Code:
PS> $data = Import-Csv .\tr.csv
PS> $data.Value
System\CurrentControlSet\Control\ProductOptions
System\CurrentControlSet\Control\Server Applications
Software\Microsoft\Windows NT\CurrentVersion
PS> $regval = ((Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control\SecurePipeServers\winreg\AllowedExactPaths).machine | Out-String).Trim()
PS> $regval
System\CurrentControlSet\Control\ProductOptions
System\CurrentControlSet\Control\Server Applications
Software\Microsoft\Windows NT\CurrentVersion
PS> Compare-Object $data.Value $regval
System\CurrentControlSet\Control\ProductOptions... =>
System\CurrentControlSet\Control\ProductOptions... <=
PS> $Tostring = ($data.Value | out-string).Trim()
PS> Compare-Object $Tostring $regval
InputObject SideIndicator
----------- -------------
System\CurrentControlSet\Control\ProductOptions... =>
System\CurrentControlSet\Control\ProductOptions... <=
PS> $Tostring.Length
145
PS> $regval.Length
147
This post no longer answers the OP's question directly but provides background information that is helpful for similar situations. This particular issue is solved by handling CR and LF characters before comparing the data. See Marked Answer for details.
Since $data in this case is an object with a property called value that holds your data, you need to compare what is stored in the value property to your $regval:
Compare-Object $data.value $regval
$regval is an array of strings before you pipe it to Out-String. After the pipe, it then becomes a string object. See below for type information before piping to Out-String.
$regval.gettype().fullname
System.String[]
$data is an array of objects (PSCustomObjects), which each have a property called Value that needs to be referenced directly if you want its data:
$data.gettype().fullname
System.Object[]
$data | Get-Member
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Value NoteProperty string Value=System\CurrentControlSet\Control\ProductOptions
($regval | Get-member).where({$_.MemberType -eq "Property"})
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Length Property int Length {get;}
In order to compare the data of two objects using Compare-Object, best results seem to come when the objects are collections of the same type. PowerShell will automatically do conversions in the background in some cases like Compare-Object "1" 1. Maybe that has something to do with value types as I am not entirely sure. I would do the comparison before converting any of your data to different types. Then if you reference the Value property of $data, this condition becomes true:
$data.value | Get-member -type Property
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Length Property int Length {get;}
You can reference MKlement0's Explanation for more information about how PowerShell handles the array type.
The likeliest explanation is that:
the multi-line value from your CSV (obtained from a single field) contains LF-only (Unix-style) newlines,
whereas the string derived form the registry values has CRLF (Windows-style) newlines, due to applying Out-String to an array of strings.
The most direct fix is to remove the CR chars. from $regval (you can use "`r" in PowerShell to generate a CR char):
# ...
# Remove all CRs from $regval.
# Note that not providing a replacement string (missing 2nd RHS operand)
# default to the empty string, which effectively *removes* what was matched.
$regval = $regval -replace "`r"
# Should now work as expected.
Compare-Object $data.Value $regval
That said:
Since you're comparing just two objects that are strings, you can avoid the overhead of Compare-Object and simply use -eq:
$data.Value -eq $regVal
Alternatively, you can split the multi-line values into arrays of lines and compare them individually; note that if you use regex "`r?`n" or ('\r?\n') to match newlines to split by - which matches both LF-only and CRLF newlines - you needn't remove CR chars. beforehand or even apply Out-String to the array output from the Get-ItemProperty HKLM:\... call to begin with; however, with the variable values from your question, you'd use:
# Newline-style-agnostic
Compare-Object ($data.Value -split "`r?`n") ($regval -split "`r?`n")
# Or, knowing that $data.Value has LF, and $regval CRLF
Compare-Object ($data.Value -split "`n") ($regval -split "`r`n")
# Or, by using the [string[]] array of registry values directly:
$regvals = (Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control\SecurePipeServers\winreg\AllowedExactPaths).machine
Compare-Object ($data.Value -split "`n") $regvals
As for what you tried:
$Tostring = ($data.Value | out-string).Trim()
If $data.Value is a single string that doesn't have a trailing newline - whether or not it has embedded newlines - the above is an effective no-op:
An input object that is already a string is passed through as-is by Out-String.
While Out-String does append a trailing CRLF newline (on Windows), the subsequent .Trim() call removes it again.
I'm confused what I'm doing wrong in ForEach method syntax of List?
PS D:\ntt> $nicInfo.IpConfigurations.Count
2
PS D:\ntt> $nicInfo.IpConfigurations[0]
PrivateIpAddressVersion Name Primary PrivateIpAddress PrivateIpAllocationMethod Subnet Name PublicIpAddress Name ProvisioningState
----------------------- ---- ------- ---------------- ------------------------- ----------- -------------------- -----------------
IPv4 ipconfig1 True 10.233.0.4 Dynamic Succeeded
PS D:\ntt> $nicInfo.IpConfigurations.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS D:\ntt> $nicInfo.IpConfigurations.ForEach({$_})
PS D:\ntt>
The problem is that PowerShell's own .ForEach() collection method is preempted by the List<T> type's own .ForEach() method in this case:
PowerShell's own .ForEach({ ... }):
defines $_ as the input object at hand for the script-block argument ({ ... })
passes any output produced inside the script block through (to PowerShell's success output stream).
By contrast, List<T>'s .ForEach({ ... }) converts the script block to an Action<T> delegate, which has the following implications:
The delegate doesn't know about $_ inside the script block and instead receives a single argument that must be accessed as $args[0].
Output from the script block is ignored, because an Action<T> delegate by definition has no return value.
While you can produce host (console) output with Write-Host from within the script block, such output cannot be used programmatically, because it bypasses PowerShell's output streams and can therefore neither be captured nor redirected.
Tip of the hat to PetSerAl for providing the crucial pointers in comments.
Workarounds:
If the script block you pass to .ForEach() need not produce any output, all that's needed is to use $args[0] in lieu of $_ in your script block, though you may still opt to use one of the other workarounds below in order to avoid confusion.
If output is needed, the simplest solution is to convert the List<T> instance to an array with .ToArray() first, on which .ForEach() works as expected; a simplified example:
$list = [System.Collections.Generic.List[object]] ('foo', 'bar')
$list.ToArray().ForEach({ $_ + '!' }) # Note the .ToArray() call.
The above produces 'foo!', 'bar!', as expected.
Alternatively, you may use:
a foreach loop to process the list items, which means you must pick an iteration variable name and refer to that instead of $_ in the loop body; e.g.:
foreach ($itm in $list) { $itm + '!' }
or ForEach-Object in a pipeline (slower, but doesn't require changing the script block), as shown in No Refunds No Returns' answer; e.g.:
$list | ForEach-Object { $_ + '!' }
Are you trying to do something with each item in the collection? Do you want to do something like this:
$nicInfo.IpConfigurations | ForEach-Object {
$ipConfiguration = $_
write-Output $ipConfiguration
# do more stuff with this $ipConfiguration
}
Just for your reference, you can use this code for list.ForEach().
$nicInfo.IpConfigurations.ForEach({write-host $args[0].ToString()})
And I test it myself, it works. Sample code as below:
$s=New-Object System.Collections.Generic.List[string]
$s.Add("hello_1")
$s.Add("hello_2")
$s.Add("hello_3")
$s.ForEach({write-host $args[0].ToString()})
Test result as below:
As well as I found this similar issue, #PetSerAl explained very well there.
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.