3

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?

2 Answers 2

8

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.

5
  • Hmm, interesting. I figured it must be a two-step expansion but I didn't know about nomatch. Funnily, it changes the behavior but still doesn't quite produce the same. Now expansion seems to "work" but mv errors out with cannot stat */*.b which is correct, somehow, but still different to the behavior in interactive mode...
    – jhr
    Commented Apr 28, 2015 at 14:47
  • 1
    Yup. My conjecture is that you have csh_null_glob set in your interactive setup, which suppresses the pattern that doesn't match anything.
    – Celada
    Commented Apr 28, 2015 at 14:51
  • 3
    See also the (N) glob qualifier to turn nullglob on on a per-glob basis. mv */*.{a,b}(N) . or mv */*.{a,b(N)} .. Here you'd rather want mv */*.(a|b) . or mv */*.[ab] . though. Commented Apr 28, 2015 at 15:07
  • 1
    Actually, this isn't no_match, but null_glob. With no_nomatch, mv would complain that it can't find */*.b. Commented Apr 28, 2015 at 23:27
  • 1
    @Gilles you're right. I chased down the wrong path with nomatch. I edited my answer, though perhaps I should just delete it.
    – Celada
    Commented Apr 28, 2015 at 23:53
2

When you run zsh interactively, it reads your ~/.zshrc (and also the system /etc/zshrc but if your administrator isn't naughty that isn't the culprit). You're likely to set a few options there that modify how zsh expands commands, in particular extended_glob (which should be the default, if zsh didn't choose to be backward compatible with the early 1990s when this option doesn't exist). These options won't be set when you execute a script.

In your case, what's happening is that the command mv */*.{a,b} . is first parsed into a list of words mv, */*.a, */*.b, . (unless you turn off brace expansion). Then each word that contains a wildcard character is treated as a glob pattern. By default, in zsh, if a glob pattern doesn't match any file, the command isn't executed and zsh signals an error. Evidently, in your .zshrc, you've turned on the null_glob option, which causes the non-matching pattern to expand to an empty list of words instead (i.e. it's removed). When you execute your script, the second pattern */*.b doesn't match any file, which triggers the error that you see. When you execute the command interactively, that pattern is removed.

You can turn on the null_glob option explicitly in your script with setopt null_glob. You can also turn it on for a specific pattern with the N glob qualifier:

mv */*.{a,b}(N) .

Rather than using brace expansion here, you should use the or operator, which is available in zsh (unlike plain sh), because that's exactly what you mean — not two patterns, but files that match either pattern.

mv */*.(a|b) .

That way there's a single pattern, and you'll only get an error if no file matches that single pattern.

If no file matches at all, this command will result in an error. You can't just silence it with (N) because that would cause the invalid command mv . to be executed. Instead, first test if there are matches, and execute mv only if there are.

files=(*.(a|b)(N))
if ((#files)); then mv -- $files[@] .; fi

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .