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

3

13

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.

Raffo answered 15/1, 2011 at 22:7 Comment(0)
G
4

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)
Georganngeorge answered 17/1, 2011 at 3:46 Comment(2)
In other words: automated copy-pasting. Just astounding.Could
If you could copy-paste, you wouldn't need code-generation. What the OP is doing is trying to subclass the command: writing a new command based on the original, but with custom modifications ... and in script. If you were doing it in C#, you'd just inherit. But you're in PowerShell, so you have to write a function which starts by duplicating the set of parameters from the original cmdlet (in PowerShell, we call those proxy functions). If there was ever anything that cried for code-generation, this is it.Georganngeorge
S
4

Making a wrapper properly is not that easy, unfortunately. First of all, the splat operator should presumably be applied to a hashtable (e.g. automatic PSBoundParameters or another), not to the array $args directly.

There are at least two options (both are not perfect), and a hack (see the update section).

Option 1. Use the "classic" way of making a wrapper. Example: see how this is done for the function help which is a wrapper of the Get-Help cmdlet:

Get-Content function:\help

You can see that the wrapper function declarers all the parameters that the wrapped cmdlet has in order to have that PSBoundParameters bounded to the existing function parameters.

Your example. If your function declares the parameter Exclude then the example code starts to work. But it works for Exclude only, not Force, though Force is also passed in:

function myls($Exclude) {
    # only Exclude is in $PSBoundParameters even though we send Force, too:
    $PSBoundParameters | Out-String | Out-Host
    Write-Output "hello!"
    Get-ChildItem @PSBoundParameters
}
cd d
myls -Exclude b -Force

Option 2. In theory in should be possible to build a hashtable from the array $args manually and apply the splat operator to it. But this task does not look practically attractive.


UPDATE

Well, there is actually yet another ad-hoc option (pure hack!) that requires minimum effort and will work in some trivial, mostly interactive, scenarios. It is not for somehow serious code!

function myls {
    # extra job
    Write-Output "hello!"

    # invoke/repeat the calling code line with myls "replaced" with Get-ChildItem
    Set-Alias myls Get-ChildItem
    Invoke-Expression $MyInvocation.Line
}

cd d

# variables can be used as the parameter values, too
$exclude = 'b'

myls -Exclude $exclude -Force
Serenity answered 16/1, 2011 at 9:58 Comment(5)
The hack works and it has the advantage that one doesn't need to handle (or even know about) all the various optional parameters.Raffo
Well, it works but not well enough for real use. $MyInvocation.Line is the whole calling line including other statements, too. As a result, this doesn't work: $result = myls .... But this seems to work: myls ... -OutVariable result.Serenity
Yeah, I noticed that too. There must be a fix. I'll let you know if I find one.Raffo
I found another solution: Invoke-Expression ("Get-ChildItem " + $MyInvocation.UnboundArguments -join " ") Also it doesn't require the alias hack. It doesn't handle param values (e.g., myls -Exclude $exclude -Force) though.Raffo
I think $MyInvocation.UnboundArguments is the same as $args or am I wrong?Serenity
G
4

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)
Georganngeorge answered 17/1, 2011 at 3:46 Comment(2)
In other words: automated copy-pasting. Just astounding.Could
If you could copy-paste, you wouldn't need code-generation. What the OP is doing is trying to subclass the command: writing a new command based on the original, but with custom modifications ... and in script. If you were doing it in C#, you'd just inherit. But you're in PowerShell, so you have to write a function which starts by duplicating the set of parameters from the original cmdlet (in PowerShell, we call those proxy functions). If there was ever anything that cried for code-generation, this is it.Georganngeorge
E
2

I believe this:

function myls { Write-Output "hello!"; iex "Get-ChildItem @args"}

will come closer to producing the expected result.

Update: There is apparently a known bug with using @args in this manner to pass named parameters:

https://connect.microsoft.com/PowerShell/feedback/details/368512/splat-operator-args-doesnt-properly-pass-named-arguments?wa=wsignin1.0

I'd discontinue using that until it's resolved.

Eschalot answered 16/1, 2011 at 5:26 Comment(2)
It's closer -- it doesn't produce an error -- unfortunately it also ignores the args. If you try the test above, the outputs of myls and myls -Exclude b are, incorrectly, the same.Raffo
Try it without that first line.Eschalot

© 2022 - 2024 — McMap. All rights reserved.