Null Conditional in Powershell? - powershell

C# and other languages have null-conditionals usually ?.
A?.B?.Do($C);
Will not error out when A or B are null.
How do I achieve something similar in powershell, what's a nicer way to do:
if ($A) {
if ($B) {
$A.B.Do($C);
}
}

Powershell 7 Preview 5 has operators that deal with nulls. https://devblogs.microsoft.com/powershell/powershell-7-preview-5/
$a = $null
$a ?? 'is null' # return $a or string if null
is null
$a ??= 'no longer null' # assign if null
$a ?? 'is null'
no longer null
EDIT: Powershell 7 Preview 6 piles on more new operators: https://devblogs.microsoft.com/powershell/powershell-7-preview-6/. Since variable names can have a '?' in the name, you have to surround the variable name with curly braces:
${A}?.${B}?.Do($C)

PowerShell doesn't have the null-conditional operator, but it silently ignores property references on null-value expressions, so you can just "skip" to the method call at the end of the chain:
if($null -ne $A.B){
$A.B.Do($C)
}
Works at any depth:
if($null -ne ($target = $A.B.C.D.E)){
$target.Do($C)
}

As Mathias R. Jessen's answer points out, PowerShell by default has null-conditional access behavior (null-soaking) with respect to property access[1]; e.g., $noSuchVar.Prop quietly returns $null
js2010's answer shows the related null-coalescing operator (??) / null-conditional-assignment operators (??=), which are available in PowerShell [Core] v 7.1+
However, up to PowerShell 7.0:
There is no way to null-conditionally ignore method calls: $noSuchVar.Foo() always fails.
Similarly, there's no way to null-conditionally ignore (array) indexing: $noSuchVar[0] always fails.
If you opt into more rigorous behavior with Set-StrictMode, even the property-access null-soaking is no longer an option: with Set-StrictMode -Version 1 or higher, $noSuchVar.Prop results in an error.
In PowerShell [Core] 7.1+, null-conditional (null-soaking) operators are available:
The new operators:
have the same form as in C# in principle: ?. and ?[...]
but - as of v7.1 - require enclosing the variable name in {...}
That is, you currently cannot use just $noSuchVar?.Foo(), $A?.B, or $A?[1], you have to use
${noSuchVar}?.Foo(), ${A}?.B, or ${A}?[1]
The reason for this cumbersome syntax is that there are backward-compatibility concerns, because ? is a legitimate character in variable names, so hypothetical existing code such as $var? = #{ one = 1}; $var?.one could break without using the {...} to disambiguate the variable name; in practice, such use is vanishingly rare.
If you think that not encumbering the new syntax is more important than potentially breaking scripts with variable names ending in ?, make your voice heard at this GitHub issue.
[1] PowerShell's default behavior even offers existence-conditional property access; e.g., $someObject.NoSuchProp quietly returns $null.

Related

Why can't I dot-reference a hashtable key's value property if the property name is an integer

