PowerShell string default parameter value does not work as expected - powershell

#Requires -Version 2.0
[CmdletBinding()]
Param(
[Parameter()] [string] $MyParam = $null
)
if($MyParam -eq $null) {
Write-Host 'works'
} else {
Write-Host 'does not work'
}
Outputs "does not work" => looks like strings are converted from null to empty string implicitly? Why? And how to test if a string is empty or really $null? This should be two different values!

Okay, found the answer # https://www.codykonior.com/2013/10/17/checking-for-null-in-powershell/
Assuming:
Param(
[string] $stringParam = $null
)
And the parameter was not specified (is using default value):
# will NOT work
if ($null -eq $stringParam)
{
}
# WILL work:
if ($stringParam -eq "" -and $stringParam -eq [String]::Empty)
{
}
Alternatively, you can specify a special null type:
Param(
[string] $stringParam = [System.Management.Automation.Language.NullString]::Value
)
In which case the $null -eq $stringParam will work as expected.
Weird!

You will need to use the AllowNull attribute if you want to allow $null for string parameters:
[CmdletBinding()]
Param (
[Parameter()]
[AllowNull()]
[string] $MyParam
)
And note that you should use $null on the left-hand side of the comparison:
if ($null -eq $MyParam)
if you want it to work predictably

seeing many equality comparisons with [String]::Empty, you could use the [String]::IsNullOrWhiteSpace or [String]::IsNullOrEmpty static methods, like the following:
param(
[string]$parameter = $null
)
# we know this is false
($null -eq $parameter)
[String]::IsNullOrWhiteSpace($parameter)
[String]::IsNullOrEmpty($parameter)
('' -eq $parameter)
("" -eq $parameter)
which yields:
PS C:\...> .\foo.ps1
False
True
True
True
True

Simply do not declare the param's type if you want a $null value to remain:
Param(
$stringParam
)
(None of the other solutions worked for me when declaring the type.)

So it seems a default value of $null for parameters of type [string] defaults to empty string, for whatever reason.
Option 1
if ($stringParam) { ... }
Option 2
if ($stringParam -eq "") { ... }
Option 3
if ($stringParam -eq [String]::Empty) { ... }

Related

How to provide custom type conversion of a PowerShell parameter

