Search code examples
powershellexpressiondeserializationunroll

How can I prevent a (serialized) expression to unroll


Thanks to the PowerShell expression mode, PowerShell has some nice ways to de-serialize objects, like:

My general expectation is that the result of an given expression should be the same as using one of above listed de-serialize commands on same serialized version of that expression (for background, see the ConvertTo-Expression answer on the 'Save hash table in PowerShell object notation (PSON)' question).
In other words:

<Expression> <=> Invoke-Command {<Expression>} <=> &([ScriptBlock]::Create('<Expression>'))
<Expression> <=> Invoke-Expression '<Expression>'

Examples:

Get-ChildItem <=> &{Get-ChildItem}
Get-ChildItem <=> Invoke-Command {Get-ChildItem}
Get-ChildItem <=> Invoke-Expression 'Get-ChildItem'

1, 2, 3 <=> &{1, 2, 3}
1, 2, 3 <=> Invoke-Command {1, 2, 3}
1, 2, 3 <=> Invoke-Expression '1, 2, 3'

This indeed appears true for mainly every expression, but due to the fact that PowerShell unrolls (enumerates) the output by default, this definition deviates in the case an expression contain an array with a single item:

,1 <≠> Invoke-Command {,1}
,1 <≠> Invoke-Expression ',1'
,"Test" <≠> Invoke-Command {,"Test"}
,"Test" <≠> Invoke-Expression ',"Test"'
@("Test") <≠> Invoke-Command {@("Test")}
@("Test") <≠> Invoke-Expression '@("Test")'
,@("Test") <≠> Invoke-Command {,@("Test")}
,@("Test") <≠> Invoke-Expression ',@("Test")'

Is there a way to prevent that expressions get unrolled when the are invoked (de-serialized) in anyway?

I am considering to request for a -NoEnumerate parameter (similar to the Write-Output cmdlet) for the Invoke-Expression on the PowerShell GitHub, but that will still leave the issue/question for the call operator and dot sourcing that do not support parameters...


Solution

  • I found a workaround to prevent the default unrolling of (serialized) expressions:
    Wrap the expression in a HashTable:

    <Expression> <=> (Invoke-Command {@{e=<Expression>}})['e']
    <Expression> <=> (Invoke-Expression '@{e=<Expression>}')['e']
    

    Examples:

    Get-ChildItem <=> (Invoke-Command {@{e=Get-ChildItem}})['e']
    Get-ChildItem <=> (Invoke-Expression '@{e=Get-ChildItem}')['e']
    
    1, 2, 3 <=> (Invoke-Command {@{e=1, 2, 3}})['e']
    1, 2, 3 <=> (Invoke-Expression '@{e=1, 2, 3}')['e']
    
    ,1 <=> (Invoke-Command {@{e=,1}})['e']
    ,1 <=> (Invoke-Expression '@{e=,1}')['e']
    

    I have further implemented this in a ConvertFrom-Expression cmdlet, which the following features:

    • -NoEnumerate switch to prevent arrays with a single item to unroll
    • -NoNewScope switch similar to Invoke-Command
    • Multiple ScriptBlock and/or String items via the pipeline or the -Expression argument

    ConvertFrom-Expression Examples:

    PS C:>'2*3', {3*4}, '"Test"' | ConvertFrom-Expression
    6
    12
    Test
    

     

    PS C:> (ConvertFrom-Expression ',"Test"' -NoEnumerate) -Is [Array]
    True