wiki:Programming/PowerShell/HowToPassCommandLineArgumentsBetweenScripts

Version 4 (modified by Vijay Varadan, 9 years ago) (diff)

--

HOWTO: Powershell - Pass Command Line Arguments Between Scripts

Jan 22 2016

PowerShell_5.0_icon

Scenario

I have to invoke different sets of scripts conditionally for setting up the build environment based on which project(s) I want to build. This is related to porting my build environment scripts from bash to Powershell.

Let's say, push0.ps1 is the main script and there are three other scripts, viz. quake.push0.ps1, doom.push0.ps1 and wolf.push0.ps1. I want to invoke push0.ps1 with some arguments from the command line and have push0.ps1 invoke one of the *.push0.ps1 scripts while passing all the command line arguments that push0.ps1 was invoked with.

A bit of code would help make the example concrete: push0.ps1

# $args is the array of input arguments to any script
# Do some work here common to all projects
# like pushing code to the master repo for developer scripts, etc.

# invoke project specific scripts here
# $Env:Project will be set to quake, doom or wolf
# call project specific push0 script if it exists 
# and pass all arguments to it
$scriptName = $Env:Project + ".push0.ps1"
if (Test-Path $scriptName) {
    & $scriptName $args
}

quake.push0.ps1

# do quake specific work here
$pushFolder = Join-Path $Env:QuakeBaseDir "win32"
cd $pushFolder
hg push

$winSpecificFolder = Join-Path $pushFolder $Env:QuakeWinSubDir
cd $winSpecificFolder
hg push

# now push any external sub-repos
$quakeExtBaseDir = Join-Path $Env:ExtBaseDir $QuakeExtWin32SubDir
$args | % {
    $extFolder = Join-Path $quakeExtBaseDir $_
    cd $extFolder
    hg push
}

doom.push0.ps1

# do doom specific work here
$pushFolder = Join-Path $Env:DoomBaseDir "win16"
cd $pushFolder
hg push

$win16SpecificFolder = Join-Path $pushFolder $Env:DoomWin16SubDir
cd $win16SpecificFolder
hg push

# now push assembly language library repos
$doomAsmBaseDir = Join-Path $Env:AsmLibBaseDir $doomAsmSubDir
$args | % {
    $asmFolder = Join-Path $doomAsmBaseDir $_
    cd $asmFolder
    hg push
}

wolf.push0.ps1

# do wolf specific work here
$pushFolder = Join-Path $Env:WolfBaseDir "dos"
cd $pushFolder
hg push

$dosSpecificFolder = Join-Path $pushFolder $env:WolfDosSubDir
cd $dosSpecificFolder
hg push

# now push dos assembly language library repos
$wolfAsmBaseDir = Join-Path $Env:AsmLibBaseDir $wolfAsmSubDir
$args | % {
    $asmFolder = Join-Path $wolfAsmBaseDir $_
    cd $asmFolder
    hg push
}

As you can see from the example above, the three project specific scripts push different sub-repos that are passed in as parameters.

Problem

The three project specific scripts {quake|doom|wolf}.push0.ps1, work fine as they are now, if they are individually invoked from Powershell. But if they are  invoked from another Powershell script, then they fail processing the arguments passed in via $args.

The issue is that when $args is passed to push0.ps1, it's an array of parameters, but when you invoke another script from within push0.ps1, the $args array (i.e. all the parameters) is treated as a single parameter passed to the callee script. If I add a line to print out the arguments in quake.push0.ps1 like this:

quake.push0.ps1

...
...
Write-Host "Quake:- args[=" $args.Count "]: " $args
...
...

And invoke push0.ps1 passing in some arguments, then the output looks like this:

    $> .\push0.ps1 boost libssl lua
    Quake:- args[=1]: boost libssl lua

As you can see, even though all 3 parameters are passed in, they are seen as a single parameter.

Essentially it would appear, that when the callee script is invoked, all 3 parameters as passed in as $args[0]. Now, Powershell is an object scripting language and it seems to pass an array of strings from the caller script as the first and only parameter to the callee script.

Knowing that since Powershell is built on top of .NET and deals with objects, we can verify this by checking the type of $args[0] like this:

