Strange behaviour of string comparison in Powershell - powershell

Consider the following function :
function myfunc()
{
if (condition1) {
return 'nowork'
} elseif (condition2) {
return $false
} elseif (condition3) {
return $true
}
Now if I call this function, and I know that condition3 is true, I can see that True is returned:
...
$result = myfunc
Write-Host $result
(this writes True to the console.)
The next statement in the calling function is an if statement to determine what was returned, and act upon that:
$result = myfunc
Write-Host $result
if ($result -eq 'nowork') {
do this..
} elseif ($result -eq $false) {
do that..
} elseif ($result -eq $true) {
do something else..
}
And this is where it gets strange (to me). Even though I can see that True is returned, the if statement decides to go do 'do this..', the first branch of the if statement, where I would have expected that 'do something else..' would have been done.
Another strange thing is that it sometimes works, sometimes not. I tried changing the if statement to:
if ('nowork' -eq $result)
and then what went wrong first now worked, but later on the same issue re-appeared.
I'm guessing there's something wrong with my first string comparison, but I can't figure out what. I'm used to writing scripts in Linux (bash), so Powershell must be acting differently.
Btw: script is run in Debian 10, Powershell 7, but the exact same problem also appears on a Windows machine with Powershell 5.0.
Please help..

You're comparing apples and oranges
PowerShell's comparison operator behavior depends on type of the left-hand side operand.
When your lhs ($result) is a [bool] (ie. $true or $false), PowerShell will attempt to convert the right-hand side operand to [bool] as well before comparing the two.
Converting a non-empty string (ie. 'nowork') to [bool] results in $true, so the if condition evaluates to $true -eq $true -> $true.
You can fix this by manually type checking:
if($result -is [bool]){
if($result){
# was $true
}
else {
# was $false
}
}
elseif($result -eq 'nowork'){
# was 'nowork'
}
The nicer way of solving this however would be to always return the same type of object. In your case where you have 3 different return options, consider an enum:
enum WorkAmount
{
None
Some
All
}
function myfunc()
{
if (condition1) {
return [WorkAmount]::None
} elseif (condition2) {
return [WorkAmount]::Some
} elseif (condition3) {
return [WorkAmount]::All
}
}

Related

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' }
}

Powershell function being passed too many parameters

