Search code examples
powershellside-effects

Undeterminable return sideeffect in Powershell


Given the following snipped with input $value='Line\"\nString' and $value_type='Interpolated'.

Note: $value contains literal escape sequences, not escaped characters.

The following variables are produced:

  • [string] $value='Line\"\nString'
  • [string] $value3='Line" String' where the space is a newline character.
  • [Object[13]] $value2=['Line"', ..., '"Line"']
  • $value2[0].GetType() = 'System.Text.StringBuilder'
        function _unescape([string] $value, [string] $value_type) {
            function _unescape_core([string] $value) {
                # this should really be part of the dotnet runtime, somewhere...
                # Url decode incorrectly decodes some characters, e.g. % = %25
                # Regex decode does not decode all characters
                $sb = [System.Text.StringBuilder]::new($value.Length)
                $last_is_bs = $false
                for ($pos = 0; $pos -lt $value.Length; $pos += 1) {
                    [char] $c = $value[$pos]
                    if ($last_is_bs) {
                        switch ($c) {
                            'a' { $sb.Append([char]0x07) }
                            'b' { $sb.Append([char]0x08) }
                            't' { $sb.Append([char]0x09) }
                            'n' { $sb.Append([char]0x0a) }
                            'v' { $sb.Append([char]0x0b) }
                            'f' { $sb.Append([char]0x0c) }
                            'r' { $sb.Append([char]0x0d) }
                            'x' {
                                $pos += 2
                                if ($pos -ge $value.Length) {
                                    throw "Invalid escape sequence: \x at end of string"
                                }
                                # hex escape sequence
                                $hex = $value.SubString($sb.Length + 1, 2)
                                $hex = [int]('0x' + $hex)
                                $sb.Append([char]$hex)
                            }
                            'u' {
                                $pos += 4
                                if ($pos -ge $value.Length) {
                                    throw "Invalid escape sequence: \u at end of string"
                                }
                                # unicode escape sequence
                                $hex = $value.SubString($sb.Length + 1, 4)
                                $hex = [int]('0x' + $hex)
                                $sb.Append([char]$hex)
                            }
                            'U' {
                                $pos += 8
                                if ($pos -ge $value.Length) {
                                    throw "Invalid escape sequence: \U at end of string"
                                }
                                # unicode escape sequence
                                $hex = $value.SubString($sb.Length + 1, 8)
                                $hex = [int]('0x' + $hex)
                                # add the surrogate pair
                                $sb.Append([System.Char]::ConvertFromUtf32($hex))
                            }
                            default {
                                $sb.Append($c)
                            }
                        }
                        $last_is_bs = $false
                    }
                    else {
                        if ($c -eq '\') {
                            $last_is_bs = $true
                        }
                        else {
                            $sb.Append($c)
                        }
                    }
                }
                if ($last_is_bs) {
                    throw "Invalid escape sequence: \ at end of string"
                }
                [string] $value3 = $sb.ToString()
                return [string]$value3
            }

            if ($value_type -ne 'Interpolated') {
                return $value
            }
            $value2 = (_unescape_core $value)
            return $value2
        }

for a more minimal working sample the entire for loop can be replaced by

[char] $c = $value[$pos]
sb.Append($c)

The result is not the same, but simmilar.

The length of the array of stringbuilders is equal to the number of characters returned. So 13 in the first, and 15 in the later example.

When does the implicit conversion of a string, to an array of 13 StringBuilders occur? How do I prevent this conversion? Can anyone explain this effect to me? I think I am going mad staring at the debugger and googling for hours.

A bit of background: I am trying to actually learn PowerShell as a language and thought this is would be a fun learning experience...

Thanks in advance for all your help!


Solution

  • Answer was provided in comments but to give it closure, all append methods in the StringBuilder class return the instance itself, basically this is to allow method chaining, for instance:

    [System.Text.StringBuilder]::new().
        AppendLine('foo').
        AppendLine('bar').
        AppendLine('baz').
        ToString()
    

    This can produce unexpected results in PowerShell, as you have encountered, the output from the method calls became part of your function's output. See this excellent answer for details.

    $output = & {
        $sb = [System.Text.StringBuilder]::new()
        $sb.Append('foo')
        $sb.ToString()
    }
    
    $output
    
    # Capacity MaxCapacity Length   <= Output from the StringBuilder
    # -------- ----------- ------
    #       16  2147483647      3
    # foo                           <= The actual output we wanted from this Script Block
    

    The solution is to simply redirect or capture the method output, i.e.:

    $sb.Append([char]0x07)
    

    Should be:

    $sb = $sb.Append([char]0x07)
    
    # or assign to `$null`
    $null = $sb.Append([char]0x07)
    
    # or redirect to `$null`
    $sb.Append([char]0x07) > $null
    
    # or cast to `void`
    [void] $sb.Append([char]0x07)
    
    # or pipe to `Out-Null` (not recommended in Windows PowerShell 5.1)
    $sb.Append([char]0x07) | Out-Null