quake.push0.ps1

...
...
Write-Host $args[0].GetType().IsArray
...
...

Output

    True

And we're in business! :-)

Solution

There are 2 solutions. The first uses PowerShell splatting, available as of PowerShell V2, but not easily found unless you use the term splatting in your search. The second involves manual expansion, which I prefer even though it involves more changes. The second method can also be used on the command line when piping the output of native programs. Program output raw strings rather than string arrays, since they don't care about the shell they're being invoked from.

Solution 1: Using Splatting

Simply invoke the callees by prefixing the args array with an @ sign rather than the $ sign, like this (see comment marked as CHANGE:):

push0.ps1

# $args is the array of input arguments to any script
# Do some work here common to all projects
# like pushing code to the master repo for developer scripts, etc.

# invoke project specific scripts here
# $Env:Project will be set to quake, doom or wolf
# call project specific push0 script if it exists 
# and pass all arguments to it
$scriptName = $Env:Project + ".push0.ps1"
if (Test-Path $scriptName) {
    & $scriptName @args # CHANGE: use @args instead of $args
}

quake.push0.ps1 - no change

doom.push0.ps1 - no change

wolf.push0.ps1 - no change

Solution 2: Manual Expansion

What we need to do in the callees, is check if the incoming $args has only 1 parameter and if it's an array. If so, then simply use $args[0] as the actual list of arguments and process it below.

Since this bit of the code is common to multiple scripts, I put it in a function and stick it in the $PROFILE file or some other file that can be dot-sourced. Here's the function:

function UnwrapArguments($params) {
    $unwrapped = $params
    if ($params -and $params.Count -eq 1 -and $params[0].GetType().IsArray) {
        $unwrapped = $params[0]
    }
    return $unwrapped
}

We make 0 (zero) changes in the caller and only 2 changes per callee. At the top of each callee file, we call UnwrapArguments and store it in $myargs (marked with comment: CHANGE #1 ) and further down in the script, we use $myargs instead of $args (marked with comment: CHANGE #2 ).

So our scripts end up looking like this:

push0.ps1 - no changes.


quake.push0.ps1

$myargs = UnwrapArguments($args) # CHANGE #1

# do quake specific work here
$pushFolder = Join-Path $Env:QuakeBaseDir "win32"
cd $pushFolder
hg push

$winSpecificFolder = Join-Path $pushFolder $Env:QuakeWinSubDir
cd $winSpecificFolder
hg push

# now push any external sub-repos
$quakeExtBaseDir = Join-Path $Env:ExtBaseDir $QuakeExtWin32SubDir
$myargs</span> | % { # CHANGE #2, we use the unwrapped $myargs instead of $args
    $extFolder = Join-Path $quakeExtBaseDir $_
    cd $extFolder
    hg push
}

doom.push0.ps1

$myargs = UnwrapArguments($args) # CHANGE #1

# do doom specific work here
$pushFolder = Join-Path $Env:DoomBaseDir "win16"
cd $pushFolder
hg push

$win16SpecificFolder = Join-Path $pushFolder $Env:DoomWin16SubDir
cd $win16SpecificFolder
hg push

# now push assembly language library repos
$doomAsmBaseDir = Join-Path $Env:AsmLibBaseDir $doomAsmSubDir
$myargs</span> | % { # CHANGE #2, we use the unwrapped $myargs instead of $args
    $asmFolder = Join-Path $doomAsmBaseDir $_
    cd $asmFolder
    hg push
}

wolf.push0.ps1

$myargs = UnwrapArguments($args) # CHANGE #1

# do wolf specific work here
$pushFolder = Join-Path $Env:WolfBaseDir "dos"
cd $pushFolder
hg push

$dosSpecificFolder = Join-Path $pushFolder $env:WolfDosSubDir
cd $dosSpecificFolder
hg push

# now push dos assembly language library repos
$wolfAsmBaseDir = Join-Path $Env:AsmLibBaseDir $wolfAsmSubDir
$myargs</span> | % { # CHANGE #2, we use the unwrapped $myargs instead of $args
    $asmFolder = Join-Path $wolfAsmBaseDir $_
    cd $asmFolder
    hg push
}

That's it. We're golden.