Uncommon zsh shell techniques, part 1
Some of these work in other shells too, but I only use zsh these days.
Anonymous functions
() { cp "$1" /tmp/ } filename
This works the same as cp filename /tmp/
, but it’s more convenient in some cases:
- When you’re running the same command on many filenames, and want to use command history (up + enter) to modify the filename. You don’t have to position the cursor onto the filename mid-command - it’s just at the end.
- When you use the argument multiple times, or need to use variable modifiers on the input:
# Print the directory containing the passed file () { echo "${1:A:h}" } file.csv # Transcode to mp3, unless the source is already mp3 () { newfn="${1:r}.mp3"; if [[ "$1" != "$newfn" ]]; then echo "$1 -> $newfn"; ffmpeg -i "$1" "$newfn"; fi } foo.flac
It’s also safe to use with spaces & other sensitive characters.
-c with variables
# List all files without extensions
find . -type f -exec zsh -c 'printf "%s\n" "${1:r}"' . '{}' ';'
It’s tempting and common to place {}
directly into the argument passed to zsh -c
, like this:
# DON'T DO THIS!
find . -type f -exec zsh -c 'printf "%s\n" "{}"' ';'
This will cause problems for filenames that contain special characters like "
, because find
(and many other programs) won’t escape them. How could it? It doesn’t know what escaping strategy to use, because it depends on the command you’re invoking. For example, we’re using zsh here, but if you were writing inline Python code you’d need to escape the string following Python rules instead of zsh.
By passing the argument to zsh -c
instead, you can use $1
in zsh as a variable with all the safety that comes along with that. You also get to use variable modifiers like :r
.
Note also:
- I passed
.
to act as the$0
argument to the command-line script. I’m not using the value of it in the script, but I need to pass it so that the filename is passed as$1
. - I used
printf
instead ofecho
becauseecho
will try to handle filenames like-n
as an argument.
Globbing flags and qualifiers
I find the zsh documentation on filename generation pretty hard to read, but here are some examples I use that might help:
Globbing flags
Globbing flags appear right before the part of the glob you want to apply them on. I usually apply them to the whole pattern so I put them right at the start.
These require extended_glob
(see docs) to be set.
# Match all .jpg files, matched case-insensitively (so it also includes
# *.JPG, *.Jpg, etc.), like the option nocaseglob.
setopt extended_glob
echo (#i)*.jpg
Glob qualifiers
Glob qualifiers are suffixes that modify how the glob works.
# List all jpg and gif files. No matches = no arguments.
echo *.jpg(N) *.gif(N)
Adding (N)
to a glob string makes it expand to no arguments if there are no matches (same as the null_glob
option). Without this, you’ll typically either pass the raw argument *.jpg(N)
if there are no matches, or zsh won’t run the command and will raise an error instead.
The exact default behaviour depends on the setting of options null_glob
, nomatch
, and null_glob
.
Together
You can use them together:
# Match GIF, JPG, JPEG, HEIF, and AVIF extensions
# with case-insensitive matching,
# and run with no arguments if no files match.
setopt extended_glob
echo "Image files:" (#i)*.(gif|jpe#g|heif|avif|png)(N)