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.