Parsing Parameters for a Script in an Executable File

This is the second blog in a multi-part series about designing a Windows PowerShell script that will be packaged 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.

PackageScript

 

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:\&gt; .\New-WordTree.exe -Word PowerShell -Number 3 -AddSpace
Line 1: A positional parameter cannot be found that accepts argument ''.PS C:\&gt;

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.