Powershell's -split behaves differently depending on method parameters - powershell

If you split a string, you get a string[].
$foo = "apple,banana,coconut"
Write-Output $foo.GetType()
$foo = $foo -split ","
Write-Output $foo.GetType()
If you split a paramaterized string, you get a string.
Function Get-Foo() {
param (
[string] $foo
)
Write-Output $foo.GetType()
$foo = $foo -split ","
Write-Output $foo.GetType()
}
Why?
I researched this and didn't turn anything up; Powershell's documentation on split was largely unhelpful.

The difference is that in your first example, you never declared the variable, so you can assign it values of any type and it will accept them without coercion, altering its type to match:
PS > $foo = "apple,banana,coconut"
PS > Write-Output $foo.GetType().Name
String
PS > $foo = $foo -split ","
PS > Write-Output $foo.GetType().Name
String[]
PS > $foo = 1
PS > Write-Output $foo.GetType().Name
Int32
But once you've explicitly declared $foo's type with [string], $foo.GetType() will continue to report string, and anything you assign to it will be coerced to string. You can't change the type of a declared variable just by assigning it a new value:
PS > [string] $foo = "apple,banana,coconut"
PS > Write-Output $foo.GetType().Name
String
PS > $foo = $foo -split ","
PS > Write-Output $foo.GetType().Name
String
PS > $foo = 1
PS > Write-Output $foo.GetType().Name
String
You can change the type by re-declaring it, though. Even within a function:
PS > [Int32] $foo = $foo
PS > Write-Output $foo.GetType().Name
Int32
Note that such a redeclaration assignment will fail if the value is not interpretable as the new type:
PS > [string] $foo = "apple,banana,coconut"
PS > [Int32] $foo = $foo
Cannot convert value "apple,banana,coconut" to type "System.Int32". Error: "Input string was not in a correct format."
As far as I know, there's no way to "un-declare" a variable's type so it goes back to accepting any type of value, short of removing the variable entirely with Remove-Variable.

Related

Converting empty string to $null back to the same variable

We have a few imports via PowerShell in to Active Directory that have a couple fields that come across as an empty string from the datasource, but need to be set as $null within Active Directory.
Since there are quite a few of these fields, I attempted to create a function that will convert an empty string to $null.
The trouble is that if I set the variable back to itself, it remains an empty string. If I set it as a new variable, it works fine.
function Get-ValueOrNull
{
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[string]$Value
)
if ([string]::IsNullOrEmpty($Value))
{
return $null
}
return [string]$Value
}
function Test-Function
{
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[string]$TestValue
)
$TestValue = Get-ValueOrNull -Value $TestValue
$TestValue2 = Get-ValueOrNull -Value $TestValue
Write-Host "TestValue: $($TestValue -eq $null)"
Write-Host "TestValue2: $($TestValue2 -eq $null)"
}
Test-Function -TestValue ""
Here the output is
PS C:\> .\Test-Function.ps1
TestValue: False
TestValue2: True
This is clearly something I'm not understanding about Types in PowerShell function parameters. I can change the [string]$TestValue to $TestValue, and it will work.
function Test-Function
{
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
$TestValue
)
...
}
...
Output:
PS C:\> .\Test-Function.ps1
TestValue: True
TestValue2: True
The reason I'd like to preserve the [string] parameter type is to enforce that it should be a string or an empty string. Can someone explain what is going on here?
Once you've casted the variable as opposed to the value being assigned you are strictly typing that variable.
This is easier to see with an [int] because basically anything can be casted to a [string] successfully:
$v = [int]'5'
$v.GetType() # int
$v = 'hello'
$v.GetType() # string
[int]$v = '5'
$v.GetType() # int
$v = 'hello'
# Exception:
# Cannot convert value "hello" to type "System.Int32". Error: "Input string was not in a correct format."
When you type a parameter, the variable that contains the parameter is the same way; you can reassign it, but the right side must be assignable / castabale / convertible to the left side's type.
A $null cast as [string] is an empty string:
([string]$null) -eq ([string]::Empty) # True
You can still strongly type your parameter, if you use a different intermediate variable in the function that isn't, as you demonstrated with $TestValue2.

