elseif statement containing multiple expressions separated by -or failing - powershell

I have created a PowerShell script which is basically a simple calculator. The script needs to be passed 3 parameters, the first one being a number, the second one being an arithmetic operator (+ - * /) and the third one being another number, after which the script returns the calculated value.
Initially, the script worked being like this:
param (
[double]$num1,
[string]$op,
[double]$num2
)
switch ($op) {
"+" {write-host $($num1+$num2)}
"-" {write-host $($num1-$num2)}
"*" {write-host $($num1*$num2)}
"/" {write-host $($num1/$num2)}
}
However, I want to make the script a bit more complex by throwing a different error message for each of these situations:
When no parameters are passed, the message would be "You haven't specified any parameters".
When the second parameter isn't either "+", "-", "*" or "/", the message would be
"You haven't specified a valid operator (+ - * /) as the second parameter".
When the previous 2 situations don't occur but no third parameter (the 2nd number) is passed, the message would be "You haven't specified the second number".
So, with this in mind, I made some changes to the script ending up like this:
param (
[double]$num1,
[string]$op,
[double]$num2
)
if ($num1 -eq "") {
write-host "You haven't specified any parameters"
} elseif (($op -ne "+") -or ($op -ne "-") -or ($op -ne "*") -or ($op -ne "/")) {
write-host "You haven't specified a valid operator (+ - * /) as the second parameter"
} elseif ($num2 -eq "") {
write-host "You haven't specified the second number"
} else {
switch ($op) {
"+" {write-host $($num1+$num2)}
"-" {write-host $($num1-$num2)}
"*" {write-host $($num1*$num2)}
"/" {write-host $($num1/$num2)}
}
}
So now, if I don't pass any parameters to the script, I get the "You haven't specified any parameters" message.
If I only pass the first parameter, I get the "You haven't specified a valid operator (+ - * /) as the second parameter", which is good.
But if I pass the first parameter and, as the second parameter I pass either "+", "-", "*" or "/", I still get the "You haven't specified a valid operator (+ - * /) as the second parameter" message so there's something wrong in the first elseif statement, although it seems to be syntactically-correct.

Use Advanced Function Parameter Options
The solution mjolinor is suggesting (in his comment) would look something like this:
[CmdletBinding()]
param(
[Parameter(
Mandatory=$true
)]
[double]
$num1,
[Parameter(
Mandatory=$true
)]
[ValidateSet('+', '-', '*', '/')]
[string]
$op,
[Parameter(
Mandatory=$true
)]
[double]
$num2
)
switch ($op) {
"+" {write-host $($num1+$num2)}
"-" {write-host $($num1-$num2)}
"*" {write-host $($num1*$num2)}
"/" {write-host $($num1/$num2)}
}
This has the added benefit of letting you tab complete the operator.
Your Solution
The reason yours isn't working is because you're using -or when you should be using -and:
elseif (($op -ne "+") -and ($op -ne "-") -and ($op -ne "*") -and ($op -ne "/"))
It can get confusing because of the fact that you're checking for the negative condition (i.e. if (an_invalid_value)) where the conditions inside are negated (-ne).
Consider what happens in your version if you choose minus -: You evaluate $op -ne '+' and this is true, so condition satisfied, nothing else is tested, you go right to telling the user they did the wrong thing. With -and, all of the -ne conditions would need to be satisfied.
Here's an even simpler way of doing it your way:
elseif ( ('+','-','*','/') -notcontains $op)
Basically you say "if this array of operators does not contain $op, then display the error." It's much easier to follow.
But what's really much easier, and safer, and maintainable, is to use the built-in parameter validation options. Learn them; they're great!

Think about your condition statement:
elseif (($op -ne "+") -or ($op -ne "-") -or ($op -ne "*") -or ($op -ne "/"))
The -or means if any of them are true, then the whole test is true.
What you want is for the whole test to be true only if all of them are true.
What can you do to fix that?

Related

Verifying a string is not empty/null in an If condition