I'm writing a set of PowerShell scripts to monitor the size of various folders. I've run into an error, and I've got no idea what's causing it.
Here is the code, with Write-Host showing what I am expecting and what the variables $ip and $loc actually contain:
function getDriveLetter($ip) {
Write-Host $ip # prints: 192.168.10.10 myfolder1\myfolder2\
# expected: 192.168.10.10
switch($ip) {
"192.168.10.10" {return "E`$"; break}
"192.168.10.20" {return "D`$"; break}
default {"Unknown"; break}
}
}
function getFullPath($loc,$folder) {
Write-Host $loc # prints: 192.168.10.10 myfolder1\myfolder2\
# expected: 192.168.10.10
$drive = getDriveLetter("$loc")
$str = "\\$loc\$drive\DATA\$folder"
return $str
}
function testPath($loc,$folder) {
$mypath = getFullPath("$loc","$folder")
if (Test-Path $mypath) {
return $true
} else {
return $false
}
}
When I run the command:
testPath("192.168.10.10","myfolder1\myfolder2\")
I'm getting a "False" result, but if I run:
Test-Path "\\192.168.10.10\E`$\DATA\myfolder1\myfolder2\"
The command returns True (as it should).
What have I missed? I've tried forcing the variables to be set with:
$mypath = getFullPath -loc "$loc" -folder "$folder"
but there's no change. If it changes anything, this is on Powershell version 4.
I'd suggest that you review the syntax of PowerShell a bit more, because there's many mistakes in there. PowerShell is quite different from C# and you seem to make a lot of assumptions. :)
First of all, that's not how you call PowerShell functions. Also not sure why you added quotes around the parameters? They are redundant. If you fix the function calls your code should function as expected.
$mypath = getFullPath $loc $folder
Then there's a semicolon in your switch statement, which is also wrong. Then, you don't have to escape the $ if you just use ''. The break is also redundant because return exits the function in this case.
"192.168.10.10" { return 'E$' }
Also, one interesting thing about PowerShell: You could just get rid of the return in getFullPath:
function getFullPath($loc, $folder) {
$drive = getDriveLetter($loc)
"\\$loc\$drive\DATA\$folder"
}
PowerShell returns uncaptured output, which is important to be aware of, it can be the cause of many obscure bugs.
The problem is in how you are calling your functions. Function arguments are space delimited in PowerShell, and do not use parentheses to enclose the arguments.
getFullPath $loc $folder
When you wrap arguments in parentheses, you are creating an array containing two values, and passing that array as the first argument.
getFullPath($loc, $folder)
This line passes an array containing two strings #($loc, $folder) as the first argument, and then, because there are no other arguments on the line, it passes $null to the second. Inside the function, the array is then joined to be used as a string, which is the behavior you observed.
The problem is how you pass the parameters to the functions.
See more details on above link:
How do I pass multiple parameters into a function in PowerShell?
function getDriveLetter() {
param($ip)
switch($ip) {
"192.168.0.228" {return "E`$"; break}
"192.168.10.20" {return "D`$"; break}
default {"Unknown"; break}
}
}
function getFullPath() {
param($loc, $folder)
$drive = getDriveLetter -ip $loc
$str = "\\$loc\$drive\DATA\$folder"
return $str
}
function testPath() {
param($loc, $folder)
$mypath = getFullPath -loc $loc -folder $folder
if (Test-Path $mypath) {
return $true
} else {
return $false
}
}
testPath -loc "192.168.10.10" -param "myfolder1\myfolder2\"

convert "Yes" or "No" to boolean

I want to parse user values contained in .CSV file. I don't want my users to enter "Yes" or "No" but instead enter "True" or "False". In each case I want to convert to the equivalent boolean values: $true or $false. Ideally I would like a default value, so if there's misspelt "Yes or "No" I would return my default value: $true or $false.
Hence, I wondered if there is a neat way of doing this other than
if(){} else (){}
One way is a switch statement:
$bool = switch ($string) {
'yes' { $true }
'no' { $false }
}
Add a clause default if you want to handle values that are neither "yes" nor "no":
$bool = switch ($string) {
'yes' { $true }
'no' { $false }
default { 'neither yes nor no' }
}
Another option might be a simple comparison:
$string -eq 'yes' # matches just "yes"
or
$string -match '^y(es)?$' # matches "y" or "yes"
These expressions would evaluate to $true if the string is matched, otherwise to $false.
Ah, the magic of powershell functions, and invoke expression.
function Yes { $true }
function No { $false }
$magicBool = & $answer
Note: This is case insensitive, but will not handle misspellings
If the only possible values are "Yes" and "No" then probably the simplest way is
$result = $value -eq 'Yes'
With misspelled values and the default $false the above will do as well.
With misspelled values and the default $true this will work
$result = $value -ne 'No'
All of these are valid approaches. If you are looking for a one liner, this will validate it is an acceptable value and set to boolean true if in the 'true' value set. This will also give you a default $false value.
$result = #("true","false","yes","no") -contains $value -and #("true","yes") -contains $value
For a default $true value you would need something like so.
$result = $true
if (#("true","false","yes","no") -contains $value) {
$result = #("true","yes") -contains $value
}
Without a full snippet of your existing code, something like this would probably be an alternative path to take, as opposed to a string of IF statements.
NOTE: This will not handle simple 'Y' or 'N' input, but is case insensitive. So, you should be able to see 'yes' or 'YES' working, as well.
$myVar = Read-Host 'What is your answer?'
switch ($myVar)
{
Yes {$myVarConverted = $true; break}
True {$myVarConverted = $true; break}
No {$myVarConverted = $false; break}
False {$myVarConverted = $false; break}
default {"Invalid Input"; break}
}
Write-Host $myVarConverted
Please see my additional comment on your question about the 'misspelling' caveat. That's difficult to code around without specific restrictions or requirements.
Here's the way I do Yes-No answers:
function ask-user
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string] $question
)
Process
{ $answer = read-Host $question
if ("yes" -match $answer) {$true}
elseif ("no" -match $answer) {$false}
else {ask-user $question}
}
}
You can easily substitute true and false for yes and no.
This one is case insensitive, and will match valid abbreviations. (Y or N).
In the case of misspellings, it asks again. Yeah, I could have done it without recursion, but I'm lazy.
These are great solutions above, but let me just say that this whole topic just proves the vast shortcomings of Powershell...
[System.Convert]::ToBoolean("False") -eq $true ?
[System.Convert]::ToBoolean("0") -eq $true ?
Really?
Give me a f--kin break.
For me :-
Function convert2Bool($this) { return ($("False","0","","N","No",'$False',"Off") -notcontains [string]$this) }
can adjust if you don't want $null blank-string going to $false, else fine.

