Home > Mobile >  String concatenation adds space when using a member of a Class that is not added when using a variab
String concatenation adds space when using a member of a Class that is not added when using a variab

Time:04-21

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 to write-host "$($a)bbb" which gets output as aaabbb.

  • write-host $b.a"bbb" is equivalent to write-host @( $b.a, "bbb" ) which gets output as aaa 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.

  • Related