zsh – Does Expansion Work Differently in Non-Interactive Scripts?

scriptingshellwildcardszsh

I'm currently working on a pretty simple zsh script. What I often do is something like:

mv */*.{a,b} .

When I run that within a zsh script, it seems to expand differently and fail while it works in interactive mode.

% mkdir dir
% touch dir/file.a
% ls file.a
ls: cannot access file.a: No such file or directory
% mv */*.{a,b} .
% ls file.a
file.a

So, this works, but as a script:

% mkdir dir
% touch dir/file.a
% ls file.a
ls: cannot access file.a: No such file or directory
% cat script.sh
#!/usr/bin/zsh
mv */*.{a,b} .
% ./script.sh
./script.sh:2: no matches found: */*.b

So, what's different? What am I doing wrong?

Best Answer

Both are wrong with the zsh default option settings. You can easily see what's going on by using echo as the command instead of mv.

Interactively, it looks like you have the null_glob option set. According to the zsh documentation that option is not set by default. What happens with that option unset depends on whether another option, nomatch, is set or unset. With nomatch unset (nonomatch) you would get this:

% mkdir dir
% touch dir/file.a
% ls file.a
ls: cannot access file.a: No such file or directory
% echo */*.{a,b} .
dir/file.a */*.b .

The expansion happens in 2 steps. First, */*.{a,b} is expanded to 2 words: */*.a and */*.b. Then each word is expanded as a glob pattern. The first expands to dir/file.a and the second expands to itself because it doesn't match anything. All of this means that, if you use mv and not echo, mv ought to try to move 2 files: dir/file.a (fine) and */*.b (no such file). This is what happens by default in most shells, like sh and ksg and bash.

The zsh defaults option settings are that null_glob is unset and nomatch is set. Scripts run with the default option settings (unless you change them in ~/.zshenv or /etc/zshenv, which you relly shouldn't). That means that in scripts, you get this:

% mkdir dir
% touch dir/file.a
% ls file.a
ls: cannot access file.a: No such file or directory
% cat script.sh
#!/usr/bin/zsh
echo */*.{a,b} .
% ./script.sh
./script.sh:2: no matches found: */*.b

Since */*.b does not match anything, you get an error due to nomatch.

If you insert setopt nonomatch in the script before the echo/mv command, you get back to the wrong behaviour as that I describe above: it tries to move a file that does not exist.

If you insert setopt null_glob in the script before the echo/mv command, you get the behaviour you got in your interactive shell, which is that is works.

Related Question