0

Following the examples in https://stackoverflow.com/a/48877892/2954547 and https://stackoverflow.com/a/31458007/2954547, I wrote this function:

function Run-Program {
  param(
    [Parameter(Position = 0, Mandatory = $true)]           [String]$Executable,
    [Parameter(Position = 1, ValueFromRemainingArguments)] [String[]]$ProgramArgs
  )

  if ($MyInvocation.ExpectingInput){
    $input | & $Executable @ProgramArgs
  } else {
    & $Executable @ProgramArgs
  }
}

I tried testing it:

'abcdef' | Run-Program python -c 'import sys; print(sys.stdin.read())'

But I got this error:

Run-Program: The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.

Whereas I expected the string 'abcdef' to be passed to stdin of python, as per the automatic variable $input.

I was able to get this to work by explicitly declaring $input as a parameter:

  param(
    [Parameter(ValueFromPipeline)]                         $input,
    [Parameter(Position = 0, Mandatory = $true)]           [String]$Executable,
    [Parameter(Position = 1, ValueFromRemainingArguments)] [String[]]$ProgramArgs
  )

but that seems to defeat the purpose of having an automatic variable.

Did I misunderstand something in the usage of $input? Do I need to do something different to ensure that the type of the pipeline input is appropriate for the automatic variable $input?

shadowtalker
  • 12,529
  • 3
  • 53
  • 96

1 Answers1

1

By using [Parameter()] attributes you're implicitly making your function an advanced (cmdlet-like) one (the explicit way is to place a [CmdletBinding()] attribute above the param(...) block), which in turn implies that you must explicitly declare (at least one) pipeline-binding parameter(s) in order for the function to accept pipeline input.

Therefore, the easiest solution is to define a simple (non-advanced) function, which allows use of the automatic $input variable without extra effort, and to use the automatic $args variable to refer to all unbound arguments (those not binding to declared parameters):

function Run-Program {
  param(
    [string]$Executable = $(throw "Please specify an executable to invoke.")
  )
  if ($MyInvocation.ExpectingInput){
    $input | & $Executable @args
  } else {
    & $Executable @args
  }
}

Note the use of a throw statement as the default value of the -Executable parameter, which in effect makes the parameter mandatory - albeit using a different mechanism: neglecting to pass an argument then aborts the call (and the entire call stack) instead of prompting for a value.


If you do need an advanced function, you can use a dummy pipeline-binding parameter (by convention named -InputObject below), which then enables you to use the automatic $input variable as you intended, which refers to the collected pipeline input in the implied end block of the advanced function:

function Run-Program {
  param(
    [Parameter(Position = 0, Mandatory = $true)]           [String]$Executable,
    [Parameter(Position = 1, ValueFromRemainingArguments)] [String[]]$ProgramArgs,
    [Parameter(ValueFromPipeline)] $InputObject # Dummy parameter
  )
  if ($MyInvocation.ExpectingInput){
    $input | & $Executable @ProgramArgs
  } else {
    & $Executable @ProgramArgs
  }
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks! I assume the `@args` solution will not work if I want to use any additional optional parameters (e.g. `[Parameter(Mandatory = $false)] [Switch]Quiet` to enable/disable printing the command before running it), and I will need to use the "dummy parameter" solution. Is that right? – shadowtalker May 22 '23 at 22:15
  • Also, did you mean to write `$InputObject` or `$Input` in the parameter list? I didn't see any mention of `$InputObject` in the doc for `$input`. – shadowtalker May 22 '23 at 22:15
  • 1
    @shadowtalker: `$args` (or its splatted form, `@args`) is by definition only available in _non-advanced_ (simple) functions. But, given that all parameters are optional _by default_, just `[switch] Quiet` would still work without extra effort, as long as you avoid any `[Parameter()]` attributes. `$InputObject` is of necessity different from `$input`, but its specific name is chosen _by convention_, given that many cmdlets use this name (which is rarely seen as such, given that its primary purpose is to facilitate pipeline input). See also: https://github.com/PowerShell/PowerShell/issues/4242 – mklement0 May 22 '23 at 22:46
  • 1
    Oh, I see. The dummy parameter `$InputObject` is completely ignored, its purpose is only to populate `$input`, and it's not used on its own. Thank you for the explanation. – shadowtalker May 22 '23 at 22:58