Powershell function argument default: Weirdness when it has a type constraint - powershell

If I have a function parameter WITHOUT the type constraint:
> function a ($s=$null) {if ($s -eq $null) {Write-Host "HI"} if ($s -eq "") {Write-Host "KK"}}
> a
HI
Now if I add the type constraint to it, the $null is interpreted differently:
> function a ([string]$s=$null) {if ($s -eq $null) {Write-Host "HI"} if ($s -eq "") {Write-Host "KK"}}
> a
KK
I can't find doc that explain this. It's also not consistent.

In your first example (function a), $s is equivalent to $null - it's truly null.
In your second example (function b), because you're casting $s to a [string] object, it's actually an empty String (equivalent to [String]::Empty), not $null.
You can check this by adding the following to each of your functions:
if($s -eq [String]::Empty){"empty!"};
Only b will print empty! - a will evaluate this to $false
Alternately, add this:
$s|get-member
a will actually throw an error - the same error you'll get if you run $null|get-member. b will show you that $s is a string and list all of the members of that class.

Related

Why does Powershell -replace change the type of an object

I'm a bit confused about some behavior I'm seeing.
The following code is designed to find and replace several strings in the registry.
$keys = #(gci -Path hkcu:\ -recurse -ErrorAction SilentlyContinue)
foreach ($key in $keys)
{
foreach ($vname in $key.GetValueNames())
{
$val = $key.GetValue($vname, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($val -like "c:\users\administrator*")
{
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
write-host "$key\$vname=$val -> $nval"
((Get-Item $Key.PSParentPath).OpenSubKey($Key.PSChildName, "True")).SetValue($vname, $nval, $key.GetValueKind($vname))
}
}
}
I continue to get the following error message when the registry value type is REG_MULTI_SZ. Exception calling "SetValue" with "3" argument(s): "The type of the value object did not match the specified RegistryValueKind or the object could not be properly converted."
If I comment out the -replace portion, i.e.:
$nval = $val #-replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
The registry keys are updated without errors (obviously with the same value they were).
So, something in the -replace operation is changing the data type. It appears to change in to type System.Object[].
The registryKey.SetValue method requires a string[] to set REG_MULTI_SZ.
Why is it changing the type? And how do I work around this behavior?
UPDATE:
Applying Option #1 in the answer did not work. $nval was still of type System.Object[] even after adding the cast. Applying option #3 did work.
Here is the final code that correctly searches and replaces a string found at the beginning of a string in the registry.
$keys = #(gci -Path hkcu:\ -recurse -ErrorAction SilentlyContinue)
foreach ($key in $keys)
{
foreach ($vname in $key.GetValueNames())
{
$val = $key.GetValue($vname, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($val -like "c:\users\administrator*")
{
if ($key.GetValueKind($vname) -eq [Microsoft.Win32.RegistryValueKind]::MultiString)
{ $nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as [string[]] }
else
{ $nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" }
write-host "$key\$vname=$val -> $nval"
((Get-Item $Key.PSParentPath).OpenSubKey($Key.PSChildName, "True")).SetValue($vname, $nval, $key.GetValueKind($vname))
}
}
}
Most things in PowerShell that return multiple objects return them as an [object[]]; most likely because you could return any number of different objects, and because PowerShell wraps most objects in [PSObject] whether you know it or not.
So although you may start with a [string[]], modifying it would result in an [object[]].
When you need an array of a specific type, the simple way is to just cast it.
You have 3 options for casting it: cast the value of the assignment or cast the variable, or use the -as operator.
Casting the value:
$nval = [string[]]($val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%")
Now $nval will be a [string[]] unless you re-assign something else to it later.
Casting the variable:
[string[]]$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
This works a little bit differently. Putting the cast on the variable in PowerShell applies that cast to all values assigned to it.
This is a good option when you know $nval always needs to be [string[]] and you don't want to cast it on every assignment.
-As operator:
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as [string[]]
This is similar to casting the value; the difference is that it doesn't throw an exception if the cast is unsuccessful, it just returns $null.
To deal with the different registry kinds and the casts needed, I recommended a switch statement in the comments, but I came up with a better idea: just use a hashtable:
$kindToType = #{
[Microsoft.Win32.RegistryValueKind]::MultiString = [string[]]
[Microsoft.Win32.RegistryValueKind]::ExpandString = [string]
[Microsoft.Win32.RegistryValueKind]::String = [string]
[Microsoft.Win32.RegistryValueKind]::DWord = [int]
# etc.
}
Then:
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as $kindToType[$key.GetValueKind($vname)]
This creates a lookup table where you can directly get a type from a registry kind.
To complement briantist's helpful answer with a focused summary:
If the LHS of a -replace expression is a collection[1]
, the replacement is performed on each of its elements.
The results are returned as a regular PowerShell array, which is of type [System.Object[]] - irrespective of the specific type of the input collection.
To preserve the input collection type, you must use an explicit cast or call an appropriate constructor - see briantist's answer.
Example:
# Apply -replace to a LHS of type [System.Collections.ArrayList]
$result = ([System.Collections.ArrayList] ('one', 'two')) -replace 'o', '#'
> $result
#ne
tw#
> $result.GetType().FullName # inspect the result's type
System.Object[]
[1] Without having looked at the source code, it seems that "collection" in this sense refers to any type that implements interface [System.Collections.IEnumerable] that doesn't also implement [System.Collections.IDictionary]; for a given instance $coll, you can test as follows:
$coll -is [System.Collections.IEnumerable] -and $coll -isnot [System.Collections.IDictionary].
Notably, this excludes hashtable-like collections, such as [hashtable] (literal syntax #{ ... }) and [System.Collections.Specialized.OrderedDictionary] (literal syntax [ordered] #{ ... }).

Printing useful variable values in powershell (in particular, involving $null/empty string)

If I have:
$a=$null
$b=''
$c=#($null,$null)
$d='foo'
write-host $a
write-host $b
write-host $c
write-host $d
the output is
foo
I'd really like to be able to easily get output that shows the variable values, e.g.,
$Null
''
#($Null,$Null)
'foo'
I can write a function to do this, but I'm guessing/hoping there's something built-in that I'm missing. Is there, or does everyone just roll their own function for something like this?
At the moment the quickest thing I've come up with is running a value through ConvertTo-Json before printing it. It doesn't handle a plain $null, but it shows me the other values nicely.
What you're looking for is similar to Ruby's .inspect method. It's something I always loved in Ruby and do miss in PowerShell/.Net.
Unfortunately there is no such thing to my knowledge, so you will somewhat have to roll your own.
The closest you get in .Net is the .ToString() method that, at a minimum, just displays the object type (it's inherited from [System.Object]).
So you're going to have to do some checking on your own. Let's talk about the edge case checks.
Arrays
You should check if you're dealing with an array first, because PowerShell often unrolls arrays and coalesces objects for you so if you start doing other checks you may not handle them correctly.
To check that you have an array:
$obj -is [array]
1 -is [array] # false
1,2,3 -is [array] # true
,1 -is [array] #true
In the case of an array, you'll have to iterate it if you want to properly serialize its elements as well. This is basically the part where your function will end up being recursive.
function Format-MyObject {
param(
$obj
)
if ($obj -is [array]) {
# initial array display, like "#(" or "["
foreach ($o in $obj) {
Format-MyObject $obj
}
# closing array display, like ")" or "]"
}
}
Nulls
Simply check if it's equal to $null:
$obj -eq $null
Strings
You can first test that you're dealing with a string by using -is [string].
For empty, you can compare the string to an empty string, or better, to [string]::Empty. You can also use the .IsNullOrEmpty() method, but only if you've already ruled out a null value (or checked that it is indeed a string):
if ($obj -is [string) {
# pick one
if ([string]::IsNullOrEmpty($obj)) {
# display empty string
}
if ($obj -eq [string]::Empty) {
# display empty string
}
if ($obj -eq "") { # this has no advantage over the previous test
# display empty string
}
}
Alternative
You could use the built-in XML serialization, then parse the XML to get the values out of it.
It's work (enough that I'm not going to do it in an SO answer), but it removes a lot of potential human error, and sort of future-proofs the approach.
The basic idea:
$serialized = [System.Management.Automation.PSSerializer]::Serialize($obj) -as [xml]
Now, use the built in XML methods to parse it and pull out what you need. You still need to convert some stuff to other stuff to display the way you want (like interpreting <nil /> and the list of types to properly display arrays and such), but I like leaving the actual serialization to an official component.
Quick example:
[System.Management.Automation.PSSerializer]::Serialize(#(
$null,
1,
'string',
#(
'start of nested array',
$null,
'2 empty strings next',
'',
([string]::Empty)
)
)
)
And the output:
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Object[]</T>
<T>System.Array</T>
<T>System.Object</T>
</TN>
<LST>
<Nil />
<I32>1</I32>
<S>string</S>
<Obj RefId="1">
<TNRef RefId="0" />
<LST>
<S>start of nested array</S>
<Nil />
<S>2 empty strings next</S>
<S></S>
<S></S>
</LST>
</Obj>
</LST>
</Obj>
</Objs>
I shared two functions that reveal PowerShell values (including the empty $Null's, empty arrays etc.) further than the usually do:
One that the serializes the PowerShell objects to a PowerShell
Object Notation (PSON)
which ultimate goal is to be able to reverse everything with the
standard command Invoke-Expression and parse it back to a
PowerShell object.
The other is the ConvertTo-Text (alias CText) function that I used in
my Log-Entry
framework. note the
specific line: Log "Several examples that usually aren't displayed
by Write-Host:" $NotSet #() #(#()) #(#(), #()) #($Null) that I wrote
in the example.
Function Global:ConvertTo-Text1([Alias("Value")]$O, [Int]$Depth = 9, [Switch]$Type, [Switch]$Expand, [Int]$Strip = -1, [String]$Prefix, [Int]$i) {
Function Iterate($Value, [String]$Prefix, [Int]$i = $i + 1) {ConvertTo-Text $Value -Depth:$Depth -Strip:$Strip -Type:$Type -Expand:$Expand -Prefix:$Prefix -i:$i}
$NewLine, $Space = If ($Expand) {"`r`n", ("`t" * $i)} Else{"", ""}
If ($O -eq $Null) {$V = '$Null'} Else {
$V = If ($O -is "Boolean") {"`$$O"}
ElseIf ($O -is "String") {If ($Strip -ge 0) {'"' + (($O -Replace "[\s]+", " ") -Replace "(?<=[\s\S]{$Strip})[\s\S]+", "...") + '"'} Else {"""$O"""}}
ElseIf ($O -is "DateTime") {$O.ToString("yyyy-MM-dd HH:mm:ss")}
ElseIf ($O -is "ValueType" -or ($O.Value.GetTypeCode -and $O.ToString.OverloadDefinitions)) {$O.ToString()}
ElseIf ($O -is "Xml") {(#(Select-XML -XML $O *) -Join "$NewLine$Space") + $NewLine}
ElseIf ($i -gt $Depth) {$Type = $True; "..."}
ElseIf ($O -is "Array") {"#(", #(&{For ($_ = 0; $_ -lt $O.Count; $_++) {Iterate $O[$_]}}), ")"}
ElseIf ($O.GetEnumerator.OverloadDefinitions) {"#{", (#(ForEach($_ in $O.Keys) {Iterate $O.$_ "$_ = "}) -Join "; "), "}"}
ElseIf ($O.PSObject.Properties -and !$O.value.GetTypeCode) {"{", (#(ForEach($_ in $O.PSObject.Properties | Select -Exp Name) {Iterate $O.$_ "$_`: "}) -Join "; "), "}"}
Else {$Type = $True; "?"}}
If ($Type) {$Prefix += "[" + $(Try {$O.GetType()} Catch {$Error.Remove($Error[0]); "$Var.PSTypeNames[0]"}).ToString().Split(".")[-1] + "]"}
"$Space$Prefix" + $(If ($V -is "Array") {$V[0] + $(If ($V[1]) {$NewLine + ($V[1] -Join ", $NewLine") + "$NewLine$Space"} Else {""}) + $V[2]} Else {$V})
}; Set-Alias CText ConvertTo-Text -Scope:Global -Description "Convert value to readable text"
ConvertTo-Text
The ConvertTo-Text function (Alias CText) recursively converts PowerShell object to readable text this includes hash tables, custom objects and revealing type details (like $Null vs an empty string).
Syntax
ConvertTo-Text [<Object>] [[-Depth] <int>] [[-Strip] <int>] <string>] [-Expand] [-Type]
Parameters
<Object>
The object (position 0) that should be converted a readable value.
-⁠Depth <int>
The maximal number of recursive iterations to reveal embedded objects.
The default depth for ConvertTo-Text is 9.
-Strip <int>
Truncates strings at the given length and removes redundant white space characters if the value supplied is equal or larger than 0. Set -Strip -1 prevents truncating and the removal of with space characters.
The default value for ConvertTo-Text is -1.
-Expand
Expands embedded objects over multiple lines for better readability.
-Type
Explicitly reveals the type of the object by adding [<Type>] in front of the objects.
Note: the parameter $Prefix is for internal use.
Examples
The following command returns a string that describes the object contained by the $var variable:
ConvertTo-Text $Var
The following command returns a string containing the hash table as shown in the example (rather then System.Collections.DictionaryEntry...):
ConvertTo-Text #{one = 1; two = 2; three = 3}
The following command reveals values (as e.g. $Null) that are usually not displayed by PowerShell:
ConvertTo-Text #{Null = $Null; EmptyString = ""; EmptyArray = #(); ArrayWithNull = #($Null); DoubleEmptyArray = #(#(), #())} -Expand
The following command returns a string revealing the WinNT User object up to a level of 5 deep and expands the embedded object over multiple lines:
ConvertTo-Text ([ADSI]"WinNT://./$Env:Username") -Depth 5 -Expand
A quick self-rolled option good for some datatypes.
function Format-MyObject {
param(
$obj
)
#equality comparison order is important due to array -eq overloading
if ($null -eq $obj)
{
return 'null'
}
#Specify depth because the default is 2, because powershell
return ConvertTo-Json -Depth 100 $obj
}

How do I assign a null value to a variable in PowerShell?

I want to assign a null value to a variable called $dec, but it gives me errors. Here is my code:
import-module activedirectory
$domain = "domain.example.com"
$dec = null
Get-ADComputer -Filter {Description -eq $dec}
These are automatic variables, like $null, $true, $false etc.
about_Automatic_Variables, see https://technet.microsoft.com/en-us/library/hh847768.aspx?f=255&MSPPError=-2147217396
$NULL
$null is an automatic variable that contains a NULL or empty
value. You can use this variable to represent an absent or undefined
value in commands and scripts.
Windows PowerShell treats $null as an object with a value, that is, as
an explicit placeholder, so you can use $null to represent an empty
value in a series of values.
For example, when $null is included in a collection, it is counted as
one of the objects.
C:\PS> $a = ".dir", $null, ".pdf"
C:\PS> $a.count
3
If you pipe the $null variable to the ForEach-Object cmdlet, it
generates a value for $null, just as it does for the other objects.
PS C:\ps-test> ".dir", $null, ".pdf" | Foreach {"Hello"}
Hello
Hello
Hello
As a result, you cannot use $null to mean "no parameter value." A
parameter value of $null overrides the default parameter value.
However, because Windows PowerShell treats the $null variable as a
placeholder, you can use it scripts like the following one, which
would not work if $null were ignored.
$calendar = #($null, $null, “Meeting”, $null, $null, “Team Lunch”, $null)
$days = Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
$currentDay = 0
foreach($day in $calendar)
{
if($day –ne $null)
{
"Appointment on $($days[$currentDay]): $day"
}
$currentDay++
}
output:
Appointment on Tuesday: Meeting
Appointment on Friday: Team lunch
Use $dec = $null
From the documentation:
$null is an automatic variable that contains a NULL or empty value. You can use this variable to represent an absent or undefined value in commands and scripts.
PowerShell treats $null as an object with a value, that is, as an explicit placeholder, so you can use $null to represent an empty value in a series of values.
If the goal simply is to list all computer objects with an empty description attribute try this
import-module activedirectory
$domain = "domain.example.com"
Get-ADComputer -Filter '*' -Properties Description | where { $_.Description -eq $null }
As others have said, use $null.
However, the handling of $null is not so simple.
In lists (or, more precisely, System.Array objects) $null is treated as a placeholding object when indexing the list, so ($null, $null).count outputs 2.
But otherwise $null is treated as a flag signifying that there is no content (no object; or, more precisely, a "null-valued expression", as reported by .GetType()), so ($null).count outputs 0.
Thus
$null.count; # Output = 0
($null).count; # Output = 0
(, $null).count; # Output = 1
($null, $null).count; # Output = 2
($null, $null, $null).count; # Output = 3
Note: the same output is returned from .count and .length in the above context.
Similarly if explicitly assigning any of the above to a variable, as in
$aaa = $null; $aaa.count
$bbb = ($null, $null); $bbb.count
which output, respectively, 0 and 2.
Similarly if looping with ForEach, as in
$aaa = $null; ForEach ($a in $aaa) {write-host "Foo" -NoNewLine}
$bbb = ($null, $null); ForEach ($b in $bbb) {write-host "Bar" -NoNewLine}
which output, respectively, nothing and BarBar.
However, note well that when operating on an individual item that has been returned from a list $null is again treated as a "null-valued expression", as can be confirmed by running
$xxx = ($null, "foo", $null); ForEach ($x in $xxx) {write-host "C=" $x.count "| " -NoNewLine}
which outputs C= 0 | C= 1 | C= 0 | .

powershell logical operators and $Error variable

Recently I had to check whether some errors occurred when my script is executed. First I've tried to check whether $Error is $null. The strange thing for me was that I haven't got any result from (neither True, nor False). Then i've wrote:
if (($error -eq $null) -or ($error -ne $null)) {Write-Host "NULL"}
And nothing was in the output. This made me very confused. I've found that such thing happens for all variables which are of System.Collections.ArrayList type.
Maybe someone knows the explanation why this happens, because for me this looks like a bug?
Version of Powershell, on which I found this, is 3.0.
#mjolinor's answer tries to explain it, but is incomplete.
When you do (1,2,3) -eq 1, you get back 1. In this case what -eq does with an array is to return the element that is equal to the RHS, and nothing if no match occurs.
On the other hand, if you do 1 -eq (1,2,3), you get False, because the above occurs only when the array is the LHS. So it is not true that the -eq operator always does behaves like the above case when it comes to arrays.
Now, coming on to the -ne usage. When you do (1,2,3) -ne 1, you get the array 2,3. That is, it returns the elements that are not equal to the RHS. And similar to -eq, 1 -ne (1,2,3), will return True
Coming to your condition - ($error -eq $null) -or ($error -ne $null)
When $error is empty, $error -eq $null will return nothing ( and is hence False in a bool statement). This is of course because there is no element matching $null in $error. Also, $error -ne $null will also return nothing ( and hence is False in a bool statement) because $error is empty and there is no element in it that is not $null.
Hence, when $error is empty, your statement will be false and the block inside if will not be executed.
If $error were not empty, either of the condition would have been true, and you would have seen the write-hostexecuted.
So how do you really solve this problem?
The straightforward way is to check the length of the $error array:
if($error.length -gt 0){
write-host "error occured"
}
Also, read this article that talks about various error handling strategies - http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/12/powershell-error-handling-and-why-you-should-care.aspx
When the -eq operator is used against an array (or arraylist), it returns all the members of the array that satisfiy the condition.
($error -eq $null) says "I want all the members of the $error arraylist that are nulls." It can't return anything but $null.
When you use it in an IF, the result is going to be cast as [bool]. $Null evaluates to $false when cast as [bool].
($error -eq $null) can never be True.
$x = new-object collections.arraylist
[void]$x.Add('a')
[void]$x.add('b')
($x -eq $null).count
[bool]($x -eq $null)
[void]$x.Add($Null)
($x -eq $null).count
[bool]($x -eq $null)
0
False
1
False

The PowerShell -and conditional operator

Either I do not understand the documentation on MSDN or the documentation is incorrect.
if($user_sam -ne "" -and $user_case -ne "")
{
Write-Host "Waaay! Both vars have values!"
}
else
{
Write-Host "One or both of the vars are empty!"
}
I hope you understand what I am attempting to output. I want to populate $user_sam and $user_case in order to access the first statement!
You can simplify it to
if ($user_sam -and $user_case) {
...
}
because empty strings coerce to $false (and so does $null, for that matter).
Another option:
if( ![string]::IsNullOrEmpty($user_sam) -and ![string]::IsNullOrEmpty($user_case) )
{
...
}
Try like this:
if($user_sam -ne $NULL -and $user_case -ne $NULL)
Empty variables are $null and then different from "" ([string]::empty).
The code that you have shown will do what you want iff those properties equal "" when they are not filled in. If they equal $null when not filled in for example, then they will not equal "". Here is an example to prove the point that what you have will work for "":
$foo = 1
$bar = 1
$foo -eq 1 -and $bar -eq 1
True
$foo -eq 1 -and $bar -eq 2
False