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)
.
printf "shopt -s failglob \n echo *.foo \n echo afterwards" | bash
will print "afterwards", butprintf "echo *.foo \n echo afterwards" | zsh
andbash -c 'shopt -s failglob; echo *.foo; echo afterwards'
won't? I'm too lazy to search if this has been discussed to depth on this site -- but please change the title of your question; it's begging for kneejerk deletion; there are a hundred thousands cases where zsh may stop processing a script ;-)afterwards
is not printed. Out of the 3 bash commands (the two you provided plus the one I just mentioned), two fail to printafterwards
. I'm not sure, but it makes me think that the one which does printafterwards
is the exception:printf "shopt -s failglob \n echo *.foo \n echo afterwards" | bash
. For some reason, the second newline is able to prevent bash from interrupting the script.