Public/ConvertTo-SplatExpression.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
using namespace Microsoft.PowerShell.EditorServices.Extensions
using namespace System.Collections.Generic
using namespace System.Management.Automation.Language

function ConvertTo-SplatExpression {
    <#
    .EXTERNALHELP EditorServicesCommandSuite-help.xml
    #>

    [CmdletBinding()]
    [EditorCommand(DisplayName='Convert Command to Splat Expression')]
    param(
        [System.Management.Automation.Language.Ast]
        $Ast
    )
    begin {
        function ConvertFromExpressionAst($expression) {
            $isStringExpression = $expression -is [StringConstantExpressionAst] -or
                                  $expression -is [ExpandableStringExpressionAst]

            if ($isStringExpression) {
                # If kind isn't BareWord then it's already enclosed in quotes.
                if ('BareWord' -ne $expression.StringConstantType) {
                    return $expression.Extent.Text
                }
                $enclosure = "'"
                if ($expression.NestedExpressions) {
                    $enclosure = '"'
                }

                return '{0}{1}{0}' -f $enclosure, $expression.Value
            }
            # When we handle switch parameters we don't create an AST.
            if ($pair.Value -isnot [Ast]) {
                return $expression
            }

            return $expression.Extent.Text
        }
    }
    end {
        $Ast = GetAncestorOrThrow $Ast -AstTypeName CommandAst -ErrorContext $PSCmdlet

        $commandName, $elements = $Ast.CommandElements.Where({ $true }, 'Split', 1)

        $splat           = @{}
        $retainedArgs    = [List[Ast]]::new()
        $elementsExtent  = $elements.Extent | Join-ScriptExtent
        $boundParameters = [StaticParameterBinder]::BindCommand($Ast).BoundParameters

        # Start building the hash table of named parameters and values
        foreach ($parameter in $boundParameters.GetEnumerator()) {
            # If the command isn't loaded positional parameters come through as their numeric position.
            if ($parameter.Key -match '\d+' -and -not $parameter.Value.Parameter) {
                $retainedArgs.Add($parameter.Value.Value)
                continue
            }
            # The "Value" property for switches is the parameter AST (e.g. -Force) so we need to
            # manually build the expression.
            if ($parameter.Value.ConstantValue -is [bool]) {
                $splat.($parameter.Key) = '${0}' -f $parameter.Value.ConstantValue.ToString().ToLower()
                continue
            }
            $splat.($parameter.Key) = $parameter.Value.Value
        }

        # Remove the hypen, change to camelCase and add 'Splat'
        $variableName = [regex]::Replace(
            ($commandName.Extent.Text -replace '-'),
            '^[A-Z]',
            { $args[0].Value.ToLower() }) +
            'Splat'

        $sb = [System.Text.StringBuilder]::
            new('${0}' -f $variableName).
            AppendLine(' = @{')

        # All StringBuilder methods return itself so it can be chained. We null the whole scriptblock
        # here so unchained method calls don't add to our output.
        $null = & {
            foreach($pair in $splat.GetEnumerator()) {
                $sb.Append(' ').
                    Append($pair.Key).
                    Append(' = ')
                if ($pair.Value -is [ArrayLiteralAst]) {
                    $sb.AppendLine($pair.Value.Elements.ForEach{
                        ConvertFromExpressionAst $PSItem
                    } -join ', ')
                } else {
                    $sb.AppendLine((ConvertFromExpressionAst $pair.Value))
                }
            }
            $sb.Append('}')
        }
        $splatText = $sb.ToString()

        # New CommandAst will be `Command @splatvar [PositionalArguments]`
        $newCommandParameters = '@' + $variableName
        if ($retainedArgs) {
            $newCommandParameters += ' ' + ($retainedArgs.Extent.Text -join ' ')
        }

        # Change the command expression first so we don't need to track it's position.
        $elementsExtent | Set-ScriptExtent -Text $newCommandParameters

        # Get the parent PipelineAst so we don't add the splat in the middle of a pipeline.
        $pipeline = $Ast | Find-Ast -Ancestor -First { $PSItem -is [PipelineAst] }

        # Prepend the existing indent.
        $lineText = ($psEditor.GetEditorContext().
            CurrentFile.
            Ast.
            Extent.
            Text -split '\r?\n')[$pipeline.Extent.StartLineNumber - 1]

        $lineIndent  = $lineText -match '^\s*' | ForEach-Object { $matches[0] }
        $splatText   = $lineIndent + (
                       $splatText -split '\r?\n' -join ([Environment]::NewLine + $lineIndent))

        # HACK: Temporary workaround until https://github.com/PowerShell/PowerShellEditorServices/pull/541
        #$splatTarget = ConvertTo-ScriptExtent -Line $pipeline.Extent.StartLineNumber
        $splatTarget = [Microsoft.PowerShell.EditorServices.FullScriptExtent]::new(
            $psEditor.GetEditorContext().CurrentFile,
            [Microsoft.PowerShell.EditorServices.BufferRange]::new(
                $pipeline.Extent.StartLineNumber,
                1,
                $pipeline.Extent.StartLineNumber,
                1))

        $splatTarget | Set-ScriptExtent -Text ($splatText + [Environment]::NewLine)
    }
}