Public/ConvertTo-Splatting.ps1
Function ConvertTo-Splatting { <# .SYNOPSIS Use to convert an existing PowerShell command to splatting .DESCRIPTION Splatting is a much cleaner and safer way to shorten command lines without needing to use backtick. This function excepts any command as a string or a scriptblock and will convert the existing parameters to a hashtable and output the fully splatted command for you. .PARAMETER Command The command string you want to convert to using splatting .PARAMETER ScriptBlock The command scriptblock you want to convert to using splatting .EXAMPLE $splatme = @' Set-AzVMExtension -ExtensionName "MicrosoftMonitoringAgent" -ResourceGroupName "rg-xxxx" -VMName "vm-xxxx" -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType "MicrosoftMonitoringAgent" -TypeHandlerVersion "1.0" -Settings @{"workspaceId" = "xxxx" } -ProtectedSettings @{"workspaceKey" = "xxxx"} -Location "uksouth" '@ ConvertTo-Splatting $splatme Converts the string splatme to splatting --- Output ---- $SetAzVMExtensionParam = @{ ExtensionName = "MicrosoftMonitoringAgent" ResourceGroupName = "rg-xxxx" VMName = "vm-xxxx" Publisher = "Microsoft.EnterpriseCloud.Monitoring" ExtensionType = "MicrosoftMonitoringAgent" TypeHandlerVersion = "1.0" Settings = @{ "workspaceId" = "xxxx" } ProtectedSettings = @{ "workspaceKey" = "xxxx" } Location = "uksouth" } Set-AzVMExtension @SetAzVMExtensionParam .EXAMPLE $splatme = { Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf } ConvertTo-Splatting $splatme Converts the scriptblock splatme to splatting --- Output ---- $CopyItemParam = @{ Path = "test.txt" Destination = "test2.txt" WhatIf = $true } Copy-Item @CopyItemParam .EXAMPLE $splatme = { Get-AzVM ` -ResourceGroupName "ResourceGroup11" ` -Name "VirtualMachine07" ` -Status } ConvertTo-Splatting $splatme Removed backticks and converts the scriptblock splatme to splatting --- Output ---- $GetAzVMParam = @{ ResourceGroupName = "ResourceGroup11" Name = "VirtualMachine07" Status = $true } Get-AzVM @GetAzVMParam .NOTES about_Splatting - https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting #> [CmdletBinding()] param( [Parameter(ParameterSetName = 'string', Position = 0)] [string] $Command, [Parameter(ParameterSetName = 'scriptblock', Position = 0)] [ScriptBlock] $ScriptBlock ) # Convert scriptblock to string for parsing if ($PSCmdlet.ParameterSetName -eq 'scriptblock') { $Command = $ScriptBlock.ToString().Trim() } # remove backticks $ScriptBlockAst = [regex]::Replace($Command, '(`\r\n|`\r|`\n)', ' ') # Parse the script block input $Errors = @() $ref = @() [void][System.Management.Automation.Language.Parser]::ParseInput($ScriptBlockAst, [ref]$ref, [ref]$Errors) # Get the command and any variables it is being set to [System.Collections.Generic.List[string]]$ParsedCommand = @() $index = $ref | Where-Object { $_.TokenFlags -eq 'CommandName' } | Select-Object -First 1 for ($i = 0; $i -le $ref.IndexOf($index); $i++) { if ($ref[$i].TokenFlags -notin 'None', 'AssignmentOperator', 'CommandName') { throw "Command should only start with a cmdlet or variable declaration '$($ref[$i].TokenFlags)'" } $ParsedCommand.Add($ref[$i].Text) } # set a name to make the hashtable variable $hash = $index.Value.Replace('-', '') + 'Param' $constants = (Get-Variable | Where-Object { $_.Options -match 'Constant' }).Name if ($ref[0].Name -eq $hash -or $hash -in $constants) { $hash = 'parameterSplat' } # Get all the parameters and their values [System.Collections.Generic.List[PSObject]]$parameters = @() $parametersExpected = $true $LParen = 0 for ($i = $ParsedCommand.Count; $i -lt $ref.Count; $i++) { if ($ref[$i].Kind -eq 'EndOfInput') { $parameters[-1].End = $i } elseif ($LParen -eq 0 -and $ref[$i].Kind -eq 'Parameter') { # default value to $true to account for switches if ($parameters.Count -gt 0) { $parameters[-1].End = $i } [System.Collections.Generic.List[PSObject]] $Value = @() $parameters.Add([pscustomobject]@{Name = $ref[$i].ParameterName; Value = $Value; Start = $i + 1; End = 0 }) $parametersExpected = $false } elseif ($ref[$i].Kind -in 'RParen', 'RCurly') { $LParen-- } elseif ($LParen -eq 0 -and $parametersExpected -eq $true) { # if parameter was expected but not passed get the parameter name based on the position try { $ParsedCommandData = Get-Command -Name $index.Value -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId -eq 'CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand') { throw "Unable to find parameters for the command '$($index.Value)' ensure all modules and functions are loaded before retrying." } break } # using alias get the actual command resolves if ($ParsedCommandData.CommandType -eq 'Alias') { $ParsedCommandData = Get-Command -Name $ParsedCommandData.ResolvedCommand } # get the default Parameters if ($ParsedCommandData.ParameterSets.Count -gt 1) { $ParameterSet = foreach ($set in $ParsedCommandData.ParameterSets) { $setParameters = $set.Parameters | Foreach-Object { $_.Name; $_.Aliases } if ($parameters | Where-Object { $_.Name -notin $setParameters }) { # Do nothing } else { $set } } if ($ParameterSet.Count -gt 1) { $ParameterSet = $ParameterSet | Where-Object { $_.IsDefault } if ($ParameterSet.Count -gt 1) { $ParameterSet = $ParameterSet[0] } } if (-not $ParameterSet) { $ParameterSet = $ParsedCommandData.ParameterSets | Where-Object { $_.IsDefault } } if (-not $ParameterSet) { $ParameterSet = $ParsedCommandData.ParameterSets[0] } } else { $ParameterSet = $ParsedCommandData.ParameterSets } # Get the parameter name based on the position number $ParameterName = ($ParameterSet.Parameters | Where-Object { $_.Position -eq $parameters.Count }).Name # if the position does not match, get the parameter from the array value if ([string]::IsNullOrEmpty($ParameterName)) { $ParameterName = ($ParameterSet.Parameters[$parameters.Count]).Name } if ($parameters.Count -gt 0) { $parameters[-1].End = $i } [System.Collections.Generic.List[PSObject]] $Value = @() $parameters.Add([pscustomobject]@{Name = $ParameterName; Value = $Value; Start = $i; End = 0 }) } elseif ($LParen -eq 0 -and $ref[$i].Kind -notin 'Dot', 'LParen', 'DollarParen', 'AtCurly') { $parametersExpected = $true } Write-Debug "$LParen $parametersExpected - $($ref[$i].Text) - $($ref[$i].Kind)" if ($ref[$i].Kind -in 'LParen', 'DollarParen', 'AtCurly') { $LParen++ } elseif ($ref[$i + 1].Kind -eq 'Dot') { $parametersExpected = $false } } foreach ($p in $parameters) { for ($i = $p.Start; $i -lt $p.End; $i++) { Write-Debug "$($ref[$i].Text) - $($ref[$i].Kind)" if ($ref[$i].Kind -in 'Identifier', 'Generic' -and $ref[$i].TokenFlags -ne 'CommandName') { # check if the value is in quotes and add them if not if ($ref[$i].Text[0] -notin '"', "'" -and ($p.End - $p.Start) -eq 1) { $p.Value.Add("'$($ref[$i].Text.Replace("'","''"))'") } else { $p.Value.Add($ref[$i].Text) } } elseif ($ref[$i].Kind -notin 'EndOfInput', 'Parameter') { $p.Value.Add($ref[$i].Text) } } } $parameters | Where-Object { $_.Start -eq $_.End -and $_.Value.Count -eq 0 } | ForEach-Object { $_.Value.Add('$true') } $spacing = ($parameters.Name | Measure-Object -Maximum -Property Length).Maximum $spacing++ [System.Collections.Generic.List[PSObject]] $output = @() # build the output $output.Add("`$$($hash) = @{") $parameters | ForEach-Object { $output.Add(("`t$($_.Name)$(' '*$($spacing - $_.Name.Length))= $($_.Value -join(' '))").Replace(' . ', '.')) } $output.Add("}") # if there were pipelines add them back in if ($Split.Count -gt 1) { $pipes = $Split[1..($Split.Length - 1)] $output.Add((@("$($ParsedCommand) @$($hash)") + @($pipes)) -join (' | ')) } else { $output.Add("$($ParsedCommand) @$($hash)") } $return = [SplatBlock]@{ Command = $index.Value IsDefault = $null HashBlock = ($output -join ("`n")) SetBlock = $null } $return.HashBlock | Set-Clipboard $return } |