Search code examples
powershellparametersparameter-passingargs

PowerShell - accessing script parameters, sent from Windows cmd shell


Questions

  1. How can you access the parameters sent to PowerShell script (.ps1 file)?
  2. Can you access parameters A: by name, B: by position, C: a mix of either?

Context

I recently tried to write a PowerShell script (.ps1) that would be called from a Windows batch file (.bat) (or potentially cmd shell, or AutoHotKey script) - which would pass parameters into the .ps1 script for it to use (to display a toast notification). Thanks to the instructions on ss64.com, I have used $args to do this kind of thing in the past, however for some reason I could access the parameters this way (despite passing parameters, $args[0] = '' (empty string) and $args.Count = 0) so eventually had to remove all the $args code, and replace it with Param() script instead.

I'm still not quite sure why, but thought this is something I should get to the bottom of before I try to write my next script...

Code Example 1: Args (un-named parameters)

ToastNotificationArgs.ps1
-------------------------

Write-Debug "The script has been passed $($args.Count) parameters"
If (!$args[0]) { # Handle first parameter missing }
If (!$args[1]) { # Handle second parameter missing }

Import-Module -Name BurntToast
New-BurntToastNotification -Text "$args[0], $args[1]"

^ I thought the above code was correct, but like I say, I kept struggling to access the parameters for some reason and could not figure out why. (If anyone can spot what I was doing wrong, please shout!)

Is $args[] a valid approach? I assume so given it's use of ss64.com, but maybe there are some pitfalls / limitations I need to be aware of?


Code Example 2: Param (named parameters)

ToastNotificationParams.ps1
---------------------------
Param(
    [Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)] [string]$Title,
    [Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$true)] [string]$Message
)

Import-Module -Name BurntToast
New-BurntToastNotification -Text "$Title, $Message"

^ This was the only way to get my script working in the end. However when I passed the parameters in, my calling cmd script sent the parameters by position i.e. (pwsh.exe -File "ToastNotificationParams.ps1" "This is the title" "Message goes here") rather than by named pairs. (Not sure if this is best practice, but is how my script was initially intended to be used to left it for now).


While Param() got my script working this time (and I also realise the inherent dangers of position-based parameters), there are times when a position-based approach might be necessary (e.g. the number of parameter is unknown)...

Code Example 3: Hybrid

ToastNotificationMix.ps1
------------------------
Param(
    [Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)] [string]$Title
)

Import-Module -Name BurntToast
For ( $i = 1; $i -lt $args.count; $i++ ) {
    New-BurntToastNotification -Text "$Title, $args[i]"
}

Is something like this valid?.. If not (or there is a better solution), any help would be greatly appreciated!

Thanks in advance!


Solution

    • The automatic $args variable is only available in simple (non-advanced) functions / scripts. A script automatically becomes an advanced one by using the [CmdletBinding()] attribute and/or at least one per-parameter [Parameter()] attribute.

      • Using $args allows a function/script to accept an open-ended number of positional arguments, usually instead of, but also in addition to using explicitly declared parameters.

      • But it doesn't allow passing named arguments (arguments prefixed by a predeclared target parameter name, e.g., -Title)

    • For robustness, using an advanced (cmdlet-like) function or script is preferable; such functions / scripts:

      • They require declaring parameters explicitly.
      • They accept no arguments other than ones that bind to declared parameters.
        • However, you can define a single catch-all parameter that collects all positional arguments that don't bind to any of the other predeclared parameters, using [Parameter(ValueFromRemainingArguments)].
    • Explicitly defined parameters are positional by default, in the order in which they are declared inside the param(...) block.

      • You can turn off this default with [CmdletBinding(PositionalBinding=$false)],
      • which then allows you to selectively enable positional binding, using the Position property of the individual [Parameter()] attributes.
    • When you call a PowerShell script via the PowerShell's CLI's -File parameter, the invocation syntax is fundamentally the same as when calling script from inside PowerShell; that is, you can pass named arguments and/or - if supported - positional arguments.

      • Constraints:
        • The arguments are treated as literals.
        • Passing array arguments (,-separated elements) is not supported.
      • If you do need your arguments to be interpreted as they would be from inside PowerShell, use the -Command / -c CLI parameter instead
      • See this answer for guidance on when to use -File vs. `-Command.

    To put it all together:

    ToastNotificationMix.ps1:

    [CmdletBinding(PositionalBinding=$false)]
    Param(
        [Parameter(Position=0)] 
        [string]$Title
        ,
        [Parameter(Mandatory, ValueFromRemainingArguments)] 
        [string[]] $Rest
    )
    
    Import-Module -Name BurntToast
    foreach ($restArg in $Rest) {
       New-BurntToastNotification -Text "$Title, $restArg"
    }
    

    You can then call your script from cmd.exe as follows, for instance (I'm using pwsh.exe, the PowerShell (Core) CLI; for Windows PowerShell, use powershell.exe):

    Positional binding only:

    :: "foo" binds to $Title, "bar" to $Rest
    pwsh -File ./ToastNotificationMix.ps1 foo bar
    

    Mix of named and positional binding:

    :: "bar" and "baz" both bind to $Rest
    pwsh -File ./ToastNotificationMix.ps1 -Title foo bar baz