I found a bit of PowerShell code from Microsoft using string concatenation like $variable".Name". I adapted this code to be used inside a method of a class and found a weird behavior, shown by this bit of code.
Class A
{
[string]$a = "A"
[void]Test()
{
$b = $this.a
Write-Host $b"B"
Write-Host $this.a"B"
}
}
$c = [A]::new()
$c.Test()
The first expression returns a string with no spaces between A and B, the second one instead has a space between them.
AB
A B
I solved the issue by using "$($this.a)B", but I'm curious to know the reason for this difference.
CodePudding user response:
After a bit of research, I think it's a combination of how PowerShell parses $a"bbb"
vs $b.a"bbb"
and the ValueFromRemainingArguments
attribute property on the -Output
parameter of Write-Host
Basically:
write-host $a"bbb"
is equivalent towrite-host "$($a)bbb"
which gets output asaaabbb
.write-host $b.a"bbb"
is equivalent towrite-host @( $b.a, "bbb" )
which gets output asaaa bbb
.
Long Version
You can see the difference in how PowerShell parses the expressions if you inspect the Ast created for the following scriptblocks:
Example 1
$s1 = {
$a = "aaa";
write-host $a"bbb";
}
$s1.Ast.EndBlock.Statements[1].PipelineElements[0].CommandElements
gives this output:
StringConstantType : BareWord
Value : write-host
StaticType : System.String
Extent : write-host
Parent : write-host $a"bbb"
Value : $abbb
StringConstantType : BareWord
NestedExpressions : {$a}
StaticType : System.String
Extent : $a"bbb"
Parent : write-host $a"bbb"
This is treating write-host $a"bbb"
as equivalent to write-host "$($a)bbb"
(see the NestedExpressions
) and ends up writing aaabbb
to the console if you invoke it.
Example 2
$s2 = {
$b = [pscustomobject] @{ "a" = "aaa" };
write-host $b.a"bbb";
}
$s2.Ast.EndBlock.Statements[1].PipelineElements[0].CommandElements
gives the output:
StringConstantType : BareWord
Value : write-host
StaticType : System.String
Extent : write-host
Parent : write-host $b.a"bbb"
Expression : $b
Member : a
Static : False
NullConditional : False
StaticType : System.Object
Extent : $b.a
Parent : write-host $b.a"bbb"
StringConstantType : DoubleQuoted
Value : bbb
StaticType : System.String
Extent : "bbb"
Parent : write-host $b.a"bbb"
which means write-host $b.a"bbb"
is equivalent to write-host $b.a "bbb"
, which outputs aaa bbb
However, we're not quite done...
Get-Help Write-Host
If you call Get-Help Write-Host
you'll see output like this:
Write-Host [[-Object] <Object>] ...
but if we mirror that signature in a custom function:
function x
{
param
(
[Object] $Object
)
write-host $Object.GetType().FullName
write-host $Object
}
"Example 1"
x $a"bbb"
"Example 2"
x $b.a"bbb"
we get this output:
Example 1
System.String
aaabbb
Example 2
System.String
aaa
In the second example the "bbb"
is being discarded in the function call because there's no parameter for this second value to be bound to, so there must be something else going on with Write-Host
as well...
Get-Help Write-Host -Full
If we call Get-Help Write-Host -Full
we'll see some additional information about the parameters to the cmdlet and it shows us the -Output
parameter is decorated with the FromRemainingArguments
property:
-Object <Object>
Required? false
Position? 0
Accept pipeline input? true (ByValue, FromRemainingArguments)
Parameter set name (All)
Aliases Msg, Message
Dynamic? false
Accept wildcard characters? false
So if we copy that in our own function we get this:
function x
{
param
(
[Parameter(ValueFromRemainingArguments=$true)]
[Object] $Object
)
write-host $Object.GetType().FullName
write-host $Object.Count
write-host ($Object | fl * | out-string)
write-host $Object
}
and now any unbound parameters get bundled into our $Object
:
"Example 1"
x $aaa"bbb"
"Example 2"
x $a.b"bbb"
gives:
Example 1
System.Collections.Generic.List`1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
1
aaabbb
aaabbb
System.Collections.Generic.List`1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
2
aaa
bbb
aaa bbb
We can see the first example passes a generic list containing a single string which write-host
simply echoes out verbatim, whereas the second example any unbound values are bundled up into -Output
which gives a list with two values in it, and the default serialisation for enumerable types in PowerShell joins the items with a space for a separator.