Zsh Scripting Guide¶
By Jan Matějka, June 04, 2019
Table of Contents
Introduction¶
This guide is continuation of Shell Scripting Survival Guide which got us surprisingly far but still falls short of achieving its goals. In this guide, we will pick up Zsh and show further techniques to boost convenience, productivity and increase the problem space that is possible to solve with shell scripting (mostly aspirational at the moment).
Understanding topics at Shell Scripting Survival Guide is essential reading for this guide.
Conventions¶
Same conventions as in Shell Scripting Survival Guide apply.
Simple Techniques¶
Just like in Shell Scripting Survival Guide, we will start with demonstrating some simple techniques and then continue to Architectural Techniques.
Array Expansion¶
$foo
is sufficient to expand an array in Zsh 8. Whereas in Bash you would only get the first
element and need to use the clunky ${foo[@]}
syntax.
This seems like a minor syntactical improvement but is incredibly annoying once you get used to Zsh and need to get back to Bash.
Array Indexing¶
Bash array indexes from 0
while Zsh indexes start at 1
. This is good and important for Is
a Value Present in Array?.
Is a Value Present in Array?¶
Assuming an array pargs
and boolean flag -v
, we may have either pargs=( -v )
or
pargs=( "whatever but -v" )
and you want to know whether '-v'
is an element in pargs
.
There is no good solution in Bash. Zsh solution is bit cryptic but essentially simple:
if (( ${pargs[(I)-v]} )); then
printf -- "-v is present in pargs"
else
printf -- "-v is not present in pargs"
fi
The trick here is
By subscript flag
(I)
we indicate we want to get an index of value-v
from arraypargs
7. If the value is not present in array, 0 is returned and that is ok, because 0 is not a valid index in Zsh.Then the index is arithmetically evaluated 6 as either
(( 0 ))
which yields false, or(( n ))
which yields true.And we have a simple pattern to check if a value is an element of array.
Does a key exist?¶
I did not find a way to ask directly for key presence, but we may get an array of keys of an
associative array via an expansion flag: ${(k)aarray}
and then it becomes
a Is a Value Present in Array? problem again 10.
Furthermore, we can nest the expansions in Zsh (can not be done in Bash).
Putting it together, you get:
declare -A map
map[foo]=bar
printf "foo %d\n" ${${(k)map}[(I)foo]}
printf "bar %d\n" ${${(k)map}[(I)bar]}
Architectural Techniques¶
Argument Parsing¶
Zsh provides zshparseopts
3 that is much easier than whatever Bash has to offer:
#!/usr/bin/env zsh
SELF=${0##*/}
. foo_prelude
declare -a pargs
declare -A paargs
zparseopts -K -D -apargs -Apaargs flag -key:
printf "flag=%s key=%s\n" ${pargs[(I)-flag]} ${paargs[--key]}
printf "%s\n" "$*"
result:
% zsh foo-cmd1.zsh foo
flag=0 key=
foo
% zsh foo-cmd1.zsh -flag --key val foo
flag=1 key=val
foo
zparseopts
will consume argv according to the given spec and set the results into arrays
pargs
and paargs
. Note it won’t just parse it, it will consume it from argv. We do not
need to adjust argv as we do in Bash.
pargs
and paargs
are named as such by convention as Parsed Arguments, and Parsed Associative
Arguments. The p
prefix is not an overspecification here as it is often desirable to have
args
variable free to use for command that is about to be executed 1 using the Breaking Long
Lines technique from Shell Scripting Survival Guide
The ${pargs[(I)-flag]}
is a trick described at Is a Value Present in Array?
Debugging¶
This is in principle the same as in bash but we can use more succinct argument parsing and we get much better looking output as Zsh will include the function names and line numbers in the output of xtrace 4.
foo code:
#!/usr/bin/env zsh
SELF="${0##*/}"
. foo_prelude
declare -a pargs
declare -A paargs
zparseopts -K -D -a pargs -Apaargs x
(( ${pargs[(I)-x]} )) && {
set -x
export FOO_XTRACE=true
}
foo_dispatch $SELF "$@"
Since x
is just a boolean flag, it will appear as was specified on command line in pargs
.
We check if -x
is in pargs
using the Is a Value Present in Array? technique and the rest
is the same as in Bash.
Practical Case for Scripting¶
Writing programs in shell scripts can be very efficient on the time spent programming as exemplified by the famous case of most frequently used words problem solved by D. E. Knuth and M. D. McIlroy 5.
It may include features for free. Assuming your code uses curl
internally and you want to access
the remote server via a proxy. All you need to do is export http_proxy
or if you integrate
envdir
you get it for free even in your configuration options.
Shell friendly solutions are also the most simple solutions to implement and just knowing these may help you focus on the problem and not on cruft around it like relatively complicated configuration formats or argument parsing that may get you sidetracked.
The shell solutions are applicable in general purpose languages as well. For example, argument parsing with subcommand dispatch becomes much simpler and once you see that, all the argument parsing libraries become needless monstrosities.
While you can expect shell script to not be efficient in terms of computer resources and especially
large data sets. They will often do the job good enough and occasionally may perform even better
because they will be done with the problem before languages like python will even just load its
runtime and standard library. Or the script may also be trivially parallelizable via xargs
.
If you look at any shell script, it is just a glue for more performant or complex
programs like git
or curl
, they are just some else’s code. Shell scripts should be nothing
more than a glue. With the demonstrated architecture, if performance or complexity becomes a
problem, you can easily upgrade the critical parts into more suitable languages. The only difference
will be that it is your code and you just to figure out the proper interfaces.
Finally, it would be good to know for what kind of problems shell scripting is suitable. This is difficult. My suspicion is that it serves very well as a skeleton for any CLI program and further can solve issues that require data structures not more complicated than a one or two hashmaps with low cyclomatic complexity.
Philosophical Case for Scripting¶
Simple solutions are sometimes obvious and sometimes takes years to find. By using shell for problems that seem should be solvable in shell, you may find simpler solutions than you would get with more powerful language.
It just might be the difference between getting the core working and iterating it into something publishable and between getting bogged down with trying to solve too many problems at once.
Techniques Applied¶
For real world software using these techniques, you may check out
https://github.com/jan-matejka/soapcli TUI SOAP client
https://github.com/jan-matejka/cnb CLI interface to http://cnb.cz daily rates
Acknowledgements¶
Thanks to Roman Neuhauser
9 who I learned much from.
References¶
- 1
This is the case of long argv splitting at
Breaking long lines
at https://www.matejka.ninja/software/lang/bash.html- 3
man 1 zshmodules
https://linux.die.net/man/1/zshmodules- 4
XTRACE in
man 1 zshoptions
https://linux.die.net/man/1/zshoptions- 5
http://www.leancrew.com/all-this/2011/12/more-shell-less-egg/
- 6
ARITHMETIC EVALUATION in man 1 zshmisc
- 7
ARRAY PAMETERS > Subscript Flags in man 1 zshparam
- 8
PARAMETER EXPANSION > Parameter Expansion Flags in man 1 zshexpn
- 9
- 10
Surprisingly this can be done in Bash as well via
${!aarray[@]}