_execq() in Bash: Run a Command Quietly and Keep Its Status
Many shell checks only care whether a command succeeded: is the service running, does the package exist, can I reach that host. The output is not needed. _execq() runs a command with both stdout and stderr thrown away and returns its exit status, so it reads naturally inside an if. It is a two-line function, but the 2009 version got both lines wrong in ways we can learn from.
The function
# _execq() -- run a command quietly: discard stdout and stderr, keep status
# Usage: _execq command [args...]
_execq() {
if [ $# -eq 0 ]; then
printf 'Usage: _execq command [args...]\n' >&2
return 2
fi
"$@" >/dev/null 2>&1
}How it works
"$@" expands to all of the function's arguments, each one as a separate word, exactly as the caller passed them. The first word becomes the command and the rest its arguments. Nothing is re-split on spaces and nothing is glob-expanded a second time, so a filename like notes file.txt stays one argument.
>/dev/null 2>&1 sends standard output to /dev/null and then points standard error at the same place. The order matters: 2>&1 >/dev/null would copy stderr to the old stdout (your terminal) before stdout is redirected. Bash also accepts the shorter &>/dev/null, which the original used. I prefer the long form because it also works in sh and dash, where &> means something else entirely. dash reads cmd &>/dev/null as cmd & followed by a separate >/dev/null with no command attached. So the command runs in the background with its output still on screen.
There is no explicit return at the end. A function's exit status is the status of the last command it ran, which is the quiet command itself. return $? would do the same thing with more typing. The only status the function invents itself is 2 when it is called with no command at all.
Usage
if _execq ps auwx; then
echo "ps OK"
fi
_execq grep -q "two words" "notes file.txt"; echo "grep: $?"
_execq grep -q "three words" "notes file.txt"; echo "grep miss: $?"
_execq ls "no such file"; echo "ls missing: $?"
_execq no-such-command; echo "missing cmd: $?"
_execq; echo "no args: $?"ps OK
grep: 0
grep miss: 1
ls missing: 2
missing cmd: 127
Usage: _execq command [args...]
no args: 2Each command's real exit status comes through unchanged: grep's 1 for "no match", ls's 2 for "cannot access", and the shell's 127 for "command not found". The usage message is the one thing that still reaches the terminal, because it comes from _execq itself and not from the silenced command.
What was wrong with the original
Here is the core of the 2009 code:
_exeq() {
local cmd=$@;
$cmd &>/dev/null;
return $?;
}The first bug is simply the name. The header and the example said _execq, but the function was defined as _exeq, so the documented example failed:
$ _execq ps auwx; echo "_execq: $?"
bash: _execq: command not found
_execq: 127The second bug is more interesting. local cmd=$@ joins all the arguments into one string, separated by spaces, and the boundaries between arguments are lost. $cmd, unquoted, then splits that string on whitespace and expands any glob characters in it. That works for ps auwx by chance, but not for anything that needs a space inside an argument:
$ _exeq ps auwx; echo "_exeq ps: $?"
_exeq ps: 0
$ _exeq grep -q "two words" "notes file.txt"; echo "_exeq grep: $?"
_exeq grep: 2
$ _exeq ls "notes file.txt"; echo "_exeq ls: $?"
_exeq ls: 2grep received two as its pattern and words, notes and file.txt as three filenames, none of which exist. With the output thrown away you never see the error, only a wrong answer. A word like *.txt in an argument would also be glob-expanded inside the function, even if the caller had quoted it.
Why commands in strings are fragile
The original was written to be called as _execq "/bin/ps auwx", with the whole command in one string. That style is common, and it breaks in ways that are hard to see, because the shell parses a command line only once. Quotes, pipes and redirections are recognised while the line is being parsed. The result of expanding a variable is only split into words and globbed. Quotes inside a variable are just characters:
$ str='grep -q "two words" "notes file.txt"'
$ $str; echo "unquoted string: $?"
grep: words": No such file or directory
grep: "notes: No such file or directory
grep: file.txt": No such file or directory
unquoted string: 2grep was given the pattern "two (with a literal quote) and three filenames that do not exist. Pipes do not work either. _exeq "ps auwx | grep -q sshd" runs ps with the arguments |, grep, -q and sshd. ps rejects them and you get exit status 1, which looks just like "sshd is not running".
The usual reaction is to use eval "$cmd". That does re-parse the string, but it also executes anything an attacker or an odd filename can put into it, such as ; rm -rf ~. There are two better options.
If you need to build a command up in a variable, use an array. Each element stays one argument, and "${cmd[@]}" expands back to exactly those words:
cmd=(grep -q "two words" "notes file.txt")
_execq "${cmd[@]}"; echo "array: $?"array: 0If you need a pipeline or redirection, pass it as a script to a child shell, quoted once, where you can see it:
_execq sh -c 'ps auwx | grep -q "[s]shd"'; echo "pipeline via sh -c: $?"pipeline via sh -c: 0The [s]shd pattern is an old trick that stops grep matching its own command line in the ps output. For this particular job pgrep -x sshd is even cleaner. See __pgrepf().
The downside is that the new _execq takes words, not a string. Calling it the old way, _execq "ps auwx", looks for a command literally named ps auwx and returns 127. That change is deliberate: it fails clearly and immediately, instead of parsing the command wrongly without any warning.
Pitfalls
- Stdin is still connected. Only output is silenced. A command that waits for input will hang your script just as it would without
_execq. Add</dev/nullto the call (or to the function) when that is possible. - Errors are hidden too. That is the point, but it means a typo in a path looks exactly like a legitimate "no". When something behaves oddly, run the same command without
_execqbefore anything else. - Exit status only. If you need the output and the status, use
out=$(command 2>&1)and test$?immediately afterwards instead. - set -e. Under
set -e, a bare_execq falseends the script. Call it fromif,while,&&or||, which is how it is meant to be used anyway. - Portability. The function uses only POSIX syntax and runs unchanged in Bash 3.2 on macOS, Bash 4 and 5, and
dash. The array example needs Bash.
Alternatives
Many tools have their own quiet mode and do not need a wrapper: grep -q, cmp -s, systemctl is-active --quiet nginx. To check that a program exists, use command -v name >/dev/null rather than running it. And for a single use you can of course write >/dev/null 2>&1 on the command itself. The function is worth it when a script does this a dozen times.
The section on special parameters in the Bash manual explains the difference between "$@", "$*" and an unquoted $@ precisely.
History of this snippet
The 2009 post defined _exeq while documenting _execq, flattened its arguments into a string, and depended on word splitting to turn the string back into a command. This version keeps the name from the documentation, passes arguments through with "$@", uses a portable redirection and returns 2 when called without a command.