Powershell loop only if condition is true

Very new to coding in general, so I fear I am missing something completely obvious. I want my program to check for a file. If it is there, just continue the code. If it has not arrived, continue cheking for a given amount of time, or untill the file shows up. My loop works on its own, so when i only select the do-part in Powershell ISE, it works. But when i try running it inside the if statement, nothing happens. The loops doesnt begin.
$exists= Test-Path $resultFile
$a = 1
if ($exists -eq "False")
{
do
{
$a++
log "Now `$a is $a "
start-sleep -s ($a)
$exists= Test-Path $resultFile
write-host "exists = $exists"
}
while (($a -le 5) -and ($exists -ne "True"))
}
Another way of doing this is using a while loop:
$VerbosePreference = 'Continue'
$file = 'S:\myFile.txt'
$maxRetries = 5; $retryCount = 0; $completed = $false
while (-not $completed) {
if (Test-Path -LiteralPath $file) {
Write-Verbose "File '$file' found"
$completed = $true
# Do actions with your file here
}
else {
if ($retryCount -ge $maxRetries) {
throw "Failed finding the file within '$maxRetries' retries"
} else {
Write-Verbose "File not found, retrying in 5 seconds."
Start-Sleep '5'
$retryCount++
}
}
}
Some tips:
Try to avoid Write-Host as it kills puppies and the pipeline (Don Jones). Better would be, if it's meant for viewing the script's progress, to use Write-Verbose.
Try to be consistent in spacing. The longer and more complex your scripts become, the more difficult it will be to read and understand them. Especially when others need to help you. For this reason, proper spacing helps all of us.
Try to use Tab completion in the PowerShell ISE. When you type start and press the TAB-key, it will automatically propose the options available. When you select what you want with the arrow down/up and press enter, it will nicely format the CmdLet to Start-Sleep.
The most important tip of all: keep exploring! The more you try and play with PowerShell, the better you'll get at it.
As pointed out in comments, your problem is that you're comparing a boolean value with the string "False":
$exists -eq "False"
In PowerShell, comparison operators evaluate arguments from left-to-right, and the type of the left-hand argument determines the type of comparison being made.
Since the left-hand argument ($exists) has the type [bool] (a boolean value, it can be $true or $false), PowerShell tries to convert the right-hand argument to a [bool] as well.
PowerShell interprets any non-empty string as $true, so the statement:
$exists -eq "False"
is equivalent to
$exists -eq $true
Which is probably not what you intended.

Exiting the function while in loops like Foreach-Object in PowerShell

I have a function like this in Powershell:
function F()
{
$something | Foreach-Object {
if ($_ -eq "foo"){
# Exit from F here
}
}
# do other stuff
}
if I use Exit in the if statement, it exits powershell, I don't want this behavior. If I use return in the if statement, foreach keeps executing and the rest of the function is also executed. I came up with this:
function F()
{
$failed = $false
$something | Foreach-Object {
if ($_ -eq "foo"){
$failed = $true
break
}
}
if ($failed){
return
}
# do other stuff
}
I basically introduced a sentinel variable holding if I broke out of the loop or not. Is there a cleaner solution?
Any help?
function F()
{
Trap { Return }
$something |
Foreach-Object {
if ($_ -eq "foo"){ Throw }
else {$_}
}
}
$something = "a","b","c","foo","d","e"
F
'Do other stuff'
a
b
c
Do other stuff
I'm not entirely sure of your specific requirements, but I think you can simplify this by looking at it a different way. It looks like you just want to know if any $something == "foo" in which case this would make things a lot easier:
if($something ? {$_ -eq 'foo')) { return }
? is an alias for Where-Object. The downside to this is that it will iterate over every item in the array even after finding a match, so...
If you're indeed searching a string array, things can get even simpler:
if($something -Contains 'foo') { return }
If the array is more costly to iterate over, you might consider implementing an equivalent of the LINQ "Any" extension method in Powershell which would allow you to do:
if($something | Test-Any {$_ -eq 'foo'}) { return }
As an aside, while exceptions in the CLR aren't that costly, using them to direct procedural flow is an anti-pattern as it can lead to code that's hard to follow, or, put formally, it violates the principal of least surprise.