I'm writing a script that will accept user input via Read-Host (set to $String), and I want to avoid any issues that could be caused by having a blank value for the variables. Since I'll be using this a lot, I want to implement it into a function that verifies no invalid characters are being used.
I thought I could use an if statement with ![string]::IsNullOrEmpty($String) as one of the conditions:
Function Test-ValidCharacters ($String, $ValidCharacters) {
if (($String -match $ValidCharacters) -and (!([string]::IsNullOrEmpty($String)))) {
return $true
}
else {return $false}
}
I also tried this:
Function Test-ValidCharacters ($String, $ValidCharacters) {
if (($String -match $ValidCharacters) -and ($String -ceq "")) {
return $true
}
else {return $false}
}
In both of these cases, I can just hit enter when presented with the $String's Read-Host prompt and the script will behave as if the function returned $True (and then later encounter fatal errors). The other half works - if I include characters not specified by $ValidCharacters the function returns $False as expected.
I am sure I'm missing something here. I even tried doing a second nested if statement and got the same result.
Edit: Here's the code snippet where I call the function and notice the issue.
$ValidCharacters = '[^a-zA-Z0-9]'
$FirstN = Read-Host -Prompt "New user's first name"
While (Test-ValidCharacters $FirstN $ValidCharacters -eq $false) {
Write-Output "$FirstN contains illegal characters. A-Z, a-z, and 0-9 are accepted."
$FirstN = Read-Host -Prompt "New user's first name"
}
Assuming $ValidCharacters isn't itself an empty string and contains an anchored character-range regex (regular expression) that covers the entire input string, such as ^[a-z0-9./:]+$, given that the -match operator matches any substring by default (note that a better name for the parameter is therefore something like $ValidationRegex):[1]
In the first function definition, the RHS of your -and operation is redundant - it adds nothing to the conditional, because if $String -match $ValidCharacters is $true, then so is ! [string]::IsNullOrEmpty($String), by definition.
Conversely, in the second function definition your -and operation always returns $false, because $String -ceq "" is by definition $false, if the LHS returned $true.
Assuming that your intent is to prevent empty or all-whitespace input and to ensure that any string - trimmed of incidental leading and/or trailing whitespace - is composed only of expected characters, use the following:
Function Test-ValidCharacters ($String, $ValidCharacters) {
# Note: no strict need for `return`.
$String.Trim() -match $ValidCharacters
}
[1] Alternatively, stick with $ValidCharacters and pass a regex that describes only a single valid character, such as '[a-z0-9./:]', and construct the entire-string matching regex inside the function with '^' + $ValidCharacters + '+$'

If not empty jump to somewhere in PS