Object variable changes to a string

I have a variable set as an object as follows:
[object] $x = 'abc','def';
If I view what $x is now, I get:
acb
def
Now my problem is when I set $x to $null and then try to rather set $x from a loop using += after reading the file it change $x type to a string and if I view what $x is now it gives me:
abcdef
instead of:
abc
def
How do I go about it to keep the variable as an object rather then a string?
Below is just a sample to get the idea:
[object] $x = 'abc','def';
$x = $null;
for ($i = 0; $i -lt $numberOfColumns; $i++) {
$x += '$_.' + $NamesOfColumns[$i] + '.Trim()';
}
What you do within your code is:
[object] $x = 'abc', 'def'
==> The type of 'abc' and 'def' is [System.String]. Because you comma seperated them PowerShell does automatically create a list. So after executing that line $x is a System.Object[]. Index 0 and 1 contains [System.String].
$x = $null;
==> Now you define $null as the value for $x. So you are removing the value. The type of $x is now undefinded. You can set the value 123 then $x will become type System.Int32. You can redefine a string and so on.
Within your for-loop you use
$x += 'somestring' + $addingSomeStuff + 'otherstring'
==> The result here is that within the first Iteration of the for-loop PowerShell will assign a String to $x. So the type of $x will be [System.String]. In the next iterations the += operator adds additionally content to the value of $x, which is still [System.String]
Don't set $x to $null. Because you'll loose the type information. For more information read about the PowerShell Extended Type System.
The following snippet works. Hope that helps.
############################################
# The following was not part of your post #
# I added it to get it run in general #
$numberOfColumns = 2
$NamesOfColumns = 'Column1', 'Column2'
############################################
[object] $x = 'abc','def';
# don't set $x to $null
# define an empty list instead ;-)
$x = #()
for ($i = 0; $i -lt $numberOfColumns; $i++) {
$x += '$_.' + $NamesOfColumns[$i] + '.Trim()';
}
In the underlying type system (.NET CTS), [object] is the base class for any object, and is the most vague type description you can give any variable - it doesn't convey any specialized meaning at all.
As mentioned in #PatM0's answer, the best solution here is to initialize the variable value with the array subexpression operator (#()) before using += :
$x = #()
If you really want to force a variable to be a collection type, use the array or psobject[] type accelerators:
PS C:\> [array]$x = 'abc','def'
PS C:\> $x = $null
PS C:\> $x += 'abc'
PS C:\> $x += 'def'
PS C:\> $x
abc
def
PS C:\> [psobject[]]$x = 'abc','def'
PS C:\> $x = $null
PS C:\> $x += 'abc'
PS C:\> $x += 'def'
PS C:\> $x
abc
def
Compare with [object]:
PS C:\> [object]$x = 'abc','def'
PS C:\> $x = $null
PS C:\> $x += 'abc'
PS C:\> $x += 'def'
PS C:\> $x
abcdef

When does Powershell honour default values when using $null splat parameters?

