powershellmetaprogrammingsplat

How to pass the 'argument-line' of one PowerShell function to another?


I'm trying to write some PowerShell functions that do some stuff and then transparently call through to existing built-in functions. I want to pass along all the arguments untouched. I don't want to have to know any details of the arguments.

I tired using 'splat' to do this with @args but that didn't work as I expected.

In the example below, I've written a toy function called myls which supposed to print hello! and then call the same built-in function, Get-ChildItem, that the built-in alias ls calls with the rest of the argument line intact. What I have so far works pretty well:

function myls
{
  Write-Output "hello!"
# $MyInvocation | Format-List          # <-- uncomment this line for debug info
  Invoke-Expression ("Get-ChildItem " + $MyInvocation.UnboundArguments -join " ")
}

A correct version of myls should be able to handle being called with no arguments, with one argument, with named arguments, from a line containing multiple semi-colon delimited commands, and with variables in the arguments including string variables containing spaces. Basically, it should be a drop-in alternative to ls.

The tests below compare myls and the builtin ls:

[NOTE: output elided and/or compacted to save space]

PS> md C:\p\d\x, C:\p\d\y, C:\p\d\"jay z"
PS> cd C:\p\d
PS> ls                                 # no args
PS> myls                               # pass
PS> cd ..
PS> ls d                               # one arg
PS> myls d                             # pass
PS> $a="A"; $z="Z"; $y="y"; $jz="jay z"
PS> $a; ls d; $z                       # multiple statements
PS> $a; myls d; $z                     # pass
PS> $a; ls d -Exclude x; $z            # named args
PS> $a; myls d -Exclude x; $z          # pass
PS> $a; ls d -Exclude $y; $z           # variables in arg-line
PS> $a; myls d -Exclude $y; $z         # pass
PS> $a; ls d -Exclude $jz; $z          # variables containing spaces in arg-line
PS> $a; myls d -Exclude $jz; $z        # FAIL!

Is there a way I can re-write myls to get the behavior I want?

Short answer: Yes, it's possible. The bad news: it requires code which knows details of the parameters and other metadata about the function one wishes to call through to. The good news: one doesn't need to write this all oneself. This metadata is available programatically and there exist modules available which one can use to auto-generate skeleton proxy code (see @Jaykul's answer below). I choose to use the module named "MetaProgramming". Once imported, generating a drop-in myls script is dead simple:

New-ProxyCommand ls > .\myls.ps1

Then one can start customizing the newly-generated myls.ps1 script, like this:

  ...
  begin
  {
    Write-Output "hello!"              # <-- add this line
    try {
      $outBuffer = $null
  ...

Voila! This new version passes all the tests.


Solution

  • If you want a drop-in wrapper for ls, you should write a proper Proxy Command. There are a couple of versions of the generator on PoshCode.org, including the one from Lee Holmes' PowerShell Cookbook,

    But the proxy command generator is built in now, so you can just write:

    $CommandName = "Get-ChildItem"
    $Command = Get-Command $CommandName
    [System.Management.Automation.ProxyCommand]::Create($Command)