| 1 | = HOWTO: Powershell - Pass Command Line Arguments Between Scripts = |
| 2 | Jan 22 2016 |
| 3 | |
| 4 | [[Image(htdocs:images/powershell/PowerShell_5.0_icon.png, alt="PowerShell_5.0_icon", align=center)]] |
| 5 | |
| 6 | === Scenario === |
| 7 | |
| 8 | 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. |
| 9 | |
| 10 | 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. |
| 11 | |
| 12 | A bit of code would help make the example concrete: |
| 13 | {{{#!html |
| 14 | <pre><strong>push0.ps1</strong> |
| 15 | <span style="color: #008000;"># $args is the array of input arguments to any script |
| 16 | # Do some work here common to all projects</span> |
| 17 | <span style="color: #008000;"># like pushing code to the master repo for developer scripts, etc.</span> |
| 18 | |
| 19 | <span style="color: #008000;"># invoke project specific scripts here |
| 20 | # $Env:Project will be set to quake, doom or wolf</span> |
| 21 | <span style="color: #008000;"># call project specific push0 script if it exists </span> |
| 22 | <span style="color: #008000;"># and pass all arguments to it</span> |
| 23 | $scriptName = $Env:Project + ".push0.ps1" |
| 24 | if (Test-Path $scriptName) { |
| 25 | & $scriptName $args |
| 26 | }</pre> |
| 27 | }}} |
| 28 | |
| 29 | ---- |
| 30 | |
| 31 | {{{#!html |
| 32 | <pre><strong>quake.push0.ps1</strong> |
| 33 | <span style="color: #008000;"># do quake specific work here |
| 34 | </span>$pushFolder = Join-Path $Env:QuakeBaseDir "win32" |
| 35 | cd $pushFolder |
| 36 | hg push |
| 37 | |
| 38 | $winSpecificFolder = Join-Path $pushFolder $Env:QuakeWinSubDir |
| 39 | cd $winSpecificFolder |
| 40 | hg push |
| 41 | |
| 42 | <span style="color: #008000;"># now push any external sub-repos |
| 43 | </span>$quakeExtBaseDir = Join-Path $Env:ExtBaseDir $QuakeExtWin32SubDir |
| 44 | $args | % { |
| 45 | $extFolder = Join-Path $quakeExtBaseDir $_ |
| 46 | cd $extFolder |
| 47 | hg push |
| 48 | } |
| 49 | </pre> |
| 50 | }}} |
| 51 | |
| 52 | ---- |
| 53 | |
| 54 | {{{#!html |
| 55 | <pre><strong>doom.push0.ps1</strong> |
| 56 | <span style="color: #008000;"># do doom specific work here |
| 57 | </span>$pushFolder = Join-Path $Env:DoomBaseDir "win16" |
| 58 | cd $pushFolder |
| 59 | hg push |
| 60 | |
| 61 | $win16SpecificFolder = Join-Path $pushFolder $Env:DoomWin16SubDir |
| 62 | cd $win16SpecificFolder |
| 63 | hg push |
| 64 | |
| 65 | <span style="color: #008000;"># now push assembly language library repos |
| 66 | </span>$doomAsmBaseDir = Join-Path $Env:AsmLibBaseDir $doomAsmSubDir |
| 67 | $args | % { |
| 68 | $asmFolder = Join-Path $doomAsmBaseDir $_ |
| 69 | cd $asmFolder |
| 70 | hg push |
| 71 | }</pre> |
| 72 | }}} |
| 73 | |
| 74 | ---- |
| 75 | |
| 76 | {{{#!html |
| 77 | <pre><strong>wolf.push0.ps1</strong> |
| 78 | <span style="color: #008000;"># do wolf specific work here |
| 79 | </span>$pushFolder = Join-Path $Env:WolfBaseDir "dos" |
| 80 | cd $pushFolder |
| 81 | hg push |
| 82 | |
| 83 | $dosSpecificFolder = Join-Path $pushFolder $env:WolfDosSubDir |
| 84 | cd $dosSpecificFolder |
| 85 | hg push |
| 86 | |
| 87 | <span style="color: #008000;"># now push dos assembly language library repos |
| 88 | </span>$wolfAsmBaseDir = Join-Path $Env:AsmLibBaseDir $wolfAsmSubDir |
| 89 | $args | % { |
| 90 | $asmFolder = Join-Path $wolfAsmBaseDir $_ |
| 91 | cd $asmFolder |
| 92 | hg push |
| 93 | } |
| 94 | </pre> |
| 95 | }}} |
| 96 | |
| 97 | As you can see from the example above, the three project specific scripts push different sub-repos that are passed in as parameters. |
| 98 | |
| 99 | === Problem === |
| 100 | |
| 101 | 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`. |
| 102 | |
| 103 | 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: |
| 104 | |
| 105 | {{{#!html |
| 106 | <pre><strong>quake.push0.ps1</strong> |
| 107 | ... |
| 108 | ... |
| 109 | Write-Host "Quake:- args[=" $args.Count "]: " $args |
| 110 | ... |
| 111 | ...</pre> |
| 112 | }}} |
| 113 | |
| 114 | And invoke `push0.ps1` passing in some arguments, then the output looks like this: |
| 115 | |
| 116 | {{{#!html |
| 117 | <pre>$> .\push0.ps1 boost libssl lua |
| 118 | Quake:- args[=<span style="color: #ff0000;"><strong>1</strong></span>]: boost libssl lua</pre> |
| 119 | }}} |
| 120 | |
| 121 | As you can see, even though all 3 parameters are passed in, they are seen as <em>a single parameter</em>. |
| 122 | |
| 123 | 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 <em>array of strings</em> from the caller script as the first and only parameter to the callee script. |
| 124 | |
| 125 | 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: |
| 126 | |
| 127 | {{{#!html |
| 128 | <pre><strong>quake.push0.ps1</strong> |
| 129 | ... |
| 130 | ... |
| 131 | Write-Host $args[0].GetType().IsArray |
| 132 | ... |
| 133 | ... |
| 134 | |
| 135 | <strong>OUTPUT</strong> |
| 136 | <span style="color: #0000ff;">True</span></pre> |
| 137 | }}} |
| 138 | |
| 139 | And we're in business! :-) |
| 140 | |
| 141 | === Solution === |
| 142 | |
| 143 | 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. |
| 144 | |
| 145 | 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: |
| 146 | {{{#!html |
| 147 | <pre><strong><span style="color: #0000ff;">function UnwrapArguments($params) {</span></strong> |
| 148 | <strong><span style="color: #0000ff;"> $unwrapped = @()</span></strong> |
| 149 | <strong><span style="color: #0000ff;"> if ($params.Count -gt 0) {</span></strong> |
| 150 | <strong><span style="color: #0000ff;"> if ($params.Count -eq 1 -and $params[0].GetType().IsArray) {</span></strong> |
| 151 | <strong><span style="color: #0000ff;"> $unwrapped = $params[0]</span></strong> |
| 152 | <strong><span style="color: #0000ff;"> } else {</span></strong> |
| 153 | <strong><span style="color: #0000ff;"> $unwrapped = $params</span></strong> |
| 154 | <strong><span style="color: #0000ff;"> }</span></strong> |
| 155 | <strong><span style="color: #0000ff;"> }</span></strong> |
| 156 | <strong><span style="color: #0000ff;"> return $unwrapped</span></strong> |
| 157 | <strong><span style="color: #0000ff;">}</span></strong></pre> |
| 158 | }}} |
| 159 | |
| 160 | So our scripts end up looking like this (changes in blue): |
| 161 | |
| 162 | {{{#!html |
| 163 | <pre><strong>push0.ps1 |
| 164 | <span style="color: #0000ff;"># NOTE: No changes to the caller.</span></strong> |
| 165 | <span style="color: #008000;"># $args is the array of input arguments to any script |
| 166 | # Do some work here common to all projects</span> |
| 167 | <span style="color: #008000;"># like pushing code to the master repo for developer scripts, etc.</span> |
| 168 | |
| 169 | <span style="color: #008000;"># invoke project specific scripts here |
| 170 | # $Env:Project will be set to quake, doom or wolf</span> |
| 171 | <span style="color: #008000;"># call project specific push0 script if it exists </span> |
| 172 | <span style="color: #008000;"># and pass all arguments to it</span> |
| 173 | $scriptName = $Env:Project + ".push0.ps1" |
| 174 | if (Test-Path $scriptName) { |
| 175 | & $scriptName $args |
| 176 | }</pre> |
| 177 | }}} |
| 178 | |
| 179 | ---- |
| 180 | |
| 181 | {{{#!html |
| 182 | <pre><strong>quake.push0.ps1 |
| 183 | <span style="color: #0000ff;">$myargs = UnwrapArguments($args) |
| 184 | </span></strong> |
| 185 | <span style="color: #008000;"># do quake specific work here |
| 186 | </span>$pushFolder = Join-Path $Env:QuakeBaseDir "win32" |
| 187 | cd $pushFolder |
| 188 | hg push |
| 189 | |
| 190 | $winSpecificFolder = Join-Path $pushFolder $Env:QuakeWinSubDir |
| 191 | cd $winSpecificFolder |
| 192 | hg push |
| 193 | |
| 194 | <span style="color: #008000;"># now push any external sub-repos |
| 195 | </span>$quakeExtBaseDir = Join-Path $Env:ExtBaseDir $QuakeExtWin32SubDir |
| 196 | <span style="color: #0000ff;"><strong>$myargs</strong></span> | % { |
| 197 | $extFolder = Join-Path $quakeExtBaseDir $_ |
| 198 | cd $extFolder |
| 199 | hg push |
| 200 | } |
| 201 | </pre> |
| 202 | }}} |
| 203 | |
| 204 | ---- |
| 205 | |
| 206 | {{{#!html |
| 207 | <pre><strong>doom.push0.ps1 |
| 208 | <span style="color: #0000ff;">$myargs = UnwrapArguments($args) |
| 209 | </span></strong> |
| 210 | <span style="color: #008000;"># do doom specific work here |
| 211 | </span>$pushFolder = Join-Path $Env:DoomBaseDir "win16" |
| 212 | cd $pushFolder |
| 213 | hg push |
| 214 | |
| 215 | $win16SpecificFolder = Join-Path $pushFolder $Env:DoomWin16SubDir |
| 216 | cd $win16SpecificFolder |
| 217 | hg push |
| 218 | |
| 219 | <span style="color: #008000;"># now push assembly language library repos |
| 220 | </span>$doomAsmBaseDir = Join-Path $Env:AsmLibBaseDir $doomAsmSubDir |
| 221 | <strong><span style="color: #0000ff;">$myargs</span></strong> | % { |
| 222 | $asmFolder = Join-Path $doomAsmBaseDir $_ |
| 223 | cd $asmFolder |
| 224 | hg push |
| 225 | }</pre> |
| 226 | }}} |
| 227 | |
| 228 | ---- |
| 229 | |
| 230 | {{{#!html |
| 231 | <pre><strong>wolf.push0.ps1 |
| 232 | <span style="color: #0000ff;">$myargs = UnwrapArguments($args) |
| 233 | </span></strong> |
| 234 | <span style="color: #008000;"># do wolf specific work here |
| 235 | </span>$pushFolder = Join-Path $Env:WolfBaseDir "dos" |
| 236 | cd $pushFolder |
| 237 | hg push |
| 238 | |
| 239 | $dosSpecificFolder = Join-Path $pushFolder $env:WolfDosSubDir |
| 240 | cd $dosSpecificFolder |
| 241 | hg push |
| 242 | |
| 243 | <span style="color: #008000;"># now push dos assembly language library repos |
| 244 | </span>$wolfAsmBaseDir = Join-Path $Env:AsmLibBaseDir $wolfAsmSubDir |
| 245 | <strong><span style="color: #0000ff;">$myargs</span></strong> | % { |
| 246 | $asmFolder = Join-Path $wolfAsmBaseDir $_ |
| 247 | cd $asmFolder |
| 248 | hg push |
| 249 | } |
| 250 | </pre> |
| 251 | }}} |
| 252 | |
| 253 | That's it. We're golden. |