Behaviour of PowerShell when Combining HashTables - powershell

Question
Why is $null + #{} valid, but #{} + $null not; even where null is cast to a hashtable (#{} + ([hashtable]$null)).
Example Code
[hashtable]$a = #{demo1=1;demo2='two'}
[hashtable]$b = #{demo3=3;demo4='Ivy'}
[hashtable]$c = $null
#combining 2 hashtables creates 1 with both hashes properties (would error if any properties were common to both)
write-verbose 'a + b' -Verbose
($a + $b)
#combining a null hashtable with a non-null hashtable works
write-verbose 'c + a' -Verbose
($c + $a)
#combing 2 null hashtables is fine; even if we've not explicitly cast null as a hashtable
write-verbose 'c + null' -Verbose
($c + $null)
#however, combinging a hashtable with null (i.e. same as second test, only putting null as the right argument instead of the left, produces an error
write-verbose 'a + c' -Verbose
($a + $c)
Output
Name Value
---- -----
demo3 3
demo4 Ivy
demo1 1
demo2 two
VERBOSE: c + a
demo1 1
demo2 two
VERBOSE: c + d
VERBOSE: a + c
A hash table can only be added to another hash table.
At line:19 char:1
+ ($a + $c)
+ ~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : AddHashTableToNonHashTable
Side Note
Incidentally, this led me to discover this useful trick for a null-coalescing operation for hashtables: ($c + #{}). e.g. ($a + ($c + #{})) avoids the error produced above / (($a + #{}) + ($c + #{})) gives us a completely safe way to add hashtables where either value may be null.

I tried to find out exactly why, but I can't for sure.
My original beliefs were:
There may be some PowerShell implicit type coercion happening, where the thing on the right is cast to match the type on the left. e.g. with "1"+1 the 1 on the right becomes a string and the output is "11", but 1+"1" the right becomes a number and the output is 2.
This definitely is not happening, or $null + #{} would either throw a cast error, or cast and do null + null = null, not output an empty hashtable as it does.
Addition of A + B for things other than numbers has some basic rules for things like array concatenation, but beyond that, it will fall down to the .Net Framework underneath and will try to do a method call like A.op_Addition(B) and it will be up to the thing on the left to say what happens when you try to add the thing on the right to it.
This does happen, but is not happening here. Consider #{} + 0 tells you that a hashtable can only be added to a hashtable. (Lies, you can add it to null). And 0 + #{} tells you that hashtables do not contain an op_Addition method. So the addition with null seems like it should give that error, but doesn't, because it's not falling back to this mechanism.
The PSv3 language spec (which I think is the latest version available), download here: https://www.microsoft.com/en-us/download/details.aspx?id=36389, mentions addition:
7.7.1 Addition Description: The result of the addition operator + is the sum of the values designated by the two operands after the usual
arithmetic conversions (§6.15) have been applied.
The usual arithmetic conversions say:
If neither operand designates a value having numeric type, then [..] all operands designating the value $null are converted to zero of type int and the process continues with the numeric conversions listed below.
That implies $null gets converted to 0 in both your examples. Although that cannot be happening because 0 + #{} and #{} + 0 are both errors.
There is an explicit mention of adding of two hashtables in section 7.7:
7.7.4 Hashtable concatenation Description: When both operands designate Hashtables the binary + operator creates a new Hashtable
that contains the elements designated by the left operand followed
immediately by the elements designated by the right operand.
OK, hashtable addition is handled by the PowerShell engine, but only adding two hashtables.
Section 7. introduction says:
Windows PowerShell: If an operation is not defined by PowerShell, the type of the value designated by the left operand is inspected to see if it has a corresponding op_ method.
And the operation $null + Hashtable doesn't seem to be defined by PowerShell from what I can see in the spec, and the corresponding method for + is op_Addition - a method which hashtables do not have, see error code earlier - and this is not throwing that error, and not only that but in the case of adding to 0 the error comes from the Right operand not the left one.
And the other interesting bit in the spec is:
4.1.2 The characteristics of the null type are unspecified.
So the summary appears to be:
#{} + $null - is triggering the PowerShell handling of adding two hashtables, even though the spec doesn't say that it will.
$null + #{} - it looks like there's an implicit $null + x = x rule, although the spec doesn't seem to mention it, and it might be implementation dependent.
[<type>]$null - casting $null to a numeric type results in 0 (6.15 arithmetic conversions), but casting it to anything else(?) appears to result in $null (not in the spec).
The comment and linked chain which says $null has no type are against the PowerShell spec 4.1.2 "the null type" which says "The null type has one instance, the automatic variable $null (§2.3.2.2), also known as the null value. The characteristics of this type are unspecified." so at least in terminology, it's described as a type in PowerShell even if you can't GetType() on it ..

Related

Who has seen this potential powershell object property treated as method call booby trap?

The following is not an actual question but a cautionary tale about some unexpected PowerShell syntax. The only real question is "Is this behaviour well known to many or only by a few PowerShell developers (i.e. those working ON PowerShell not just WITH PowerShell)?" Note: the examples are only to demonstrate the effect and do not represent meaningful code (no need to ask what the purpose is).
While playing with a PowerShell (5.1.18362.145) switch statement, I received the following error,
PS > $xx = gi somefile
PS > switch ($xx.directory) {
>> $xx.directory{6}
>> }
At line:2 char:17
+ $xx.directory{6}
+ ~
Missing statement block in switch statement clause.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MissingSwitchStatementClause
Given previous research on switch, I expected both the $xx.directory expressions to be evaluated and converted to (matching) strings. Clearly {6} would be expected to be the statement clause. Maybe there is some weird parsing happening. Try separating the expression from the statement,
PS > switch ($xx.directory) {
$xx.directory {6}
}
6
PS >
OK, so what happens if we try both,
PS > switch ($xx.directory) {
>> $xx.directory{5} {6}
>> }
Method invocation failed because [System.IO.FileInfo] does not contain a method named 'directory'.
At line:2 char:1
+ $xx.directory{5} {6}
+ ~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
What the??? I know braces kinda look like parentheses but what is happening here? Let's try it with an actual method,
PS > 'fred'.substring{1}
Cannot find an overload for "substring" and the argument count: "1".
At line:1 char:1
+ 'fred'.substring{1}
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
but String.Substring does have an overload with one argument, though it is supposed to be an int. What is this trying to pass? (Hint: what does it look like?) Let's find out,
PS > Add-Type #'
>> public class huh {
>> public void passing(object o)
>> {
>> System.Console.WriteLine(o.GetType().ToString());
>> }
>> }
>> '#
PS > $whatsit=New-Object huh
PS > $whatsit.passing{1}
System.Management.Automation.ScriptBlock
PS >
Who'da thunk it?
About the only other question would be, "Anybody know where this is described in the documentation (assuming it is still happening in 7.2+)?" (Seriously, I'd like to know if it is.)
As - perhaps unfortunate - syntactic sugar, PowerShell allows you to shorten:
$object.Method({ ... })
to:
$object.Method{ ... }
Note:
In both cases there mustn't be a space after the method name (whereas C# allows "foo".Substring (1), for instance).
Method in the above example merely has to be syntactically valid as a method name in order for both expressions to be treated as method calls - a method call is attempted even if no such method exists or if the name happens to refer to a property instead.
In other words:
Methods that accept exactly one (non-optional) argument of type script block ([scriptblock]; { ... }) allow invocation without parentheses ((...)).
Arguably, such a narrow use case wouldn't have called for syntactic sugar:
Limiting support to script blocks limits the syntactic sugar to PowerShell-provided/-targeted types and their methods, given that script blocks are a PowerShell-specific feature.
The requirement not to separate the name and the opening { with a space is at odds with how script blocks are customarily passed to cmdlets (e.g. 1, 2, 3 | ForEach-Object { $_ + 1 } vs. (1, 2, 3).ForEach{ $_ + 1 } - see below)
Having to switch back to (...) as soon as two or more arguments must be passed is awkward.
Presumably, this was introduced to cut down on the "syntactic noise" of one common scenario: the use of the PSv4+ .ForEach() and .Where() array methods, introduced for the DSC (Desired State Configuration) feature, which are typically invoked with only a script block; e.g.:
(1, 2, 3).ForEach({ $_ + 1 }) can be simplified to (1, 2, 3).ForEach{ $_ + 1 }
As for documentation:
The behavior is described only in the context of the aforementioned .ForEach() and .Where() methods in the context of the conceptual about_Arrays help topic:
The syntax requires the usage of a script block. Parentheses are optional if the scriptblock is the only parameter. Also, there must not be a space between the method and the opening parenthesis or brace.
Given that it applies to any method with the appropriate signature (irrespective of the .NET type and irrespective of whether it is an instance or a static method), it should arguably (also) be documented in the conceptual about_Methods help topic, which isn't the case as of this writing.
Even with coverage in about_Methods, however, the challenge is to even infer that a method call is being attempted when you don't expect that - at least in the switch case the error message doesn't help.
Design musings:
Note that the .ForEach() and .Where() methods are generally a somewhat awkward fit, given that most native PowerShell features do not rely on methods, and that even regular method-invocation syntax can cause confusion with the shell-like syntax used to invoke all other commands (cmdlets, functions, scripts, external programs) - e.g., $foo.Get('foo', bar') vs. Get-Foo foo bar.
This rejected RFC on GitHub proposed introducing -foreach and -where operators, which would have allowed the following, more PowerShell-idiomatic syntax:
1, 2, 3 -foreach { $_ + 1 }
1, 2, 3, 2 -where { $_ -eq 2 }, 'First'

Create an incrementing variable from 2 variables in PowerShell

OK, First I consider myself a newbie and have much to learn about PowerShell and this is my first post ever.
I am trying to loop through some data and put it into a custom object and put them into separate arrays for later use. The issue is that I want to create a variable representing $week_data1 by using a counter $i so I can reduce the amount of code required. I do have a concatenated variable being written out: write-host '$week++ ='$week$i But I think it is being represented as a string?
How can I get $week_data$i to represent the array to insert the data?
Input data. Each week ends on Saturday.
$week1=#('2021-05-01')
$week2=#('2021-05-02', '2021-05-03', '2021-05-04', '2021-05-05', '2021-05-06', '2021-05-07', '2021-05-08')
$week3=#('2021-05-09', '2021-05-10', '2021-05-11', '2021-05-12', '2021-05-13', '2021-05-14', '2021-05-15')
$week4=#('2021-05-16', '2021-05-17', '2021-05-18', '2021-05-19', '2021-05-20', '2021-05-21', '2021-05-22')
$week5=#('2021-05-23', '2021-05-24', '2021-05-25', '2021-05-26', '2021-05-27', '2021-05-28', '2021-05-29')
$week6=#('2021-05-30', '2021-05-31')
$month =#($week1, $week2, $week3, $week4, $week5, $week6)
Create the output structures to be populated.
$week_data1=#()
$week_data2=#()
$week_data3=#()
$week_data4=#()
$week_data5=#()
$week_data6=#()
$month_data =#($week_data1, $week_data2, $week_data3, $week_data4, $week_data5, $week_data6)
Loop through the array and count the week number that is being processed.
$i = 0
foreach($week in $month)
{ $i++
$n=0
Here I can write out a Variable and it concatenates properly.
**write-host '$week++ ='$week$i**
foreach($day in $week)
{$n++
write-host '$day ='$day
Pull in data from a .csv file to populate the custom object.
foreach($line in $csv)
{
if($line -match $day)
Match the line in the CSV file that has the correct Date in it. One line in the file per date in the month.
{ #write-host '$line.Day = ' $line.Day
# custom object to be used later
$date_data = [PSCustomObject] #{
week_numb = $i
date = $line.Day
attempts = $line.Attempts
connects = $line.Connects
}
I have tried different syntax versions but it does not work here? I want to put the custom object data into the new array for the week being processed.
#write-host '$week_data[$i]='$week_data[$i]
$week_data$i += $date_data # Add data from csv file into a
#$week_data[$i] += $date_data
}
}
}
}
Issue using $week_data$i as a variable I get an error:
At line:38 char:17
$week_data$i += $date_data # Add data from csv file into a
~~
Unexpected token '$i' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnexpectedToken
You're looking for variable indirection, i.e. the ability to refer to a variable indirectly, by a name stored in another variable or returned from an expression.
Note, however, that there are usually superior alternatives, such as using arrays or hashtables as multi-value containers - see this answer for an example.
If you do need to use variable indirection, use Get-Variable and Set-Variable:
$week_data1 = 'foo', 'bar'
$i = 1
# Same as: $week_data1
# Note that "$" must NOT be specified as part of the name.
Get-Variable "week_data$i" -ValueOnly
# Same as: $week_data1 = 'baz', 'quux'
Set-Variable "week_data$i" baz, quux
# Updating an existing value requires nesting the two calls:
# Same as: $week_data1 += 'quuz'
Set-Variable "week_data$i" ((Get-Variable "week_data$i" -ValueOnly) + 'quuz')
As an aside: "extending" an array with += is convenient, but inefficient: a new array must be created behind the scenes every time - see this answer.
Similarly, calling cmdlets to set and get variables performs poorly compared to direct assignments and variable references.
See this answer for applying the indirection technique analogously to environment variables, using Get-Content / Set-Content and the Env: drive.
As for what you tried:
$week_data$i = ... is an assignment expression, which is interpreted as directly juxtaposing two variables, $week_data and $i, which causes the syntax error you saw.
By contrast, something like Write-Output $week_data$i is a command, and while $week_data$i is also interpreted as two variable references, as a command argument it is syntactically valid, and would simply pass the (stringified) concatenation of the two variable values; in other words: $week_data$i acts as if it were double-quoted, i.e. an expandable string, and the command is therefore equivalent to Write-Output "$week_data$i"
Unrelated to the answer, but likely helpful for you, I have a function that will determine what week in a month a given date is.
Function Get-Week{
[cmdletbinding()]
param([parameter(ValueFromPipeline)][string[]]$Date)
process{
ForEach($Day in $Date){
$DTDay=[datetime]$Day
$Buffer = ([datetime]("{0}-01-{1}" -f $DTDay.Month,$DTDay.Year)).dayofweek.value__ -1
[math]::Truncate(($DTDay.Day+$Buffer)/7)+1
}
}
}
So you feed that a string that can be converted to a date like:
'5-13-2021' | Get-Week
or
Get-Week '5-13-2021'
and you get back a number indicating what week (ending on Saturday) of the month that day falls in.

Error using an if statement with the -AND conditional and 2 variables [duplicate]

Today (2017-05-29) I am using PowerShell 5.0.10586.117 on Windows 7 Enterprise and run the following (shortened):
$dateOfLicense = "2017-04-20"
$dateOfToday = '{0:yyyy-MM-dd}' -f (Get-Date)
$TimeDifference = [DateTime]$dateOfToday - [DateTime]$dateOfLicense
if (($TimeDifference) = 14)
{
Write-Host "test"
}
Even the difference between both days is 39, my code jumps in the if-clause and sends "test" to screen.
What am I doing wrong here?
You are assigning 14 to $TimeDifference. Instead you wan't to compare the Days property using -le:
if ($TimeDifference.Days -le 14)
{
Write-Host "test"
}
To complement Martin Brandl's helpful answer:
Like many other languages - but unlike VBScript, for instance - PowerShell uses distinct symbols for:
the assignment operator (=)
vs. the equality test operator (-eq).
This distinction enables using assignments as expressions, which is what you inadvertently did:
if ($TimeDifference = 14) ... # same as: if (($TimeDifference) = 14) ...
assigns 14 to variable $TimeDifference, as Martin explains, and, because the assignment is (of necessity, to serve as a conditional for if) enclosed in (...), returns the assigned value (the inner (...) around $TimeDifference make no difference here, however) and that value is used as the Boolean conditional for if.
That is, the (...) expression evaluated by if has value 14 - a nonzero number - and is therefore interpreted as $true in this Boolean context, irrespective of the original value of $TimeDifference.
Note:
To learn more about PowerShell's operators, run Get-Help about_Operators
To learn about how PowerShell interprets arbitrary values as Booleans in conditionals (to-Boolean coercion), see the bottom section of this answer.
To test variables or expressions that already are Booleans, just use them as-is or, if necessary, negate them with -not (!); e.g.:
if ($someBoolean) { # Better than: if ($someBoolean -eq $true)
if (-not $someBoolean) { # Better than: if ($someBoolean -eq $false)
Finally, here's a streamlined version of your code that doesn't require intermediate variables, uses a cast to convert the string to a [datetime] instance and uses [datetime]::now, the more efficient equivalent of Get-Date (though that will rarely matter).
if (([datetime]::now - [datetime] '2017-04-20').Days -eq 14) {
"test"
}
Note how "test" as a statement by itself implicitly sends output to PowerShell's (success) output stream, which prints to the console by default.
Write-Host bypasses this stream and should generally be avoided.
Not better solution of Martin, just an shorty code
$dateOfLicense = [DateTime]"2017-04-20"
$TimeDifferenceDays = ((Get-Date) - $dateOfLicense).Days
if ($TimeDifferenceDays -lt 14)
{
Write-Host "test"
}

DateTime subtraction not working in PowerShell - assignment vs. equality operator

Today (2017-05-29) I am using PowerShell 5.0.10586.117 on Windows 7 Enterprise and run the following (shortened):
$dateOfLicense = "2017-04-20"
$dateOfToday = '{0:yyyy-MM-dd}' -f (Get-Date)
$TimeDifference = [DateTime]$dateOfToday - [DateTime]$dateOfLicense
if (($TimeDifference) = 14)
{
Write-Host "test"
}
Even the difference between both days is 39, my code jumps in the if-clause and sends "test" to screen.
What am I doing wrong here?
You are assigning 14 to $TimeDifference. Instead you wan't to compare the Days property using -le:
if ($TimeDifference.Days -le 14)
{
Write-Host "test"
}
To complement Martin Brandl's helpful answer:
Like many other languages - but unlike VBScript, for instance - PowerShell uses distinct symbols for:
the assignment operator (=)
vs. the equality test operator (-eq).
This distinction enables using assignments as expressions, which is what you inadvertently did:
if ($TimeDifference = 14) ... # same as: if (($TimeDifference) = 14) ...
assigns 14 to variable $TimeDifference, as Martin explains, and, because the assignment is (of necessity, to serve as a conditional for if) enclosed in (...), returns the assigned value (the inner (...) around $TimeDifference make no difference here, however) and that value is used as the Boolean conditional for if.
That is, the (...) expression evaluated by if has value 14 - a nonzero number - and is therefore interpreted as $true in this Boolean context, irrespective of the original value of $TimeDifference.
Note:
To learn more about PowerShell's operators, run Get-Help about_Operators
To learn about how PowerShell interprets arbitrary values as Booleans in conditionals (to-Boolean coercion), see the bottom section of this answer.
To test variables or expressions that already are Booleans, just use them as-is or, if necessary, negate them with -not (!); e.g.:
if ($someBoolean) { # Better than: if ($someBoolean -eq $true)
if (-not $someBoolean) { # Better than: if ($someBoolean -eq $false)
Finally, here's a streamlined version of your code that doesn't require intermediate variables, uses a cast to convert the string to a [datetime] instance and uses [datetime]::now, the more efficient equivalent of Get-Date (though that will rarely matter).
if (([datetime]::now - [datetime] '2017-04-20').Days -eq 14) {
"test"
}
Note how "test" as a statement by itself implicitly sends output to PowerShell's (success) output stream, which prints to the console by default.
Write-Host bypasses this stream and should generally be avoided.
Not better solution of Martin, just an shorty code
$dateOfLicense = [DateTime]"2017-04-20"
$TimeDifferenceDays = ((Get-Date) - $dateOfLicense).Days
if ($TimeDifferenceDays -lt 14)
{
Write-Host "test"
}

Use PowerShell to divide two decimals

This seems easy, but after searching, I have come up empty-handed.
How can I divide two decimals in powershell?
204.50 / 1,917.75
Throws the following error:
Method invocation failed because [System.Object[]] does not contain a method named 'op_Division'.
At line:1 char:1
+ 204.50 / 1,917.75
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Division:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Try leaving the comma out. What you've typed looks like 204.50 divided by 1 and then 917.75
Walter Mitty's answer is an effective solution.
To explain why 204.50 / 1,917.75 failed:
PowerShell number literals (a) always use . as the decimal mark, irrespective of the current culture, and (b) do not support use of a thousands separator (such as ,).
This implies that 1,917.75 is not recognized as a number literal, which begs the question (kinda): how is it parsed?
, is the array-construction operator in PowerShell: that is, tokens separated by , constitute the elements of an array.
Thus, 1,917.75, is the equivalent of #( 1, 917.75 ): a 2-element array containing [int] 1 and [double] 917.75.
, has higher precedence than /, the division operator, so that 204.50 / 1,917.75 is the equivalent of:
204.50 / #( 1, 917.75 )
That is, PowerShell tries to divide [double] literal 204.50 by array #( 1, 917.75 )
Since PowerShell doesn't know how to divide anything by an array (generically represented in PowerShell as [System.Object[]]), you get the following error message:
Method invocation failed because [System.Object[]] does not contain a method named 'op_Division'.
That is, PowerShell looks for a way to apply division operator / (op_Division) to array operands, and since that isn't defined, an error occurs.
As an aside: PowerShell does overload some operators to work with arrays, but only if the array is on the LHS (the left-hand side operand).
Operators -eq / ne, -like / -notlike, -match / -notmatch (do let me know if I'm missing any) accept an array as the LHS and a scalar as the RHS, in which case the operator acts as a filter:
The operator is applied individually to the elements of the LHS (against the scalar on the RHS), and the subset of elements for which the operation returns $true is returned as a subarray of the input array; e.g.:
#( 'donald trump', 'hillary clinton', 'gary johnson' ) -notmatch 'trump'
outputs #( 'hillary clinton', 'gary johnson' ), the subarray of the input containing only those elements that do not contain the substring trump.