Hello and good morning(:
I'm looking to see if I'm able to jump to somewhere in PS without wrapping it in a ScriptBlock; hell, I'd even be okay with that but, I'm just unsure on how to go about it.
What I'm trying to do is: add a Parameter Set to a function and if something is supplied to the parameter -GrpSelec(I know imma change it), then just skip the rest of the script and go to my $swap variable to perform the switch.
$Group1 = #("1st Group", "2nd Group")
$Group2 = #("3rd Group", "4th Group")
Function Test-Group{
param(
[ValidateSet("Group1","group2")]
[array]$GrpSelec)
if($GrpSelec){ &$swap }
$AllGroups = #("Group1", "Group2")
for($i=0; $i -lt $AllGroups.Count; $i++){
Write-Host "$($i): $($AllGroups[$i])"}
$GrpSelec = Read-Host -Prompt "Select Group(s)"
$GrpSelec = $GrpSelec -split " "
$swap = Switch -Exact ($GrpSelec){
{1 -or "Group1"} {"$Group1"}
{2 -or "Group2"} {"$Group2"}
}
Foreach($Group in $swap){
"$Group"}
}
Is something like this even possible?
I've googled a couple of similar questions which point to the invocation operator &(as shown above), and/or, a foreach which is definitely not the same lol.
take it easy on me, im just experimenting(:
How about a simple if statement?
function Test-Group {
param(
[string[]]$GrpSelec
)
if(!$PSBoundParameters.ContainsKey('GrpSelect')){
# no argument was passed to -GrpSelec,
# populate $GrpSelec in here before proceeding with the rest of the script
}
# Now that $GrpSelec has been populated, let's do the work
$swap = Switch -Exact ($GrpSelec){
{1 -or "Group1"} {"$Group1"}
{2 -or "Group2"} {"$Group2"}
}
# rest of function
}

Powershell Read-Host Not adding or multiplying correctly?

What exactly am I doing wrong here, it seems to be subtracting just fine but adding and multiplying seems to not work at all.
How do I get it to do the calculations correct and allow the if statement to also work as it seems to always run even if the numbers are incorrect size.
$a = Read-Host "What is your name?"
$b = Read-Host "Enter a 2 digit number"
$c = Read-Host "Enter a 3 digit number"
if (($b -ge 10) -and ($b -le 99) -and ($c -ge 100) -and ($c -le 999)){
$d = $b + $c
$e = $b * $c
$g = $b - $c
$d
$e
$g
Write-host "Here you go $a"
}
else {
write-host "Enter the numbers correctly"
}
Here the results I get
Read-Host always outputs a string.
In order to treat the output as a number, you must explicitly convert it to one:
$a = Read-Host "What is your name?"
# Note: Add error handling with try / catch
# and a retry loop to deal with invalid input.
[int] $b = Read-Host "Enter a 2 digit number"
[int] $c = Read-Host "Enter a 3 digit number"
The above type-constrains variables $b and $c to integer values (by placing the [int] cast to the left of the target variable in the assignment), which automatically converts Read-Host's [string] output to [int].
To spell it out with a concrete example that prompts until a two-digit (decimal) number is entered:
do {
try {
[int] $b = Read-Host "Enter a 2 digit number"
} catch {
continue # Not a number - stay in the loop to prompt again.
}
if ($b -ge 10 -and $b -le 99) { break } # OK, exit the loop.
} while ($true)
Note: Strictly speaking, the [int] cast accepts anything that would work as a number literal in PowerShell, which includes hexadecimal representations, such as 0xA, as well as number with a type suffix, such as 10l - see this answer for more information.
As for what you tried:
Except for -, all the operators used in your code have string-specific overloads (meaning); note that it is sufficient for the LHS to be of type [string] to trigger this behavior.[1]
-lt / -ge perform lexical comparison with strings; e.g., '10' -gt '2' yields $false, because, in lexical sorting, string '10' comes before string '2'.
-and / -or treat empty strings as $false, and any nonempty string as $true; e.g., '0' -and '0' is $true, because '0' is a nonempty string.
+ performs string concatenation; e.g., '1' + '0' is '10'.
* performs string replication; e.g., '1' * 3 is '111' - the LHS is repeated as many times as specified by the number on the RHS; note that '1' * '3' works the same, because the RHS is coerced to an [int] in this case.
- is the only exception: it always performs a numeric operation, if possible; e.g, '10' - '2' yields 8, because both operands were implicitly converted to [int]s.
[1] Typically, it is the LHS of an operation that determines its data type, causing the RHS to be coerced to a matching type, if necessary.

Why doesn't my condition (-not A -and (A -or B -or C)) work?

I wrote _in function to detect if we must install packages or not. The arguments -packages and +packages work but +base and +full don't work, how can I fix it ?
$scriptArgs=$args
function _in {
Param($find)
foreach ($i in $scriptArgs) {
if ($i -eq $find) {
return 1
}
}
return 0
}
# Install packages
if (-not (_in("-packages")) -and (_in("+packages") -or _in("+base") -or _in("+full"))) {
PrintInfo "* Installing packages"
}
This works:
PS> powershell .\scripts\win\install_zds.ps1 +packages
* Installing packages
PS> powershell .\scripts\win\install_zds.ps1 +packages -packages
-packages disables package installation and +packages enables package installation.
This doesn't work:
PS> powershell .\scripts\win\install_zds.ps1 +base
PS> powershell .\scripts\win\install_zds.ps1 +full
+base and +full should enable package installation.
EDIT: I would like understand why:
I follow PetSerAI comment, then, I remove the parentheses like this:
if (-not (_in "-packages") -and ((_in "+packages") -or (_in "+base") -or (_in "+full"))) { }
This works, but I don't understand why. I found this explain about parentheses in PowerShell:
Powershell is a parsed and interpreted language. The interpreter see's parenthesis as a control structure and is not expected or required at the Call Site.
But with test-function("Hello"), hello is string not a structure.
function Test-Function {
Param(
[string]
$hello
)
"String: $hello"
}
Test-Function("Hello")
Test-Function "Hello"
The expression
-not (_in("-packages")) -and (_in("+packages") -or _in("+base") -or _in("+full"))
isn't evaluated in the way you apparently expect.
PowerShell functions (unlike method calls) expect their arguments as a whitespace separated list without parentheses, i.e. _in("foo") should be _in "foo". The parentheses aren't syntactically wrong (_in("foo") is a valid expression), but PowerShell will parse the parentheses as a grouping expression, which is evaluated first. Meaning that PowerShell will first expand _in("foo") to _in "foo" before actually calling the function.
However, since you're putting function calls in a boolean expression you need to put grouping parentheses around each function call to have the function calls evaluated first, so that the result of the function calls is used in the boolean expression:
(_in "foo") -and (_in "bar")
Without that the boolean operators would be parsed as parameters for the first function. In other words
_in("foo") -and _in("bar")
would be expanded to
_in "foo" -and _in "bar"
which would then invoke the function _in() with the arguments foo, -and, _in, and bar.
Because of that your condition must be written as
-not (_in "-packages") -and ((_in "+packages") -or (_in "+base") -or (_in "+full"))
With that said, what you're trying to implement would not only re-implement the -in/-contains operators, it is also contrary to normal PowerShell parameter handling. I strongly recommend you look into advanced function parameters and parameter sets. They work on both function and script level.
Example:
[CmdletBinding(DefaultParameterSetName='none')]
Param(
[Parameter(ParameterSetName='base', Mandatory=$true)]
[Switch]$Base,
[Parameter(ParameterSetName='full', Mandatory=$true)]
[Switch]$Full
)
switch ($PSCmdlet.ParameterSetName) {
'none' { 'install nothing' }
'base' { 'base install' }
'full' { 'full install' }
}
Note that Powershell is very unusual when it comes to -and and -or, they have equal precedence. Most other languages aren't like this (C#, vbscript...). It seems like it was overlooked in the beginning, and now they don't want to break existing scripts.
$true -or $true -and $false
False
$true -or ($true -and $false)
True
This is more typical behavior, with + and *. * has higher priority than +.
1 + 2 * 3
7
(1 + 2) * 3
9

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

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.