8

In zsh

  • rm foo.bar prints rm: foo.bar: No such file or directory.
  • rm foo.bar 2>/dev/null prints nothing, as I expect.

But if the command contains pattern matches, the error is not suppressed by 2>/dev/null:

  • rm *.bar prints zsh: no matches found: *.bar.
  • rm *.bar 2>/dev/null prints the same.

Is there a general way to suppress error messages in zsh? One simple method for all kinds of error messages.

Dr. Gut
  • 908
  • 1
  • 10
  • 20

1 Answers1

12

When you try to run rm *.bar and there is no file that matches *.bar, basically two things can happen:

  • The POSIX behavior is the shell runs rm with literal *.bar as an argument. The tool tries to remove a file literally named *.bar (as if you run rm '*.bar'), it fails and prints something like rm: *.bar: No such file or directory. This is how the POSIX shell (sh) and compatible shells behave.

  • Non-POSIX behavior is the shell detects there is no match, prints something like no matches found: *.bar and doesn't run rm at all. This is how Zsh behaves by default. (For comparison: in Bash you can switch to this behavior by shopt -s failglob.)

An error from rm (the former case) is printed to the stderr of rm. An error from the shell (the latter case) is printed to the stderr of the shell.

The redirection in rm *.bar 2>/dev/null affects the stderr of rm only+. In your example rm doesn't even run.

To redirect the stderr of the main shell you need exec. By "the main shell" I mean the interactive shell where you type; or, in case of running a script, the shell interpreting the script. Example:

exec 2>/dev/null

In Zsh you can even close it:

exec 2>&-

A reasonable approach is to duplicate the original stderr beforehand, just in case you need to use (or restore) it later. Example (note if you want to paste this code into an interactive Zsh then you should invoke setopt interactive_comments first):

exec 7>&2           # "save" stderr
exec 2>/dev/null    # redirect
rm *.bar
whatever
exec 2>&7           # restore
exec 7>&-           # close descriptor which is no longer needed

Here 7 is an arbitrary single-digit number, otherwise unused as a file descriptor. The first two lines can be written as one: exec 7>&2 2>/dev/null; similarly the last two lines can.

Note rm or any command (like whatever) invoked without stderr redirection inherits stderr from the shell. This means in the above example rm (if it ever runs) and whatever will print their error messages (if any) to /dev/null. After exec 2>/dev/null you can invoke any number or commands and they all will have /dev/null as their stderr. If you really want to suppress all kinds of error messages then this is the way.

The solution works in many shells, not only in Zsh. If you want to redirect stderr of an interactive shell then keep in mind some less sophisticated shells use stderr to print their prompt.

Note a process may redirect its own stderr (like our shell does with exec 2>…); or it can print error messages to stdout or to /dev/tty (it shouldn't, but it technically can). Therefore exec 2>/dev/null does not guarantee you will see no messages that look like error messages.

After exec 2>/dev/null you still can redirect stderr of any command on demand. E.g. if instead whatever we had:

whatever 2>&7

then its error messages would go to the original stderr we deliberately saved as file descriptor 7.


exec is not the only way to redirect stderr (or another file descriptor) of a shell. You can treat a shell (not the whole main shell though) like you treat rm in rm … 2>/dev/null. You can treat a subshell like this:

( rm *.bar ) 2>/dev/null

or just some code interpreted by the main shell:

{ rm *.bar; } 2>/dev/null

(; here is optional in Zsh, mandatory in some other shells; I just wanted the code to work outside of Zsh as well).

Any of these lines can solve your problem. In the context of suppressing error messages coming from a shell, the two lines are equivalent++.

Note the redirection starts at (/{ and ends at )/}, its scope is limited. Still you can place many commands inside the parentheses, so the "limited" scope may actually be the whole script. Or it may be just a part of the script (including whatever introduced earlier in this answer, whatever you want). The fact the redirection stops working after )/} makes this approach a neat alternative to saving and restoring stderr with exec.


+ Strictly: it affects "rm" before and after it becomes (or tries to become) rm. Bear with me. The redirection in rm … 2>/dev/null starts as a redirection performed by a forked shell that is about to replace itself with rm executable. This shell redirects its own stderr before it tries to replace itself with rm. Any error it reports after performing the redirection will go to /dev/null. E.g. if rm is not to be found then the already performed redirection will suppress the command not found error.

++ Technically ( … ) creates a subshell, { … } does not. A subshell behaves like a separate shell process that inherits variables and current working directory, but cannot change anything (like variables or current working directory) in its parent shell. It may or may not be realized as a truly separate process, it's the behavior that matters. OTOH { … } only groups commands for some purpose (in our case the purpose is redirection). This doesn't mean there's never a subshell when you use { … }; e.g. in a pipeline all parts but the last one are subshells anyway (in some shells: all parts including the last one). These technical details are irrelevant in the context of our problem alone, but in general they can make a difference.