🔤 Zsh Native Scripting Handbook | Zi

Salvydas Luklosius
6 min readJul 22, 2022

How do access all array elements in a shell? The standard answer: use @ subscript, i.e. ${array[@]}. However, this is the Bash & Ksh way (and with the option KSH_ARRAYS, Zsh also works this way, i.e. needs @ to access the whole array). Z shell is different: it is $array that refers to all elements anyway. There is no need for the @ subscript.

So what use has @ in the Zsh-world? It is: " keep array form" or " do not join". When is it activated? When the user quotes the array, i.e. invokes"$array", he induces joining of all array elements (into a single string). @ is to have elements still quoted (so empty elements are preserved), but not joined.

Two forms are available, "$array[@]" and "${(@)array}". The first form has an additional effect - when an option KSH_ARRAYS is set, it indeed induces referencing to the whole array instead of a first element only. It should then use braces, i.e.${array[@]}, "${array[@]}" ( KSH_ARRAYS requirement).

In practice, if you’ll use @ as a subscript - [@], not as a flag - ${(@)...}, then you'll make the code KSH_ARRAYS-compatible.

Glob-flags #b and #m require setopt extended_glob. Patterns utilizing ~ and ^ also require it. Extended-glob is one of the main features of Zsh.

This preserves empty lines because of double-quoting (the outside one). @-flag is used to obtain an array instead of a scalar. If you don't want empty lines preserved, you can also skip@-splitting, as is explained in the Information section:

Note: $(<...) construct strips trailing empty lines.

Note that instead of four double-quotes ", an idiom that is justified (simply suggested) by the Zsh documentation (and was used in the previous paragraph, in the snippet... "${(@f)"$(<path/file)"}" ...), only two double-quotes are being used. I've investigated this form with the main Zsh developers on the[email protected] mailing list and it was clearly stated that single, outside quoting of${([email protected])...} substitution works as if it was also separately applied to$(command ...) (or to $(<file-path)) inner substitution, so the second double-quoting isn't needed.

dirname and basename can be skipped by:

Read more: zsh: 14 Expansion.

Symbolic links can be turned into an absolute path with:

To have the grep -v effect, skip the M-flag. To grep case-insensitively, use \#i glob flag (...:#(#i)\*query*}).

Side-note: (M) flag can be used also with ${(M)var#pattern} and other substitutions, to retain what's matched by the pattern instead of removing that.

Suppose you have a Subversion repository and want to check if it contains files not under version control. You could do this in Bash style like follows:

Those are 3 forks: for svn status, for echo, and for grep. This can be solved by the :# substitution and (M) flag described above in this section (just check if the number of matched lines is greater than 0). However, there's a more direct approach:

If the extendedglob option cannot be used for some reason, this can be achieved also without it, but essentially it means that the alternative (i.e.|) of two versions of the pattern will have to be matched:

In general, multi-line matching falls into the following idiom ( extended glob version):

It does a single fork (calls svn status). The ${~variable} means (the~ init): "the variable is holding a pattern, interpret it". All in all, instead of regular expressions we were using patterns (globs) (see this section).

The ~ is a negation -- match \*abc* but not .... Then, ^ is also a negation. The effect is:\*ABC* but not those that don't have \*efg* which equals to: \*ABC* but those that have also \*efg*. This is a regular pattern and it can be used with:# above to search arrays, or with the R-subscript flag to search hashes (${hsh[\(R)\*pattern*]}), etc. The inventor of those patterns is Mikael Magnusson.

#m flag enables the $MATCH parameter. At each // substitution, $map is queried for character-replacement. You can substitute a text variable too, just skip[@] and parentheses in the assignment.

Ternary expression is known from the C language but exists also in Zsh, but directly only in a math context, i.e.\(( a = a > 0 ? b : c )). The flexibility of Zsh allows such expressions also in a normal context. Above is an example.:+ is "if not empty, substitute ..." :- is "if empty, substitute ...". You can save a great number of lines of code with those substitutions, it's normally at least 4-lines if condition or lengthy &&/|| use.

A one-line “if var = x, then …, else …”. Again, can spare a great amount of boring code that makes a 10-line function a 20-line one.

\## is: "1 or more". (#c2,2) is: "exactly 2". A few other constructs: # is "0 or more", ? is "any character",(a|b|) is "a or b or empty match". #b enables the $match parameters. There's also #m but it has one parameter$MATCH for whole matched text, not for any parenthesis.

Enable the -U flag for the array so that it guards elements to be unique, or use the u-flag to make unique elements of an array.

Thanks to in-substitution code-execution capabilities it’s possible to use s-flag to apply it to multiple lines:

There is a problem with the (s::) flag that can be solved if Zsh is version 5.4 or higher: if there will be single input column, e.g. list=( "column1" "a,b") instead of two or more columns (i.e. list=( "column1,column2" "a,b" )), then(s::) will return string instead of 1-element array. So the index [4] in the above snippet will index a string, and show its 4-th letter. Starting with Zsh 5.4, thanks to a patch by Bart Schaefer ( 40640: the (A) parameter flag forces array result even if...), it is possible to force array-kind of result even for a single column, by adding(A) flag, i.e.:

\[[:space:]] contains unicode spaces. This is often used in conditional expression like [[ -z ${array[(r)...]} ]].

Note that Skipping grep that uses :# substitution can also be used to search arrays.

z-flag means: split as if Zsh parser would split. So quoting (with backslashes, double quoting, and others) is recognized. Obtained is array( "key" "value") which is then de-quoted with Q-flag. This yields original data, assigned to hash deserialized. Use this to e.g. implement an array of hashes.

Note: to be compatible with setopt ksharrays, use [@] instead of (@), e.g.:...( "${(Q)${(z)serialized[@]}[@]}" )

This method works also with Zsh. The drawback is the use of eval, however, no problem may occur unless someone compromises variable's value, but as always, eval should be avoided if possible.

Following code checks, if there is a git subcommand $mysub:

Those are 4 forks. The code can be replaced according to this guide:

The result is just 1 fork.

A project was needing this to do some Zle line-continuation tricks (when you put a backslash-\ at the end of the line and press enters — it is the line-continuation that occurs at that moment).

The required functionality is: in the given string, count the number of apostrophes, but only the unquoted ones. This means that only apostrophes with null or an even number of preceding backslashes should be accepted into the count:

Below follows a variation of the above snippet that doesn’t use math-code execution:

This is possible thanks to (S) flag - non-greedy matching, ([\\][\\])# trick - it matches only unquoted following(\'\'##) characters (which are the apostrophes) and a general strategy to replace anything-apostrope(s) (unquoted ones) with the-apostrope(s) (and then count them with ${#buf}).

Originally published at https://wiki.zshell.dev on July 22, 2022.

--

--