I have a scenario where one of the parameters to my cmdlet is a tuple of a CSV file name and the name of a column header. I would like to properly import that as a tuple with type [System.Tuple[System.IO.FileInfo,String]]. The full code looks like this:
[Parameter(Mandatory=$false)]
[ValidateScript( {
if (-Not ($_.item1 | Test-Path) ) {
throw "File or folder does not exist"
}
if (-Not ($_.item1 | Test-Path -PathType Leaf) ) {
throw "The Path argument must be a file. Folder paths are not allowed."
}
return $true
})]
[System.Tuple[System.IO.FileInfo,String]]$SomePath
However, when you provide an argument like -SomePath book1.csv,'Some Column Header' you get:
Invoke-MyCmdlet.ps1: Cannot process argument transformation on parameter 'SomePath'. Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Tuple`2[System.IO.FileInfo,System.String]".
Microsoft provides some documentation on this subject, but if I'm honest I'm struggling to wrap my head around what is going on in that example. There's a lot of syntax and commands there that I'm not familiar with and don't understand their relevance to the type conversion process.
My question is: is there a way to tell PowerShell how to perform the type conversion correctly? If so, what is the syntax for that? Is it documented more clearly elsewhere and I've missed it?
You could implement and register your own PSTypeConverter (here using PowerShell classes):
class CustomTupleConverter : System.Management.Automation.PSTypeConverter
{
[bool]
CanConvertFrom([object]$value, [type]$targetType)
{
# We only convert TO [Tuple[FileInfo,string]]
if($targetType -ne [System.Tuple[System.IO.FileInfo,string]]){
return $false
}
# ... and only from 2-item arrays consisting of #([string],[string]) or #([FileInfo],[string])
if($value -is [array] -and $value.Length -eq 2){
if(($value[0] -is [string] -or $value[0] -is [System.IO.FileInfo]) -and $value[1] -is [string]){
return $true
}
}
return $false
}
[object]
ConvertFrom([object]$value, [type]$targetType, [IFormatProvider]$format, [bool]$ignoreCase)
{
# Resolve individual values in the input array
$fileInfo = if($value[0] -is [System.IO.FileInfo]){
$value[0]
}
else{
Get-Item -Path $value[0]
}
if($fileInfo -isnot [System.IO.FileInfo]){
throw "Path didn't resolve to a file."
}
$headerName = $value[1] -as [string]
# Create corresponding tuple and return
return [System.Tuple]::Create($fileInfo, $headerName)
}
[bool]
CanConvertTo([object]$value, [type]$targetType){
return $this.CanConvertFrom($value, $targetType)
}
[object]
ConvertTo([object]$value, [type]$targetType, [IFormatProvider]$format, [bool]$ignoreCase){
return $this.ConvertFrom($value, $targetType, $format, $ignoreCase)
}
}
Once defined, we'll need to register it as a possible type converter for [System.Tuple[System.IO.FileInfo,string]]:
$targetTypeName = [System.Tuple[System.IO.FileInfo,string]].FullName
$typeConverter = [CustomTupleConverter]
Update-TypeData -TypeName $targetTypeName -TypeConverter $typeConverter
Now we can bind 2 strings to a tuple:
function Test-TupleBinding
{
param(
[Parameter(Mandatory = $true)]
[System.Tuple[System.IO.FileInfo,string]] $PathAndColumnName
)
$PathAndColumnName
}
Coversion magic in action during parameter binding:
PS C:\> Test-TupleBinding -PathAndColumnName windows\system32\notepad.exe,ColumnNameGoesHere
Item1 Item2 Length
----- ----- ------
C:\windows\system32\notepad.exe ColumnNameGoesHere 2

Powershell not passing Parameter to If Statement

I have this
Param (
[Parameter(Mandatory=$true)]
[ValidateSet('Yes','No')]
[string]$ContinueSetup,
[Parameter(Mandatory=$true)]
[Validateset('yes', 'no')]
[string]$InstallDropbox = 'Yes',
[Parameter(Mandatory=$true)]
[Validateset('yes', 'no')]
[string]$InstallSlack,
[Parameter(Mandatory=$true)]
[Validateset('yes', 'no')]
[string]$InstallOffice
)
if ($ContinueSetup -eq 'yes'){
if ($InstallDropbox = 'yes'){
write-host 'install dropbox'
}
else
{write-host 'dropbox not selected'}
if ($InstallSlack = 'yes'){
write-host 'install slack'
}
else
{write-host 'slack not selected'}
if ($InstallOffice = 'yes'){
write-host 'install office'
}
else
{write-host 'Office not selected'}
}
if ($continuesetup -eq 'no') {write-host 'no setup'; break}
It asks for my parameters as I want, but doesn't pass the parameters on, instead it just sets them all to 'yes'. Are parameters inherent? How should I set this so that it stops at each if statements, checks the parameter and does one of two actions, yes/no, and then moves onto the next if statement?
Comparison in PowerShell is done using -eq, not =.
Also, do not make parameters yes and no. There is already a built-in type that's immune to typos: bool. $true and $false are what you're after
As others have said, the bool type is designed for this sort of thing. Since they're only true or false, you don't have to include equality statements, which makes the code much more readable. Instead of
if( $InstallSlack -eq 'yes' ) # Note the -eq comparison is used in Powershell, not '=' or '=='
with a bool, you can just write
if( $InstallSlack )
which I think is much clearer.
To do the opposite, you put a -not in front of the bool:
if( -not $InstallSlack )
Here's your code rewritten with bool's (and with me changing the formatting a bit by adding some whitespace and aligning the code block indentation).
Param(
[Parameter(Mandatory=$true)]
[bool]$ContinueSetup,
[Parameter(Mandatory=$true)]
[bool]$InstallDropbox = 'Yes',
[Parameter(Mandatory=$true)]
[bool]$InstallSlack,
[Parameter(Mandatory=$true)]
[bool]$InstallOffice
)
if ($ContinueSetup){
if ($InstallDropbox){
write-host 'install dropbox'
}
else{
write-host 'dropbox not selected'
}
if ($InstallSlack){
write-host 'install slack'
}
else {
write-host 'slack not selected'
}
if ($InstallOffice){
write-host 'install office'
}
else {
write-host 'Office not selected'
}
}
else{
write-host 'no setup'
break
}
Bools are a very powerful programming concept. In Powershell, they can be combined/modified with -not, -or, -and, and a few others. This link has the details.
(Just for completeness, I'll note that some people (including me) don't like seeing [bool] parameters in Powershell. There's something called a [switch] parameter that can be used instead, but I wouldn't worry about it too much. Here's a link.)
I'd also recommend using [switch] instead of [bool] for your parameter types and not specifying $true as the default. SwitchParameter parameters make invocation simplier and more straightforward.
Param (
[switch]$Setup,
[switch]$NoDropboxInstall,
[switch]$NoSlackInstall,
[switch]$NoOfficeInstall
)
if ($Setup) {
"NoDropboxInstall: $NoDropboxInstall"
"NoSlackInstall: $NoSlackInstall"
"NoOfficeInstall: $NoOfficeInstall"
}
if (!$Setup) {
write-host 'no setup'
return
}
Invoking the script is simpler:
.\parameters.ps1 -Setup -NoDropboxInstall
Output:
NoDropboxInstall: True
NoSlackInstall: False
NoOfficeInstall: False
Yes, an assignment statement can be an expression. Occasionally useful, but confusing. Any non-null result will be true.
if ($a = 'yes') {'yes'}
yes
if ($a = 'no') {'yes'}
yes
if ($a = '') {'yes'}
# nothing

Storing value of a variable as a type

Alright - really nitpicky, but i HATE writing the same code twice (even if at this point it would have saved me more time)
So I'm writing a PowerCLI function that has 2 switch parameters to either find a poweredon or poweredoff event.
behold
function pGet-PowerEvent {
[cmdletbinding()]
PARAM (
[parameter(ValueFromPipeline=$true,
Mandatory=$true,
Position=0)]
[string[]]
$Entity,
[parameter(ValueFromPipeline=$true,
Mandatory=$false,
Position=1)]
[switch]
$onEvent,
[parameter(ValueFromPipeline=$true,
Mandatory=$false,
Position=2)]
[Switch]
$offEvent
)
if ($onEvent) {
$EventType = "VmPoweredOnEvent"
}
if ($offEvent) {
$EventType = "VmPoweredOffEvent"
}
$entity.ForEach{write-host $_; Get-VIEvent -Entity $_ -MaxSamples([int]::MaxValue) | ?{$_ -is [vmware.vim."$EventType"] |
select createdtime, username }
}
and running the command:
pGet-PowerEvent -Entity $vm -OnEvent
and the error:
Cannot convert the "[VMware.Vim.VmPoweredOffEvent]" value of type "System.String" to type "System.Type".
However when i run this command - it sees [vmware.vim.vmpoweredoffevent] as a string, rather than a type.
But i need the variable to be the type. And the value of the variable as the type.
Thanks
I believe you want either:
Where-Object { $_.GetType().FullName -eq "vmware.vim.$EventType" }
Or:
Where-Object { $_ -is ([type]"vmware.vim.$EventType") }
Or this may work better for you:
Where-Object { $_.GetType().Name -eq $EventType }
As an aside, you almost certainly don't want all three of your parameters to have ValueFromPipeline=$true.
If you run this:
Get-VM $VMName | pGet-PowerEvent
It's basically identical to running this:
$VM = Get-VM #VMParameters
pGet-PowerEvent -Entity $VM -onEvent:$VM -offEvent:$VM
You see how that doesn't make sense?
Additionally, it doesn't really make sense to assign Position or Mandatory=$false to switch parameters. There is no reason you'd ever want to do this:
pGet-PowerEvent $VI $true $false
Instead of this:
pGet-PowerEvent $VI -onEvent
It's just much more readable. If you need to call it dynamically, you would want to do something like this:
$OnEventSetting = (Get-Date).DayOfWeek -eq [DayOfWeek]::Monday
$OffEventSetting = (Get-Date).DayOfWeek -eq [DayOfWeek]::Tuesday
pGet-PowerEvent $VI -onEvent:$OnEventSetting -offEvent:$OffEventSetting

Inconsistent behavior in powershell with null parameters

I need to write a function in powershell that tells apart a 'parameter not being passed' from one passed with string empty (or any other string)
I wrote it like this:
function Set-X {
param(
[AllowNull()][string]$MyParam = [System.Management.Automation.Language.NullString]::Value
)
if ($null -ne $MyParam) { write-host 'oops' }
else { write-host 'ok' }
}
If I call Set-X without parameters from ISE, it works as I expect and prints 'ok'.
But if I do that from the normal console, it prints 'oops'.
What is going on? What is the proper way to do it?
Allowing the user to pass in a parameter argument value of $null does not change the fact that powershell will attempt to convert it to a [string].
Converting a $null value in powershell to a string results in an empty string:
$str = [string]$null
$null -eq $str # False
'' -eq $str # True
(same goes for $null -as [string] and "$null")
Remove the type constraint on the MyParam parameter if you not only want to allow $null but also accept $null as a parameter value:
function Set-X {
param(
[AllowNull()]$MyParam = [System.Management.Automation.Language.NullString]::Value
)
if ($null -ne $MyParam) { write-host 'oops' }
else { write-host 'ok' }
}
As Mathias and BenH have written, the culprit is casting $null to the [string] type, which results in an empty string:
[string]$null -eq '' #This is True
But for the sample code in Mathias answer to work correctly we also have to replace
[System.Management.Automation.Language.NullString]::Value
with $null
function Set-X {
param(
[AllowNull()]$MyParam = $null
)
if ($null -ne $MyParam) { write-host 'oops' }
else { write-host 'ok' }
}

In PowerShell, how can I test if a variable holds a numeric value?

In PowerShell, how can I test if a variable holds a numeric value?
Currently, I'm trying to do it like this, but it always seems to return false.
add-type -Language CSharpVersion3 #'
public class Helpers {
public static bool IsNumeric(object o) {
return o is byte || o is short || o is int || o is long
|| o is sbyte || o is ushort || o is uint || o is ulong
|| o is float || o is double || o is decimal
;
}
}
'#
filter isNumeric($InputObject) {
[Helpers]::IsNumeric($InputObject)
}
PS> 1 | isNumeric
False
You can check whether the variable is a number like this: $val -is [int]
This will work for numeric values, but not if the number is wrapped in quotes:
1 -is [int]
True
"1" -is [int]
False
If you are testing a string for a numeric value then you can use the a regular expression and the -match comparison. Otherwise Christian's answer is a good solution for type checking.
function Is-Numeric ($Value) {
return $Value -match "^[\d\.]+$"
}
Is-Numeric 1.23
True
Is-Numeric 123
True
Is-Numeric ""
False
Is-Numeric "asdf123"
False
Modify your filter like this:
filter isNumeric {
[Helpers]::IsNumeric($_)
}
function uses the $input variable to contain pipeline information whereas the filter uses the special variable $_ that contains the current pipeline object.
Edit:
For a powershell syntax way you can use just a filter (w/o add-type):
filter isNumeric($x) {
return $x -is [byte] -or $x -is [int16] -or $x -is [int32] -or $x -is [int64] `
-or $x -is [sbyte] -or $x -is [uint16] -or $x -is [uint32] -or $x -is [uint64] `
-or $x -is [float] -or $x -is [double] -or $x -is [decimal]
}
You can do something like :
$testvar -match '^[0-9]+$'
or
$testvar -match '^\d+$'
Returns True if $testvar is a number.
If you want to check if a string has a numeric value, use this code:
$a = "44.4"
$b = "ad"
$rtn = ""
[double]::TryParse($a,[ref]$rtn)
[double]::TryParse($b,[ref]$rtn)
Credits go here
PS> Add-Type -Assembly Microsoft.VisualBasic
PS> [Microsoft.VisualBasic.Information]::IsNumeric(1.5)
True
http://msdn.microsoft.com/en-us/library/microsoft.visualbasic.information.isnumeric.aspx
-is and -as operators requires a type you can compare against. If you're not sure what the type might be, try to evaluate the content (partial type list):
(Invoke-Expression '1.5').GetType().Name -match 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal'
Good or bad, it can work against hex values as well (Invoke-Expression '0xA' ...)
filter isNumeric {
$_ -is [ValueType]
}
-
1 -is [ValueType]
True
"1" -is [ValueType]
False
-
function isNumeric ($Value) {
return $Value -is [ValueType]
}
isNumeric 1.23
True
isNumeric 123
True
isNumeric ""
False
isNumeric "asdf123"
False
-
(Invoke-Expression '1.5') -is [ValueType]
$itisint=$true
try{
[int]$vartotest
}catch{
"error converting to int"
$itisint=$false
}
this is more universal, because this way you can test also strings (read from a file for example) if they represent number. The other solutions using -is [int] result in false if you would have "123" as string in a variable. This also works on machines with older powershell then 5.1
If you know the numeric type you want to test against (such as int for example in the code below), you can do it like this:
> [bool]("42" -as [int])
True
> [bool](42 -as [int])
True
> [bool]("hi" -as [int])
False
But note:
> [bool](42.1 -as [int])
True
Careful!:
It was pointed out that the code above fails to identify 0 as an int. You would need to add a guard for 0:
> $n -eq 0 -or $n -as [int]
Where $n is the object you are testing.
Thank you all who contributed to this thread and helped me figure out how to test for numeric values. I wanted to post my results for how to handle negative numbers, for those who may also find this thread when searching...
Note: My function requires a string to be passed, due to using Trim().
function IsNumeric($value) {
# This function will test if a string value is numeric
#
# Parameters::
#
# $value - String to test
#
return ($($value.Trim()) -match "^[-]?[0-9.]+$")
}
I ran into this topic while working on input validation with read-host. If I tried to specify the data type for the variable as part of the read-host command and the user entered something other than that data type then read-host would error out. This is how I got around that and ensured that the user enters the data type I wanted:
do
{
try
{
[int]$thing = read-host -prompt "Enter a number or else"
$GotANumber = $true
}
catch
{
$GotANumber = $false
}
}
until
($gotanumber)
"-123.456e-789" -match "^\-?(\d+\.?\d*)(e\-?\d+)?$|^0x[0-9a-f]+$"
or
"0xab789" -match "^\-?(\d+\.?\d*)(e\-?\d+)?$|^0x[0-9a-f]+$"
will check for numbers (integers, floats and hex).
Please note that this does not cover the case of commas/dots being used as separators for thousands.
Each numeric type has its own value. See TypeCode enum definition:
https://learn.microsoft.com/en-us/dotnet/api/system.typecode?view=netframework-4.8
Based on this info, all your numeric type-values are in the range from 5 to 15.
This means, you can write the condition-check like this:
$typeValue = $x.getTypeCode().value__
if ($typeValue -ge 5 -and $typeValue -le 15) {"x has a numeric type!"}
Testing if a value is numeric or a string representation of a numeric value.
function Test-Number
{
Param
(
[Parameter(Mandatory=$true,
Position=0)]
[ValidatePattern("^[\d\.]+$")]
$Number
)
$Number -is [ValueType] -or [Double]::TryParse($Number,[ref]$null)
}
Testing if a value is numeric.
function Test-Number
{
Param
(
[Parameter(Mandatory=$true,
Position=0)]
[ValidatePattern("^[\d\.]+$")]
$Number
)
$Number -is [ValueType]
}