Powershell cmdlets with dynamic param AND $args don't work

Over the weekend I tried to implement auto completion in Elixir’s mix (in Windows). Unfortunately I didn’t make it without introducing some problems. So I didn’t committed my changes to upstream. Currently I try to reach some of the more renowned Elixir/Windows contributors, to discuss the changes .

Motivation

Under normal circumstances I don’t use more mix tasks then test, phoenix.server and release but sometimes you need this weird command, you just can’t remember. The command mix help  is your friend here as it shows you all available commands (project aware!). Yet I don’t like to look the documentation up, if I need just some information on spelling. For example in the beginning I often tried to start the phoenix project with mix phoenix.start (Hint: that does not work). I am used to auto completion in my development environments so I tried to extend mix  as well.

Background

As I am using the Powershell for all my command line related tasks and the default file extension of Powershell is ps1, my command mix  execute the mix.ps1  in the Elixir bin folder.

Approach

Powershell scripts can have auto completion of parameters with an so called [validateSet(“Param1”,“Param2”,…)] , which incorporate all valid parameters. Sadly this is of no help, if we have to hard code the possible values for the parameter. A possible solution to this problem is the usage of a DynamicParam with dynamic validateSet (good resource here). To test my various iterations I wrote down all test cases (sorry no automated testing yet).

Iteration 1

Changes

If you have a look at the original mix  script (here) you can see that the script locates the mix.bat , flattens possible array arguments (is this still needed?) and then execute the mix.bat  with the newly flattened arguments.

The first problem we see here is the usage of the $args array. As Keith Hill points out in this SO comment the $args array “… contain any argument that doesn’t map to a defined function parameter…”. Which introduces the first problem: The DynamicParam ONLY works for defined function parameters.

I copied the linked resource (again, here) and moved the old script to the process block. Because we are creating a script and not a function the signature of function Test-DynamicValidateSet {…}  needs to be removed. To generate the validateSet I replaced the line $arrSet = … with

$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
$param = 'help', '--names'

$arrSet = $(& $mixBatPath $param[0] $param[1])

This populates the $arrSet  with all valid task. I also changed the value of the variable $ParameterName  to ‘Task’  and renamed the variable $Path  to $Task

Test

A short test shows, the command mix  does work, the command mix help  does not. Reason for that is, we assign the first value to the parameter $Task .

Iteration 2

Changes

The call to mix.bat in the last row now get the $Task parameter as well:

& $mixBatPath $Task $newArgs

Test

mix  works, mixd help  works. Awesome! Lets try auto completion. mix [tab]  …

This is weird. The auto completion takes it times (this is actually the time mix help –names  takes to return all valid tasks) yet the auto completion fills in file names from that folder… To fix that we need to make it clear, that our dynamic parameter is actually the first parameter. So after we set the $ParameterAttribute.Position = 0  (it was 1) we repeat our test.

mix  works, mixd help  works, mix [tab]  works, mix he[tab]  works also. What about arguments to parameters? like mix help –names ?

mix.ps1: A positional parameter cannot be found that accepts argument "--names".

Damn.

Iteration 3

Changes

OK, we need positional arguments. Lets add some.

[Parameter(Mandatory=$false, Position=1)]
[string]$p1,
[Parameter(Mandatory=$false, Position=2)]
[string]$p2,
[Parameter(Mandatory=$false, Position=3)]
[string]$p3,
[Parameter(Mandatory=$false, Position=4)]
[string]$p4,
[Parameter(Mandatory=$false, Position=5)]
[string]$p5,
[Parameter(Mandatory=$false, Position=6)]
[string]$p6

I don’t like that approach, because this script will fail on having more than seven parameters (our dynamic and $p1  - $p6 ) with the aforementioned error message.

We also have to forward our new parameters to the mix.bat :

& $mixBatPath $Task $p1 $p2 $p3 $p4 $p5 $p6

OK, besides the now unused “flatten possible array parameter” logic and our “it will fail on having eight or more parameters” problem, how good are we?

Test

All tests in the test cases pass. Yet we have some unfinished problems.

Problems with this solution

  1. We can have only a fixed amount of parameters. This is not a big problem (as we can add more parameters in the signature), but this is neither elegant nor good practice.
  2. We now completely omit the “flatten array logic”. I have to admit, I’m not sure if  this is still needed, so I asked the original contributor of this logic but still wait for response.
  3. Most of the code was copied from our resource. We clearly added some of our own logic, yet we probably shouldn’t use this code without asking for permissions. I asked the author if I could use this snippet and wait for a response.
  4. Even if I omit the “flatten array logic” I tripled the Lines Of Code. I don’t know if the auto complete feature is worth this much code (read about code as a liability here)

As soon as the problem 3 is clarified I will upload the file here. As soon as the other problems are clarified (and/or fixed) I create a pull request in GitHub to upstream the changes.