Bash

_execq() in Bash: Run a Command Quietly and Keep Its Status

By Nawawi JamiliPublished Updated 6 min read

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

bash
# _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

bash
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: $?"
output
ps OK
grep: 0
grep miss: 1
ls missing: 2
missing cmd: 127
Usage: _execq command [args...]
no args: 2

Each 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:

bash
_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:

terminal
$ _execq ps auwx; echo "_execq: $?"
bash: _execq: command not found
_execq: 127

The 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:

terminal
$ _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: 2

grep 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:

terminal
$ 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: 2

grep 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:

bash
cmd=(grep -q "two words" "notes file.txt")
_execq "${cmd[@]}"; echo "array: $?"
output
array: 0

If you need a pipeline or redirection, pass it as a script to a child shell, quoted once, where you can see it:

bash
_execq sh -c 'ps auwx | grep -q "[s]shd"'; echo "pipeline via sh -c: $?"
output
pipeline via sh -c: 0

The [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/null to 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 _execq before 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 false ends the script. Call it from if, 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.