1

Using powershell, I'd like to print the result of splatting a hash table. This is an example I came up with (but the Write-Output doesn't work as I expect):

$SomePath = "C:\test\path"

$params = @{
    p = "$SomePath"
    r = "Foo"
    a = "Bar"
}

$destinationDir = ''

if (![string]::IsNullOrEmpty($destinationDir)) {
    $params['b'] = $destinationDir
}

Write-Output "Params: @params"

The result of Write-Output is literally:

Params: @params

Is there a way I can print the expansion of this command for debugging purposes? I'd also like to eventually store the expanded invocation to invoke later from a variable, like so (I assume it would work):

$cmd = "MyProgram.exe @params"
& "$cmd"

But if that's a whole new can of worms, I'm happy enough with getting just Write-Output to work in my initial example.

Here is my powershell version:

PS C:\> $PSVersionTable.PSVersion

Major  Minor  Patch  PreReleaseLabel BuildLabel
-----  -----  -----  --------------- ----------
7      0      3
void.pointer
  • 24,859
  • 31
  • 132
  • 243
  • You can't - the result of _splatting_ a hashtable depends on the parameter set it's bound to. – Mathias R. Jessen Oct 29 '20 at 18:22
  • Oops, used the wrong word didn't I? I fixed it. Do you have a link that explains the "depends on the parameter set its bound to" part? I'm still learning this and haven't seen that explained anywhere. Thank you. – void.pointer Oct 29 '20 at 18:55

2 Answers2

4

If you want to observe the effect splatting has on a Win32 PE in your current shell, I would suggest using Add-Type to compile a simple debugging utility:

$sourceCode = @'
using System;
using System.Management;
using System.Diagnostics;
public class Program
{
  public static void Main(string[] args)
  {
    using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + Process.GetCurrentProcess().Id))
    using (ManagementObjectCollection procs = searcher.Get())
    {
      foreach (ManagementBaseObject process in procs)
      {
        Console.WriteLine(process["CommandLine"].ToString());
      }
    }
  }
}
'@

Add-Type -TypeDefinition $sourceCode -OutputType ConsoleApplication -OutputAssembly ~\printCmdline.exe

The resulting executable will print the command line arguments of its own process - exactly what we need to see.


Now that we have an executable to test against, let's see what happens:

$SomePath = "C:\test\path"

$params = @{
    p = "$SomePath"
    r = "Foo"
    a = "Bar"
}

~\printCmdline.exe @params

Which should print something like:

"C:\Users\void.pointer\printCmdline.exe" -r:Foo -p:C:\test\path -a:Bar
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
2

Note: Splatting only works reliably with PowerShell commands, because the way PowerShell translates it for external programs, as shown in Mathias' answer, is not a format that most external programs understand.

As Mathias points out in a comment on the question, you cannot directly get a splatting invocation's equivalent expressed as individual arguments.
However, you can use the Trace-Command cmdlet to trace parameter binding, IF you have a command (represented by $yourCommand below) that actually accepts the parameters specified in the hashtable:

Trace-Command -pshost -name ParameterBinding { & $yourCommand @params }

Note: Trace-Command's output is verbose and technical; interpreting it takes some practice.


Your desire to capture the equivalent of a splatting-based invocation in a command-line string is impossible to achieve in all cases, given the object-based nature of PowerShell: not all objects can be represented faithfully as strings.

That said, if you know that the arguments being passed are limited to strings, numbers, and switches, you can do the following:

# Sample hashtable used for splatting, limited to strings, numbers, switches.
$somePath = 'C:\test\path'
$params = [ordered] @{
    Path = "$SomePath" # string
    Count = 42         # number
    Skip = $True       # [switch] parameter, expressed as a [bool] value in splatting
}

# Translate the hashtable into an equivalent command-line string.
$params.GetEnumerator().ForEach({
  $val = $_.Value
  if ($val -is [string]) { $val = "'{0}'" -f $val.Replace("'", "''") }
  '-{0}:{1}' -f $_.Key, $val
}) -join ' '

The above yields a string with the following content:

-Path:'C:\test\path' -Count:42 -Skip:True 

Prepending the command name to the string resulting from the above would give you a string representation of a complete command.

That string representation could be saved to a script file, for instance.

If you wanted to execute the string containing the command-line directly, you'd have to resort to Invoke-Expression, because &, the call operator, does not operate on entire command lines, only on the names/paths of commands specified via a variable, quoted string, or expression.

The obligatory warning about Invoke-Expression:

Invoke-Expression should generally be avoided and used only as a last resort, due to its inherent security risks. In short: Avoid it, if possible, given that superior alternatives are usually available. If there truly is no alternative, only ever use it on input you either provided yourself or fully trust - see this answer.

In general, if possible, use PowerShell's own, fully object-aware way of defining snippets of code to be executed on demand later, namely script blocks ({ ... }).

mklement0
  • 382,024
  • 64
  • 607
  • 775