How do I turn the $input automatic variable into a list (in an end block)?

Viewed 87

Apparently, $input is of type System.Collections.ArrayList+ArrayListEnumeratorSimple, and not just Object[]. And [Object[]]$input fails. I guess I could just manually add pipeline elements to my own $input, but is there an easier way to do this?

2 Answers
  • A function that doesn't use begin, process and end blocks implicitly behaves as if its body were inside an end block; that is, the body isn't executed until after all pipeline input has been received.

  • In an (implied) end block, you can use @(), the array-subexpression operator to collect all pipeline input ($input) in an [object[]] array:

function foo {
  $allPipelineInputAsArray = @($input) # creates [object[]] array
  $allPipelineInputAsArray.ForEach({ "[$_]" }) # sample output
}

The above relies on $input implementing the IEnumerator interface, which means that you can also use it directly in a foreach loop, for instance:

function foo {
  foreach ($obj in $input) {
    "[$obj]" # sample output.
  }
}

Both function definitions, when invoked as 1..3 | foo, yield the following:

[1]
[2]
[3]

Caveat: If you define your function as an advanced (cmdlet-like) function (explicitly, via a [CmdletBinding(...)] attribute or implicitly, via a [Parameter(...)] attribute, the automatic $input variable is no longer available - you would then have to manually collect all input objects in a list data structure via the process block, which you can then process in the end block - see the second function definition in Mathias R. Jessen's answer.

Also note that by using this approach you're forgoing the core feature of the PowerShell pipeline, namely the memory-throttling, one-by-one processing of input objects (you're collecting all input objects in memory); however, this would likely only become a problem with very large input sets.


As for what you tried:

[Object[]] $input

Since $input doesn't implement IEnumerable - only its auxiliary IEnumerator interface - this cast doesn't cause the enumerator $input to be walked; instead, the enumerator object itself is cast to [object[]], which is not the intent.

To put it differently: $input isn't the collection of input objects, it is a helper object for enumerating the collection of input objects.

Use the array subexpression operator @() to "extract" all items from the enumerator:

function f
{
  end {
    # this will be of type [object[]]
    $InputItems = @($input)
  }
}

If you want to support pipeline input and non-pipeline input all the same, use the following pattern:

  • Declare an array-type input parameter
    • Mark it ValueFromPipeline
  • Use the process block to "collect" all input items that might come in via the pipeline
  • Execute actual code against your collected pipeline items in the end block:
function f
{
  param(
    [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
    [object[]]$InputObject)

  begin {
    $AllInputItems = [System.Collections.Generic.List[object]]::new()
  }

  process {
    $AllInputItems.AddRange($InputObject)
  }

  end {
    # Work on $AllInputItems down here
  }
}

Now the user can do:

1,2,3 |f
# or
f 1,2,3
Related