Consider the following function:
function f1{
param(
$sb = {},
$s = ''
)
if ($sb -isnot [scriptblock]) { 'scriptblock' }
if ($s -isnot [string] ) { 'string' }
}
Now invoke it with a splat parameter:
PS C:\> $splat = #{foo='bar'}
PS C:\> f1 #splat
As expected, nothing is returned. Now try it again with a $null splat parameter:
PS C:\> $splat = $null
PS C:\> f1 #splat
scriptblock
Oddly, scriptblock is returned. Clearly, at least for the [scriptblock] parameter, powershell is not honoring the default value when a $null splat parameter is used. But powershell does honor the default value for the [string]. What is going on here?
For what types does Powershell honour default values when using $null splat parameters?
Isn't this just normal application of positional parameters? You are splatting a single $null which is being applied to $sb.
Compare:
> function f{ param($sb = {}, $s = '') $PSBoundParameters }
> $splat = #(1,2)
> f #splat
Key Value
--- -----
sb 1
s 2
> f #flkejlkfja
Key Value
--- -----
sb
> function f{ param($aaa = 5, $sb = {}, $s = '') $PSBoundParameters }
> f #splat
Key Value
--- -----
aaa 1
sb 2
It's an old question but if it is still interesting...
As others have written with $splat = $null calling f1 #splat the first parameters will get the value $null instead it's default value.
If you want the parameters use their default value in this case you have to use $splat = #{} or $splat = #().
Here's a demonstration to help understand what's happening
$splat = #{foo='bar'}
"$(&{$args}#splat)"
-foo: bar
When you splat the hash table, it gets converted to -Key: Value string pairs that become the parameters to your function.
Now try:
$splat = $null
"$(&{$args}#splat)"
Nothing is returned. There are no keys to generate the parameter string from, so the end result is the same as not passing any parameters at all.
To complement Etan Reisner's helpful answer with a more direct demonstration that splatting $null indeed passes $null as the first (and only) positional argument:
$splat = $null
& { [CmdletBinding(PositionalBinding=$False)] param($dummy) } #splat
The above yields the following error:
A positional parameter cannot be found that accepts argument '$null'.
...
Decorating the param() block with [CmdletBinding(PositionalBinding=$False)] ensures that only named parameter values can be passed, causing the positional passing of $null from splatting to trigger the error above.
Note that using the special "null collection" value ([System.Management.Automation.Internal.AutomationNull]::Value) that you get from commands that produce no output for splatting is effectively the same as splatting $null, because that "null collection" value is converted to $null during parameter binding.
VargaJoe's helpful answer explains how to construct a variable for splatting so that no arguments are passed, so that the callee's default parameter values are honored.

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 | .

How can I alternate/switch parts of a string in PowerShell without using an intermediate value?

I'm trying to alternate a setting in a config file using PowerShell. For example, if a certain value is true, I'd like to switch it to false. If it's false, I'd like to switch it to true. I'd also like to change a path from \\servername\folder\ to \\servername\. Is there a way I can perform this in PowerShell without using an intermediate value?
If I do this:
$foo = 'aaa'
$foo -replace 'aaa', 'bbb' -replace 'bbb', 'aaa'
$foo will always be 'aaa'. I realize I could make an intermittent change ('aaa' becomes 'ccc' and then changes to 'bbb') but that's messy to read.
How can I alternate values without using an intermediate value?
I'm not quite sure what you're trying to achieve. Do you want to switch two values without a buffer variable? Or do you want to toggle a value between two states? The latter can be achieved like this:
function Toggle($s, $v1, $v2) {
$e1 = [regex]::Escape($v1)
$e2 = [regex]::Escape($v2)
$r = $s
if ($s -match $e1) {
$r = $s -replace $e1, $v2
} elseif ($s -match $e2) {
$r = $s -replace $e2, $v1
}
return ($r)
}
$foo = "..."
Toggle $foo "\\servername\folder\" "\\servername\"
Using a match evaluator. Not sure if this is any better or not.
Begin{
[regex]$ValueRegex = 'aaa|bbb'
$ValueToggles = #{
aaa='bbb'
bbb='aaa'
}
$toggleValue = {$ValueToggles[$args[0].groups[0].value]}
}
Process{
$foo = 'Value: aaa'
$ValueRegex.replace($foo,$toggleValue)
}
Value: bbb
You can alternate a boolean value ($true or $false) simply by using the -not operator; e.g.:
PS C:\> $value = $true
PS C:\> $value = -not $value
PS C:\> $value
False
Regular expression replacement:
PS C:\> '\\servername\folder\' -replace '^(\\\\[^\\]+)\\[^\\]+', '$1'
\\servername\
Bill