I was trying to write a short script which would write all the executable programs found in $PATH
:
for dir in $(tr ':' ' ' <<<"${PATH}"); do
for pgm in $dir/*; do
if command -v "${pgm}" >/dev/null 2>&1; then
echo "${pgm}"
fi
done
done | sort >file
In bash, it works as expected, but zsh stops processing the script as soon as a filename generation fails in the inner loop:
for pgm in $dir/*; do
^^^^^^
...
done
As a result, since my $PATH
contains a directory which doesn't contain any file (/usr/local/sbin
), in zsh, the script fails to write the executables found in the directories afterwards.
Here's another code showing the same issue:
for f in /not_a_dir/*; do
echo 'in the loop'
done
echo 'after the loop'
In bash, this command outputs:
in the loop
after the loop
And exits with the code 0
.
While in zsh, the same command outputs:
no matches found: /not_a_dir/*
And exits with the code 1
.
The difference of behavior between the shells seems to come from the nomatch
option, which is described in man zshoptions
:
NOMATCH (+3) <C> <Z>
If a pattern for filename generation has no matches, print an error, instead of leaving it unchanged
in the argument list. This also applies to file expansion of an initial~
or=
.
And also explained in man zshexpn
(section FILENAME GENERATION):
The word is replaced with a list of sorted filenames that match the pattern. If no matching pattern is found, the shell gives an error message, unless the NULL_GLOB option is set, in which case the word is deleted; or unless the NOMATCH option is unset, in which case the word is left unchanged.
Because if I unset nomatch
, zsh behaves like bash:
unsetopt nomatch
for f in /not_a_dir/*; do
echo 'in the loop'
done
echo 'after the loop'
Now I understand the difference of behaviors between bash and zsh, and why the script raises an error in zsh, but I want to understand why a failed filename generation makes zsh immediately stop processing a script.
So, I tried to reproduce the same issue by replacing the failed filename generation with a failed command (by executing not_a_cmd
):
for f in ~/*; do
not_a_cmd
done
echo 'after the loop'
But the output of this script is almost identical in both shells (apart from the error messages due to not_a_cmd
). In particular, both shells print:
after the loop
And both shells exit with the code 0
.
Why does a failed filename generation (like for f in /not_a_dir/*
) make zsh stop processing a script, but not a failed command (like not_a_cmd
)?
I'm using zsh 5.6.2-dev-0 (x86_64-pc-linux-gnu)
.
Best Answer
It's a feature, not a bug. ?
As it says in Zsh's manual, section "Errors":
As for the reasoning behind it, I found this email exchange in the Zsh mailing list archives: