This is the second blog in a multi-part series about designing a Windows PowerShell script that will be packaged in an executable file.
- Passing Parameters to a Script in an Executable File explains how to use the special parsing features of PowerShell Studio and PrimalScript to make passing parameters easy for PowerShell users and authors.
- Parsing Parameters for a Script in an Executable File explains how to parse parameters manually for special uses.
- Displaying Help for a Script in an Executable File explains how to display help for a script in an executable file.
- Output from a Script in an Executable File explains how to manage string output from a script in an executable file.
—–
The new input parsing features for executable files in PowerShell Studio and PrimalScript make the task of passing features to a script in an exe easier than ever. Users can enter parameter names and values when they run the exe, just as they would with a script. (To learn about the new parsing feature, see Passing Parameters to a Script in an Executable File.)
But, what if you want to do something a bit unconventional? In that case, you need to parse the input manually. And we’ve even made that easier for you by adding an $EngineArgs variable that returns the values typed at the command line as an array. You can also use the PowerShell Studio snippets in the Packager Functions directory, ‘Packager Commandline Parsing Functions’ and ‘Packager Convert EngineArgs to Hash Table.’
Why Parse Manually
In Passing Parameters to a Script in an Executable File, I explain that PowerShell Studio and PrimalScript parse Windows PowerShell parameter names and parameter values for you. You don’t need to grab the input strings, parse them, and associate them with your script parameters. It’s all done for you.
However, the executable passes only string values and, for the automatic parsing to work, the parameters must be in standard Windows PowerShell name and value format. Also, if the end-user wants to enter multiple values for a parameter, they must enclose the parameter values in a single quoted string.
Most of the time, you can live with these limitations, but if you cannot, or you want to do something unconventional, you need to parse the input strings manually. And that’s where the $EngineArgs variable comes in.
The $EngineArgs Variable
Beginning in PowerShell Studio 4.2.96 and PrimalScript 7.1.72, the $EngineArgs variable contains an array of the arguments entered at the command line.
You don’t have to define $EngineArgs. It’s added for you, much like an automatic variable in Windows PowerShell. But, it’s has values only when the script is packaged as an executable file.
For example, if I create a Get-EngineArgs.ps1 script, and run it, the $EngineArgs variable is empty.
#In Get-EngineArgs.ps1
$EngineArgs |
When you run the script with a few arguments, it returns nothing, as expected.
PS C:\> .\Get-EngineArgs.ps1 Happy Birthday to you PS C:\>
But, if I package it as an executable file in PowerShell Studio or PrimalScript, and then run Get-EngineArgs.exe, the $EngineArgs variable is populated.
And, then run the Get-EngineArgs.exe file with parameters and values (any parameters, any values), you get the values in $EngineArgs.
PS C:\> .\Get-EngineArgs.exe Happy Birthday to you Happy Birthday To You
$EngineArgs contains an array, so you can access the members of the array by using standard array notation. (Need help with array notation in Windows PowerShell? See about_Arrays.)
PS C:\> (.\Get-EngineArgs.exe Happy Birthday to you)[0] Happy PS C:\> (.Get-EngineArgs.exe Happy Birthday to you)[-1] you
You can discover and filter the values in $EngineArgs.
PS C:\> $result = .\Get-EngineArgs.exe Happy Birthday to you PS C:\> $result | where {$_ -like '*y'} Happy Birthday PS C:\> 'you' -in $result True
Using $EngineArgs for Slash and Switch Parameters
We all know the person who insists on slash-prefixed parameters (e.g. /name Joe /city Baltimore) instead of PowerShell’s dash-prefixed parameters (-Name Joe -City Baltimore). When you parse parameters manually, you can provide this experience to your users.
Also, when you package a script in an executable file, you convert your Switch parameters to String parameters that take values of ‘True’ and ‘False’. But, if that really doesn’t work for your users, you can parse the parameters manually and process the switch parameter name without a value.
For example, the New-WordTree.ps1 script that we used in Part 1 of this series creates a word tree from a word and a number. Its AddSpace parameter, which is a Switch type, adds a space between the words in the word tree.
PS C:\> Get-Command .\New-WordTree.ps1 -Syntax New-WordTree.ps1 [-Word] <string> [[-Number] <int>] [[-AddSpace] <Switch>] [<CommonParameters>] PS C:\> .\New-WordTree.ps1 -Word PowerShell -Number 3 PowerShell PowerShellPowerShell PowerShellPowerShellPowerShell PS C:\> .\New-WordTree.ps1 -Word PowerShell -Number 5 -AddSpace PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell
When you package the script in an executable file, the -AddSpace no longer works.
PS C:\> .\New-WordTree.exe -Word PowerShell -Number 3 -AddSpace Line 1: A positional parameter cannot be found that accepts argument ''.PS C:\>
Instead, you have to enter a string value.
PS C:\> .\New-WordTree.ps1 -Word PowerShell -Number 3 -AddSpace True PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell
When you parse the parameters manually, you can deliver an experience like this:
PS C:\> .\New-WordTree.exe /Word PowerShell /Number 3 PowerShell PowerShellPowerShell PowerShellPowerShellPowerShell
You can also restore the experience of a switch parameter, even though we’re really passing strings.
PS C:\> .\New-WordTree.exe /Word PowerShell /Number 3 /AddSpace PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell
And, still allow users to list the parameters in any order.
PS C:\> .\New-WordTree.exe /AddSpace /Word Automation /Number 3 Automation Automation Automation Automation Automation Automation
There are many ways to approach this task, but here’s what I did. I took the original New-WordTree.ps1 script and converted it into a function. Here’s the function code. It’s pretty cool.
function New-WordTree { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String]$Word, [int]$Number = 1, [Switch]$AddSpace ) $space = '' if ($AddSpace ) { $space = ' ' } 1..$Number | foreach { "$word$space" * $_ } } |
The New-WordTree function is now part of the New-WordTree.ps1 script. In the main part of the script, we won’t define any parameters. (You can, but it’s not necessary.) Instead, we’ll use the array in the $EngineArgs parameter.
My plan is to parse the values in $EngineArgs, create a hash table of parameters ($params), and then use splatting to pass the hash table to the New-WordTree function. (If you need help with splatting, see about_Splatting.
To parse the input in the $EngineArgs array, I use a For loop. I could have used ForEach, but when I find input that begins with a slash, I’m going to assume that it’s a parameter and that the next item in the array is the parameter value. For that sequence, I need to keep track of the positions (“indexes”) of the items in the $EngineArgs array. A For loop is the best way to manage those indexes.
My For loop is a bit unconventional, because it has a starting point ($i = 0) and a condition for continuing ($i -lt $EngingArgs.count), but it doesn’t have an automatic increment (typically $i++). I omitted the automatic increment (replaced by a ‘;’), because I want to increment by 2 when I encounter a parameter and its value, but increment by 1 when I encounter the AddSpace parameter, which doesn’t have a value.
Here’s my parsing loop.
for ($i = 0; $i -lt $EngineArgs.count;) { # A parameter name (/*) followed by a value (no /) if ($EngineArgs[$i] -like '/*' -and $EngineArgs[$i + 1] -and $EngineArgs[$i + 1] -notlike '/*') { #Get rid of the slash. It can be a dash or unprefixed. $p = $EngineArgs[$i] -replace '/', '' #Add the parameter and its value to $params $params.Add($p, $EngineArgs[$i + 1]) #Skip to the next parameter ($i = $i + 2) $i += 2 } elseif ($EngineArgs[$i] -eq '/AddSpace') { $params['-AddSpace'] = $True #Go to the next item. Don't skip. ($i = $i + 1) $i++ } else { throw $FormatError } |
If you want this parsing loop to enable Switch parameters, but keep dash-prefixed parameters, it’s just a bit simpler.
for ($i = 0; $i -lt $EngineArgs.count;) { # A parameter name (-…) followed by a value (no -) if ($EngineArgs[$i] -like '-*' -and $EngineArgs[$i + 1] -and $EngineArgs[$i + 1] -notlike '-*') { #Add the parameter and its value to $params $params.Add($EngineArgs[$i], $EngineArgs[$i + 1]) #Skip to the next parameter ($i = $i + 2) $i += 2 } elseif ($EngineArgs[$i] -eq '-AddSpace') { $params['-AddSpace'] = $True #Go to the next item. Don't skip. ($i = $i + 1) $i++ } else { throw $FormatError } |
The parsing is the hard part. The rest of the script is easier. I create some text strings and the $params hash table. Then, I make sure that $EngineArgs has a value. If it doesn’t, I display a help string. (For information about Help in an executable file, see Displaying Help for a Script in an Executable File.)
Finally, if the $params hash table has values in it, I call the New-WordTree function with the $params hash table. Because I’m splatting, I replace the ‘$’ of the $params variable with a ‘@’.
$params = @{ } if (!$EngineArgs) { New-WordTree -Word Help } else { < parsing For-loop > if ($params.Count -gt 0) { New-WordTree @params } } |
Now, package the script as usual: Home/Package/Build, and save it in an executable file, New-WordTree.exe
PS C:\> .\New-WordTree.exe /Word PowerShell /Number 3 /AddSpace PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell
Using $EngineArgs for Multiple Values
You can also use manual parsing to make it easier for users to enter multiple values for a parameter. This is a bit more complex, so make sure it’s worth it to your users.
For example, the automatic parsing requires users to enter multiple values for a single parameter in a quoted string. Otherwise, the second value is interpreted as a positional value for the next parameter.
PS C:\ps-test> .\New-WordTree.exe -Word PowerShell, NanoServer, DSC -Number 3 Line 1: A positional parameter cannot be found that accepts argument 'NanoServer'.PS C:\ps-test>
The user must enter the value collection in a quoted string.
PS C:\ps-test> .\New-WordTree.exe -Word "PowerShell, NanoServer, DSC" -Number 3 PowerShell PowerShellPowerShell PowerShellPowerShellPowerShell NanoServer NanoServerNanoServer NanoServerNanoServerNanoServer DSC DSCDSC DSCDSCDSC
By parsing the $EngineArgs string manually, you can enable users to enter a typical Windows PowerShell command with a comma-separated string value.
NOTE: This scenario assumes that the user is running the executable file in Windows PowerShell. If they run it in Cmd.exe, the Command Prompt window, you need to remove the commas manually in your script.
PS C:\ps-test> .\New-WordTree.exe -Word PowerShell, NanoServer, DSC -Number 4 PowerShell PowerShellPowerShell PowerShellPowerShellPowerShell PowerShellPowerShellPowerShellPowerShell NanoServer NanoServerNanoServer NanoServerNanoServerNanoServer NanoServerNanoServerNanoServerNanoServer DSC DSCDSC DSCDSCDSC DSCDSCDSCDSC PS C:\> .\New-WordTree.exe -Word PowerShell, NanoServer, DSC -Number 4 -AddSpace PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell PowerShell NanoServer NanoServer NanoServer NanoServer NanoServer NanoServer NanoServer NanoServer NanoServer NanoServer DSC DSC DSC DSC DSC DSC DSC DSC DSC DSC
To make this work, I start with the simpler version of the For-loop.
for ($i = 0; $i -lt $EngineArgs.count;) { # A parameter name (-…) followed by a value (no -) if ($EngineArgs[$i] -like '-*' -and $EngineArgs[$i + 1] -and $EngineArgs[$i + 1] -notlike '-*') { #Add the parameter and its value to $params $params.Add($EngineArgs[$i], $EngineArgs[$i + 1]) #Skip to the next parameter ($i = $i + 2) $i += 2 } elseif ($EngineArgs[$i] -eq '-AddSpace') { $params['-AddSpace'] = $True #Go to the next item. Don't skip. ($i = $i + 1) $i++ } else { throw $FormatError } |
But, in this version, when I find a parameter, I save it in a $p variable, then use a While loop to continue examining the items in the $EngineArgs array. If I find a parameter value (-notlike ‘-*’), I add it to a $v string array. When I hit another parameter (-like -*), I stop. Then, I add the parameter in $p and the values in $v to the $param hash table. There’s a bit of fiddling to put the commas in the right place in the string array.
$p = $EngineArgs[$i] # Create a variable to hold the values. $v = [string]@() # To find multiple values, start with the next item in $EngineArgs $i++ while ($EngineArgs[$i] -and $EngineArgs[$i] -notlike '-*') { # Unless it's the first item, it needs a comma if ($v -eq '') { $v += $EngineArgs[$i] } else { $v += ',' + $EngineArgs[$i] } $i++ } $params.Add($p, $v) |
Here’s the complete For loop with the While loop that manages multiple values.
for ($i = 0; $i -lt $EngineArgs.count;) { if ($EngineArgs[$i] -like '-*' -and $EngineArgs[$i + 1] -and $EngineArgs[$i + 1] -notlike '-*') { $p = $EngineArgs[$i] $v = [string]@() #Find multiple values. Start with the next item in $EngineArgs $i++ while ($EngineArgs[$i] -and $EngineArgs[$i] -notlike '-*') { if ($v -eq '') { $v += $EngineArgs[$i] } else { $v += ',' + $EngineArgs[$i]} $i++ } $params.Add($p, $v) } elseif ($EngineArgs[$i] -eq '-AddSpace') { $params['AddSpace'] = $True $i++ } else { throw $FormatError } } |
As in the simpler case, when I’m done parsing the $EngineArgs values, I call the New-WordTree function with the $params hash table in its splatted form (@params).
if ($params.Count -gt 0) { New-WordTree @params } |
That’s quite a bit of work, but it allows you to satisfy your most discerning users.
The default parameter name and parameter value parsing works for most Windows PowerShell scripts in executable files. But if you need to parse parameters in another style, the $EngineArgs variable makes this task manageable.
New stuff from the SAPIEN blog you might like: Parsing Parameters for a Script in an Executable File https://t.co/WCck7N89tL
RT @SAPIENTech: Parsing Parameters for a Script in an Executable File: This is the second blog in a multi-part series about des… https://…
RT @JeffHicks: New stuff from the SAPIEN blog you might like: Parsing Parameters for a Script in an Executable File https://t.co/WCck7N89tL
RT @JeffHicks: New stuff from the SAPIEN blog you might like: Parsing Parameters for a Script in an Executable File https://t.co/WCck7N89tL
Part 2 of our series on designing #PowerShell scripts to be packaged in executable files. https://t.co/Ox5H05qFe8
RT @juneb_get_help: Part 2 of our series on designing #PowerShell scripts to be packaged in executable files. https://t.co/Ox5H05qFe8
RT @juneb_get_help: Part 2 of our series on designing #PowerShell scripts to be packaged in executable files. https://t.co/Ox5H05qFe8
RT @juneb_get_help: Part 2 of our series on designing #PowerShell scripts to be packaged in executable files. https://t.co/Ox5H05qFe8
RT @SAPIENTech: Parsing Parameters for a Script in an Executable File: This is the second blog in a multi-part series about des… https://…
RT @juneb_get_help: Part 2 of our series on designing #PowerShell scripts to be packaged in executable files. https://t.co/Ox5H05qFe8
For ‘those guys’ who agree to use your PowerShell script, just so they can call it ‘their way.’ https://t.co/Ox5H05Ig5G