Zsh Scripting Guide

By Jan Matějka, June 04, 2019

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

  1. By subscript flag (I) we indicate we want to get an index of value -v from array pargs 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.

  2. Then the index is arithmetically evaluated 6 as either (( 0 )) which yields false, or (( n )) which yields true.

  3. 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

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

https://github.com/roman-neuhauser

http://rants.sigpipe.cz/

10

Surprisingly this can be done in Bash as well via ${!aarray[@]}