Last Updated: March 19, 2020
·
14.06K
· robjens

ZSH option parsing with zparseopts

Here's a little trick I had to pull in order to get the desired result from my option parser implementation. I was using zparseopts, a zsh equivalent of bourne shells getopt(s).

Let me illustrate the problem I faced, if anyone has a (more) idiomatic way of doing this (aka I overlooked some feature/option) I'd be interested to find out.

What I wanted was a repeated option parsing. I don't have any default values, I don't like using a opts array either by I guess I could demonstrate that route afterwards as well, since it perfectly highlights my dilemma.

Say I have a function which takes multiple file arguments. Of course I could use a colon or space separated list of file names but I wanted/needed them as separate switches.

#!/usr/bin/env zsh

function zomg/check()
{
    zparseopts -D -E -- f+:=files -files+:=files
}

What I've just done is declare a function named zomg/check and in it I've placed our option parser which has the -D switch (to remove any options found from the positional arguments list) and the -E switch (to not stop when unknown options - not in the spec - are encountered) defined. Those two I generally like to keep around although technically they aren't really needed here yet.

The part after -- are called specs and they must be in the format opt[=array]. In our case, we have two specs which both refer to the same array namely files. The first is the -f switch and the second is the long options --files (which is denoted by the single prefix - hyphen). The + plus sign signals that we wish to have this option repeatedly used, if we do not explicitly require this, only the last provided -f or --file will be stored. The single : colon notates that this option is optional in fact.

To make it a little easier, here's a rewrite of the same function without the extra's just bare bones:

zparseopts -- f+:=storage -files+:=storage

Now our storage is of the type array so we should be able to reference it by using the index to retrieve elements.

Lets take a quick look how the variable array (list) turns out after we pass it zomg/check -f foo -f bar -f baz

print $storage[@]
-f foo -f bar -f baz

As you can see, this is the repeated pattern and checking how many elements this array has

print ${#storage}

Will tell us 6 as a result. That leaves us with the task to filter out the values. As this array is not of the associative kind (dictionary, hash-map, key/value store) we cannot just simply do ${(v)storage} and get our foo bar baz 3-element list returned. As I've come up with, I found two reasonably easy ways of getting the values out.

The first is the easiest: we drop every occurrence of -f in our array:

storage=("${(@)storage:#-f}")
print $storage[@]

Will return foo bar baz and testing the length of the array using ${#storage} will return 3 as answer. It would fail to remove any --files from the array though.

The second method is a bit more contrived albeit more fun as well and doesn't fail to remove any long option synonym/alias switches. We reason as if we'd have a dictionary of key-value pairs (use a helper) to get a list of even and odd integers. Odd numbers starting at 1 will be the keys and subsequently the even numbers form our values:

typeset -A helper
helper=($(seq 1 ${#storage}))

What we've just done is created a associative array which is populated by the sequence of numbers from 1..to..number of elements in array in this case 6. The zparseopts generated array ensures us a even number of elements as long as we test for zero value on the second element. I always use test -z ${storage[2]} for this.

With these key/value pairs of 1 and 2, 3 and 4 and 5 and 6 we can now easily obtain the elements from our regular 1-dimensional array by means of our values as index number:

for item in ${(@v)helper}
do
    print ${storage[$item]}
done

This will loop numbers 2, 4 and 6 which correlate to the positions in the list (-f foo -f bar -f baz) and as such, pull the data out for us.