Understanding Arguments and Parameters

Listen to this blog post!

Table of contents:

PowerShell functions and scripts provide several built-in techniques for callers to submit information. Since both functions and scripts are based on script blocks, these techniques work in any script block. In the remainder of this article, I will use the term “script blocks” to include both *.ps1 script files and functions.

At a minimum, script blocks support the special variables $args, $input, and $_. Here is what they do:

$args

$args is an array that holds the positional arguments specified when calling the script block. The first argument is stored in $args[0], the second in $args[1], and so on.

Here is a function to experiment with $args:

function Test-Me
{
  "You submitted $($args.Count) arguments."
  
  for ($x=0; $x -lt $args.Count; $x++)
  {
     "Argument $($x): $($args[$x])" 
  } 
} 

If you prefer to do the same with a *.ps1 script, simply remove the function wrapper (since both are really script blocks):

"You submitted $($args.Count) arguments."

for ($x=0; $x -lt $args.Count; $x++)
{
   "Argument $($x): $($args[$x])" 
} 

Now, when you call the code—such as the Test-Me function—you can observe how the external arguments are passed into the code:

PS> Test-Me
You submitted 0 arguments.

PS> Test-Me hello
You submitted 1 argument.
Argument 0: hello

PS> Test-Me hello 1 2 test
You submitted 4 arguments.
Argument 0: hello
Argument 1: 1
Argument 2: 2
Argument 3: test

In conclusion, $args works exclusively for arguments passed to a script block. The order of arguments matters, and named parameters are not supported.

$input

Arguments submitted via the pipeline are assigned to either $input or $_. $input is an array that combines all arguments submitted through the pipeline.

However, $input is tricky because it is actually an enumerator. You can read it only once, and if you want to read it again, you must call its Reset() method to return the enumerator to the beginning:

function Test-Me
{
  "You submitted $(@($input).Count) arguments via the pipeline."
  $input.Reset()
  
  foreach ($information in $input)
  {
     "Pipeline Data: $information" 
  }
}  

Other than that, it works as expected:

PS> Test-Me
You submitted 0 arguments via the pipeline.

PS> Test-Me does not work for "normal" arguments
You submitted 0 arguments via the pipeline.

PS> 1 | Test-Me
You submitted 1 arguments via the pipeline.
Pipeline Data: 1

PS> 123,"Hello",8.8 | Test-Me
You submitted 3 arguments via the pipeline.
Pipeline Data: 123
Pipeline Data: Hello
Pipeline Data: 8.8
 

To work around the enumerator quirks of $input, simply convert it to a regular array. This makes working with $input much more intuitive, allowing you to treat it the same way as $args:

function Test-Me
{
  # convert $input to a normal array first so you can read its
  # content as often as you want
  $input = @($input)
  "You submitted $($input.Count) arguments via the pipeline."
  
  for ($x=0; $x -lt $input.Count; $x++)
  {
     "Argument $($x): $($input[$x])" 
  }
}  

Here is what the output looks like:

PS> Test-Me
You submitted 0 arguments via the pipeline.

PS> Test-Me does not work for "normal" arguments
You submitted 0 arguments via the pipeline.

PS> 1 | Test-Me
You submitted 1 arguments via the pipeline.
Argument 0: 1

PS> 123,"Hello",8.8 | Test-Me
You submitted 3 arguments via the pipeline.
Argument 0: 123
Argument 1: Hello
Argument 2: 8.8 

Here is what the output looks like:

PS> Test-Me
You submitted 0 arguments via the pipeline.

PS> Test-Me does not work for "normal" arguments
You submitted 0 arguments via the pipeline.

PS> 1 | Test-Me
You submitted 1 arguments via the pipeline.
Argument 0: 1

PS> 123,"Hello",8.8 | Test-Me
You submitted 3 arguments via the pipeline.
Argument 0: 123
Argument 1: Hello
Argument 2: 8.8 

Using $_

The special variable $_ represents the current pipeline element passed from the upstream command. To use it, you must reference $_ within a process block. Like $input, $_ captures only pipeline input:

function Test-Me
{
  # individual pipeline elements can only be "seen" inside the
  # pipeline loop (the "process" block)
  
  process
  {
    "I am receiving from the pipeline: $_"
  }
  
}  

In contrast to $input, $_ processes pipeline elements in real time as they arrive, whereas $input collects all pipeline data before returning control to your code. As a result, in this example your code has no way of knowing how many pipeline items will be received in total.

PS> Test-Me 
I am receiving from the pipeline: 

PS> 1 | Test-Me 
I am receiving from the pipeline: 1

PS> 1..4 | Test-Me 
I am receiving from the pipeline: 1
I am receiving from the pipeline: 2
I am receiving from the pipeline: 3
I am receiving from the pipeline: 4

Note that you can not use both $input and $_ simultaneously: once you add a process block, $input is no longer available in the end block:

function Test-Me
{
  # individual pipeline elements can only be "seen" inside the
  # pipeline loop (the "process" block)
  
  process
  {
    "I am receiving from the pipeline: $_"
  }
  
  end
  {
    # does not work (works only when you remove the process block)
    "Total received pipeline elements: $input"
  }
  
}  
PS> 1..4 | Test-Me 
I am receiving from the pipeline: 1
I am receiving from the pipeline: 2
I am receiving from the pipeline: 3
I am receiving from the pipeline: 4
Total received pipeline elements:  

Once you remove the process block, $input starts working:

PS> 1..4 | Test-Me 
Total received pipeline elements: 1 2 3 4 

Conclusion

$args, $input, and $_ are the most basic means of handling user input. They have remained unchanged throughout all PowerShell versions and continue to work as described. These variables are still widely used, especially in simple scenarios or when working with anonymous script blocks:

# typical every-day use-case for using $_
1..10 | ForEach-Object -Process { 'accessing $_: ' + $_ }
# won't work technically because Foreach-Object requires a process block
1..10 | ForEach-Object -End { "Collected Data: $input" }
# will work with a simple script block
1..10 | & {end  { "Collected Data: $input" }} 

From a user’s perspective, this approach has two drawbacks:

  • It is unintuitive — arguments are strictly positional, with no support for descriptive parameter names, and the user cannot easily know which arguments are available or what they do.
  • It is inconsistent — you must handle normal arguments differently from those received via the pipeline.

Part 2 of this series explores ways to improve parameter handling and address these drawbacks.

Related links