Consider the following hashtable:
$table = #{
6 = '10.11.12.13', '10.11.12.14'
15 = 'domain.tld'
NameServers = '10.11.12.13', '10.11.12.14'
}
Normally when you have a hashtable, you can . reference the key name to work with it in a more object-oriented style vs. using the array-accessor syntax [key]. For example, invoking the NameServers key on the hashtable above with a . results in the value being returned, and I can work with members of that property's value as expected:
$table.NameServers # ========> 10.11.12.13
# ========> 10.11.12.14
$table.NameServers.Count # ========> 2
But if I try accessing the 6 key with a . which contains the same string content, it references the correct value fine, but I cannot invoke any members on the returned object. I must use the traditional array-accessor here:
$table.6 # =====> 10.11.12.13
# =====> 10.11.12.14
$table.6.Count # =====> ParserError:
# =====> Line |
# =====> 1 | $hi.6.Count
# =====> | ~
# =====> | Missing property name after reference operator.
$table[6].Count # =====> 2
Of course, ( $table.6 ).Count works around this issue but this is still a weird quirk of the syntax which I can't explain. Interestingly enough, converting the hashtable to a PSCustomObject yields the same issue. Is this perhaps a parser bug? Or is there something else going on here?
I tested this in Windows PowerShell and PowerShell 7.1 and it happens in both.
Note: This answer originally incorrectly claimed that $table.6 doesn't work, due to the hashtable key being integer-typed, but it works just fine, because the 6 in the .6 "property" access is parsed as an integer as well.
As Gyula Kokas' helpful answer points out, you're seeing problematic behavior in PowerShell's parser, present as of PowerShell 7.2, discussed in detail in GitHub issue #14036. The behavior is unrelated to hashtables per se:
What follows ., the member-access operator, is parsed as a number if it looks like a number literal, such as 6, 6l (a [long]) or even 6.0(!, a [double]).
See below for how that number is then used.
Any attempt to use another property access then triggers the error you saw:
$foo.6.bar # !! Error "Missing property name after reference operator."
In fact, following a property name that starts with a digit with a letter causes the error too - even though 6a can not interpreted as a number literal.
$foo.6a # !! Same error.
As an aside: PowerShell even allows you to use variable references, and even expressions (enclosed in (...)) as property names (e.g., $propName = 'Length'; 'foo'.$propName or ('foo'.('L' + 'ength')
Workarounds:
As you've demonstrated, enclosing the first property access in (...) - ($table.6).Count works, and so does $table.(6).Count
You've also demonstrated $table[6].Count, which works for hashtables.
For accessing an object's actual property, $obj.'6'.Count would work too (given that property names are always strings), as it would with hashtables with string keys (e.g. #{ '6'= 'six' }.'6'.Length)
Considerations specific to hashtables:
As a syntactic convenience, PowerShell allows property-access syntax ($obj.someProperty) to also be used to access the entries of a hashtable, in which case the property "name" is taken as the key of an entry in the hashtable.
The type-native syntax to access hashtable entries is index syntax ($hash[$someKey]).
While property names (referring to members of a .NET type) are invariably strings:
a hashtable's keys can be of any type.
on accessing an entry, the lookup key must not only have the right value, but must also be of the exact same type as the key stored in the hashtable.
Perhaps surprisingly, when defining a hashtable, unquoted keys situationally become either strings or numbers:
If the unquoted word can be interpreted as a number literal (e.g., 6), it is parsed as such, and you end up with a numeric key; in the case of 6, it will be [int]-typed key, because the usual number-literal typing applies (e.g., 1.0 would become a [double] key (not advisable, because binary floating-point values do not always have exact decimal representations), and 2147483648 would become a [long]).
Otherwise (e.g., NameServers), the key is [string]-typed.
Caveat: If the name starts with a digit (but isn't a number literal; e.g. 6a), an error occurs; use quoting ('6a') as a workaround - see GitHub issue #15925
That is, even though strings in expressions normally require quoting, for convenience you may omit the quoting when defining keys in hashtable literals - but the rules for recognizing number literals still apply.
Explicitly typing hashtable keys:
To ensure that a given key is interpreted as a [string], quote it (e.g., '6')
Generally, you may also use a cast to type your keys explicitly. (e.g. [long] 6) or, in number literals, the usual number-type suffix characters, such as L for [long] (e.g. 6L) - see this answer for an overview of the supported suffixes, whose number has grown significantly in PowerShell (Core) 7+.
An example, based on your hashtable:
It follows from the above that your 6 key is of type [int] (and would have to be defined as '6' in your hashtable literal if you wanted it to be a string).
Because the 6 in $name.6 is also parsed as an [int], the lookup succeeds, but note that it wouldn't succeed if different numeric types were at play:
# !! Output is $null, because the entry key is of type [long],
# !! whereas the lookup key is [int].
#{ 6L = '[long] 6' }.6
Considerations specific to property access:
With actual property access (accessing a native member of a .NET type), the fact that names that look like numbers are actually parsed as numbers first - before, of necessity, being converted to strings - can result in surprising behavior:
# !! Output is $null, because the 6L after "." is parsed as a
# !! ([long]) number first, and then converted to a string, which
# !! results in "6" being used.
([pscustomobject] #{ '6L' = 'foo' }).6L
# OK - The quoting forces 6L to be a string.
([pscustomobject] #{ '6L' = 'foo' }).'6L' # -> 'foo'
I think here is the official issue for your question:
https://github.com/PowerShell/PowerShell/issues/14036
I think the best workaround is $table.(6).count
Hashtables in powershell expect key names to be strings. When you type $table.6 based on your original code the 6 is interpreted as an integer when it's expecting it to be a string (a type conversion feature). The simplest way to resolve this would be to replace 6 with 'six', however you can make this work in a round about way.
If you make the alterations to the code like so:
$table = #{
'6' = '10.11.12.13', '10.11.12.14'
'15' = 'domain.tld'
NameServers = '10.11.12.13', '10.11.12.14'
}
You can reference it by implicitly instructing powershell to treat the 6 as a string by doing the following:
$table.'6'.count
2
It's the same principal as adding keynames with special characters, they have to be instantiated as strings to prevent interpolation or type conversion. For example:
$table = #{'bla $something' = 'anything'}
$table.'bla $something'
anything
In your case I'd do the following and move on to the next problem:
$table = #{
Six = '10.11.12.13', '10.11.12.14'
Fifteen = 'domain.tld'
NameServers = '10.11.12.13', '10.11.12.14'
}
$table.Six.count
2
For further investigation into type conversion follow this link: Powershell Type Conversion Reference

Why does Powershell not process a string correctly which has a dollar sign and a question mark?

In PowerShell, why does this return nothing:
PS > $y = "$prid?api"
PS > echo $y
But these work better:
PS > $y = "$prid\?api"
PS > echo $y
18\?api
PS > $y = "${prid}?api"
PS > echo $y
18?api
Surprisingly, ? can be used in a PowerShell variable name without requiring that the name be enclosed in {...}.
Therefore, based on the rules of PowerShell's expandable strings (string interpolation inside "..." strings), "$prid?api" looks for a variable with verbatim name prid?api rather than considering the ? to implicitly terminate the preceding identifier.
That is, you can actually define and reference a variable named prid?api as follows:
PS> $prid?api = 'hi'; $prid?api
hi
This surprising permissiveness actually gets in the way of a recently introduced language feature, null-conditional access, introduced in PowerShell (Core) 7.1:
# v7.1+; note that the {...} around the name is - unexpectedly - *required*.
${variableThatMayBeNull}?.Foo()
GitHub issue #14025 advocates for obviating the need to use {...} in this case.

Powershell function call changing passed string into int

So I am using the kind of buggy Sapien powershell studio to make a powershell driven GUI application, and I am attempting to perform an ADSI query.
$nameOfDeviceInput is a System.Windows.Forms.TextBox
On one form, I have the following function:
$buttonPerformAction_Click={
if (FindInAD($nameOfDeviceInput.Text).Count -gt 0)
{
$buttonPerformAction.BackColor = 'Red'
$buttonPerformAction.Text = "System already exists in AD with that name. Try another name"
return
}
.....
}
On the "main" form, I have the function FindInAD
function FindInAd($nameOfSystem)
{
Write-Host "seeking system" $nameOfSystem
([adsisearcher]"(CN=$nameOfSystem)").FindAll()
}
FindInAd() is failing because for whatever reason, $nameOfSystem is set to 1, and if I don't explicitly cast it as a string, it gets implicitly cast to Int32 (obviously)
I have tried the following:
Fully qualifying the textbox input by notating the form it belongs to ( $adObjectModifier )
$buttonPerformAction_Click={
if (FindInAD($adObjectModifier.$nameOfDeviceInput.Text).Count -gt 0)
{
$buttonPerformAction.BackColor = 'Red'
$buttonPerformAction.Text = "System already exists in AD with that name. Try another name"
return
}
.....
}
Explicitly casting the $nameOfSystem parameter as a type of [string]
function FindInAd([string]$nameOfSystem)
{
Write-Host "seeking system" $nameOfSystem
([adsisearcher]"(CN=$nameOfSystem)").FindAll()
}
Passing a raw string into FindInAD from the AdObjectModifier form.
....
if (FindInAD("Test").Count -gt 0)
....
There is nothing else on the output pipeline at the time, (at least not from me) in between the method invocation. It is EventHandler > Function Call with String parameter
Why are the strings I'm passing getting changed to a digit???
EDIT: I think my passed parameter is being automatically replaced with the resulting boolean somehow, but this doesn't make any sense to me....
Your have a syntax problem:
FindInAD($nameOfDeviceInput.Text).Count # WRONG
Note: Wrong in this context means: the syntax is formally valid, but doesn't do what you expect - see the bottom section.
It should be:
(FindInAD $nameOfDeviceInput.Text).Count
PowerShell commands - functions, cmdlets, scripts and external programs - are invoked like shell commands - foo arg1 arg2 - and not like C# methods - foo('arg1', 'arg2').
That is:
Do not put (...) around the list of arguments.
However, you do need (...) around the call as a whole if you want a command call to participate in an expression, as shown above with the access to property .Count - see this answer for more information.
Separate arguments with spaces, both from each other and from the command name - do not use ,
, between arguments functions differently: It constructs an array that is passed as a single argument - see below.
You may pass simple strings (ones that contain neither spaces nor PowerShell metacharacters such as ; or &) as barewords; that is, quoting them is optional; e.g., instead of foo 'bar', you can call foo bar - see this answer for how PowerShell parses unquoted command arguments.
Also, if a target function or script has explicitly declared parameters (which binary cmdlets invariably do), such as -bar and -baz, you can pass your values as named arguments, i.e. by prepending them with the target parameter name; doing so is good practice in scripts: foo -bar arg1 -baz arg2
By contrast, calling methods of objects uses the syntax familiar from regular programming languages such as C# ($obj.foo('arg1', 'arg2'))
This difference relates two PowerShell's two fundamental parsing modes, explained in detail in this answer:
Commands are parsed in argument mode - as in shells.
Method calls and operator-based expressions are parsed in expression mode - as in regular programming languages.
These modes are required in order to allow PowerShell serve double duty: as a shell on the one hand, and as a scripting (programming) language on the other.
PowerShell can help you avoid this syntax problem:
Note that the problem isn't that using method syntax to call a command is invalid syntax, but that it doesn't work as intended, which can be difficult to diagnose.
In short: When you call command foo as foo('foo', 'bar'), ('foo', 'bar')is a 2-element array, which is then passed to foo as a single argument.
To prevent the problem to begin with, you can set Set-StrictMode to -Version 2 or higher, which makes PowerShell report an error if you accidentally use method syntax when calling a command:
# Turn on the check for accidental method syntax.
# Note: This also turns on ADDITIONAL checks - see below.
Set-StrictMode -Version 2
# This call now produces an ERROR, because the proper syntax would be:
# foo 'a' 'b'
foo('a', 'b')
Caveats:
Set-StrictMode -Version 2 comprises additional strictness checks that you must then also conform to, notably:
You must not reference non-existent variables.
You must not reference non-existent properties; see GitHub issue #2798 for an associated pitfall in connection with PowerShell's unified handling of scalars and collections.
An error is reported only for pseudo method calls with multiple arguments (e.g.,
foo('bar', 'baz')), not with only one; e.g., foo('bar') is accepted, because the single-argument case generally still (accidentally) works.
The errors reported for strictness violations are statement-terminating errors: that is, they only terminate the statement at hand, but by default the script continues; to ensure that overall execution aborts - on any type of error - you'd have to set
$ErrorActionPreference = 'Stop' at the start of your code. See this answer for more information.
As for what you tried:
FindInAD($nameOfDeviceInput.Text).Count
is the same as:
FindInAD ($nameOfDeviceInput.Text).Count
That is, the result of expression ($nameOfDeviceInput.Text).Count is passed as an argument to function FindInAD.

Powershell - add variables inside a json string

I have the following json code in my powershell script.
I set the $variable to 1111111111
$jsonfile = '{"Version": "2012-10-17","Statement": {"Effect": "Allow","Action": "sts:AssumeRole","Resource": "arn:aws:iam::$variable:role/xxxxxx"}}'
The output gives ....arn:aws:iam::$variable:role/xxxxxx..... instead of ....arn:aws:iam::1111111111:role/xxxxxx
The problem is that I must use the single quote for the json string otherwise I will get an error. If I use single quote I wont be able to put the variables inside the string. How do I workaround this problem?
There are various ways to solve your problem, but perhaps the easiest approach is to use PowerShell's string interpolation:
use a double-quoted string overall to enable interpolation of embedded variable references and subexpressions ($(...)).
escape embedded " chars. as `" (using backticks)
disambiguate variable references by enclosing the variable name in {...}.
Simplified example:
PS> $variable='111'
PS> "{`"Version`": `"arn:aws:iam::${variable}:role/xxxxxx`"}}"
{"Version": "arn:aws:iam::111:role/xxxxxx"}}
Note that enclosing variable names in {...} in interpolated strings is only necessary if the following char. could be misinterpreted as part of the variable name.
A : following the variable name - as is the case here - is such a case, because PS variables can have a scope specifier preceding the variable name that is separated from the variable name with :, such as in $env:USERNAME.
DAXaholic's helpful answer shows an alternative based on PowerShell's binary -f operator, which is essentially the same as the .NET framework's String.Format method; as such:
it introduces additional complexity, such as needing to know what its escaping rules are ({ chars. must be escape as {{, and how to format its arguments specified on the RHS of -r ({0} refers to the 1st RHS argument, ...)
on the flip side, -f offers many sophisticated formatting options.
Also, consider use of the Convert*-Json cmdlets his answer demonstrates: even though they're less performant, they ultimately make manipulation of JSON much easier and more robust.
Alternatives in the realm of native PowerShell code:
String concatenation with the binary + operator:
'{"Version": "arn:aws:iam::' + $variable + ':role/xxxxxx"}}'
String templating with $ExecutionContext.InvokeCommand.ExpandString():
$variable='111'
$tmpl = '{"Version": "arn:aws:iam::${variable}:role/xxxxxx"}}' # string template *literal*
$ExecutionContext.InvokeCommand.ExpandString($tmpl) # performs on-demand interpolation
Another solution would be
$jsonfile = '{{"Version": "2012-10-17","Statement": {{"Effect": "Allow","Action": "sts:AssumeRole","Resource": "arn:aws:iam::{0}:role/xxxxxx"}}}}' -f $variable
So you have to escape the braces with another brace but in your case you have fewer braces than quotes so it is "less obfuscation" :)
In your case, maybe the simplest solution is just concatenating the strings together instead of using string formatting / interpolation.
In addition you could also go the way with the JSON cmdlets:
$jsonfile |
ConvertFrom-Json |
% { $_.Statement.Resource = "arn:aws:iam::${variable}:role/xxxxxx"; $_ } |
ConvertTo-Json

How comes `if (Test-Path...)` actually works?

In PowerShell, syntax for if is as so:
if (<test1>)
{<statement list 1>}
[elseif (<test2>)
{<statement list 2>}]
[else
{<statement list 3>}]
Another syntax rule is that for subexpressions, you need to use parentheses like this:
write-output (get-date)
So with these two rules combined, I would expect that the test for some path needs to be written with two sets of parentheses like this:
if ((Test-Path ...)) {
# do something
}
However, this also works:
if (Test-Path ...) {
# do something
}
and just for the sake of completeness, this doesn't work:
if (!Test-Path ...) {
# do something
}
(here, you would need to wrap the subexpression in parenthesis as usual).
Can anyone explain the syntax rules that apply here and how comes that I can use the IF test with one parenthesis only? Is it some PowerShell magic or am I misunderstanding the basic syntax rules?
Referring to C.2.2 from Appendix C: The PowerShell grammar in Bruce Payette's Windows PowerShell in Action, we have:
<ifStatementRule> =
'if' '(' <pipelineRule> ')' <statementBlockRule>
[ 'elseif' '(' <pipelineRule> ')' <statementBlockRule> ]*
[ 'else' <statementBlockRule> ]{0|1}
This indicates the ( and ) tokens as part of the literal syntax for recognizing an if statement, and that the <test> from the about_If documentation refers to a pipeline that will be resolved to a Boolean.
Following the pipeline rules, we find:
Test-Path ... parses to a <cmdletCall> of <name> <parameterArgumentToken>,
!Test-Path ... results in an <expressionRule> of <UnaryOperatorToken> <propertyOrArrayReferenceRule>, which fails when the cmdlet call cannot match the simple property or array rule, whereas
!(Test-Path ...) is able to match the parenthesized cmdlet call as a sub-expression.
Edit: See also PowerShell 2.0 Language Specification (thanks to Roman's answer to another question).
The parentheses after the if define a subexpression (if parentheses were required around Test-Path, then we would need parens around $num -eq 5 and every other expression).. The additional parentheses after the not operator is required because Test-Path needs to be evaluated before it can be negated. You can try this without an if statement.
This does not work:
PS> !Test-Path NonExistent.file
This does work:
PS> !(Test-Path NonExistent.file)