Skip to content

grep exits 0, 1 or 2

Level: 101 · you wrote if grep -q ... and want to know what it cannot tell you

One line: grep exits 0 when it selected a line, 1 when it selected none, and 2 when something went wrong: a missing file, a bad pattern. if grep -q sees only zero or not zero, so a file that is not there reads as "not found"; a case on $?, or fish's switch $status, tells the three apart. grep -c prints 0 and still exits 1, and set -e stops a script at a grep that matched nothing.

Measured

On demo/hosts.txt, four lines, two of them with darkfi:

Verified output of grepexit_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ grep darkfi hosts.txt; echo "status $?"
darkfi   10.0.0.20   node
darkfi   10.0.0.21   wallet
status 0

$ grep zzz hosts.txt; echo "status $?"
status 1

$ grep darkfi nosuch.txt; echo "status $?"
grep: nosuch.txt: No such file or directory
status 2

$ grep "[" hosts.txt 2>/dev/null; echo "status $?"
status 2

$ grep -q darkfi hosts.txt; echo "status $?"
status 0

$ grep -c zzz hosts.txt; echo "status $?"
0
status 1

$ grep -s darkfi nosuch.txt; echo "status $?"
status 2

$ grep -s darkfi hosts.txt nosuch.txt; echo "status $?"
hosts.txt:darkfi   10.0.0.20   node
hosts.txt:darkfi   10.0.0.21   wallet
status 2

$ grep -qs darkfi hosts.txt nosuch.txt; echo "status $?"
status 0

$ if grep -q darkfi hosts.txt; then echo "found"; else echo "not found"; fi
found

$ if grep -q darkfi nosuch.txt 2>/dev/null; then echo "found"; else echo "not found"; fi
not found

$ grep -q darkfi nosuch.txt 2>/dev/null; case $? in 0) echo "found" ;; 1) echo "not found" ;; *) echo "grep failed" ;; esac
grep failed

$ if ! grep -q zzz hosts.txt; then echo "no zzz"; fi
no zzz

$ bash -c "set -e; grep -q zzz hosts.txt; echo reached"; echo "status $?"
status 1

$ seq 1 1000000 | grep -q 1; echo "PIPESTATUS ${PIPESTATUS[*]}"
PIPESTATUS 141 0

$ set -o pipefail; seq 1 1000000 | grep -q 1; echo "status $?"; set +o pipefail
status 141
  • 0, 1 and 2 are found, not found, and could not look. A pattern grep cannot compile, [, is also 2; its message is not the same on the two machines, which is why this line throws it away. GNU grep and BSD grep shows both wordings.
  • -q prints nothing, so the status is the whole answer.
  • grep -c zzz printed 0 and exited 1. The count is an answer, but no line was selected, and the status says so.
  • -s silences the message, not the status. grep -s darkfi nosuch.txt printed nothing and still exited 2.
  • A match in one file does not cancel an error in another. grep -s darkfi hosts.txt nosuch.txt printed both lines and exited 2. Add -q and the same search exits 0: with -q, one selected line is enough.
  • if cannot see the difference. if grep -q darkfi nosuch.txt took the "not found" branch, although nothing was searched. The case on $? said "grep failed".
  • set -e treats 1 as a failure. The child bash stopped at grep -q zzz and never printed reached; its status was grep's 1.
  • grep -q stops at the first match, and the writer pays for it. seq still had lines to write when grep exited, and the next write killed it with SIGPIPE, 141. The pipeline's status is grep's 0; under pipefail it is 141, a failure nobody made. A pipeline reports its last command measures the same trap with yes | head.

On a Mac

Nothing changes: the key above is shared, and BSD grep uses the same three numbers as GNU grep for every line on it. The difference between the two is in a message: with -q and a missing file after the match, BSD grep prints the missing-file message and GNU grep prints nothing. The status is 0 on both, and the line above passes -s so the key can be shared. GNU grep and BSD grep records both.

In zsh and fish

zsh spells all of it as bash does, and keeps every stage's status in $pipestatus:

Verified output of grepexit_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ grep -q darkfi hosts.txt; echo "status $?"
status 0

$ grep -q zzz hosts.txt; echo "status $?"
status 1

$ grep -q darkfi nosuch.txt 2>/dev/null; echo "status $?"
status 2

$ if grep -q darkfi hosts.txt; then echo "found"; fi
found

$ grep -q darkfi nosuch.txt 2>/dev/null; case $? in (0) echo "found" ;; (1) echo "not found" ;; (*) echo "grep failed" ;; esac
grep failed

$ zsh -fc "setopt errexit; grep -q zzz hosts.txt; echo reached"; echo "status $?"
status 1

$ seq 1 1000000 | grep -q 1; echo "pipestatus $pipestatus"
pipestatus 141 0

The case patterns are written (0), with an opening parenthesis, which zsh accepts. setopt errexit is zsh's set -e, and it stopped at the grep that matched nothing just as bash did.

fish has its own words for every piece:

Verified output of grepexit_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ grep -q darkfi hosts.txt; echo "status $status"
status 0

$ grep -q zzz hosts.txt; echo "status $status"
status 1

$ grep -q darkfi nosuch.txt 2>/dev/null; echo "status $status"
status 2

$ if grep -q darkfi hosts.txt; echo "found"; else; echo "not found"; end
found

$ grep -q darkfi hosts.txt; and echo "found"
found

$ grep -q zzz hosts.txt; or echo "not found"
not found

$ if not grep -q zzz hosts.txt; echo "no zzz"; end
no zzz

$ grep -q darkfi nosuch.txt 2>/dev/null; switch $status; case 0; echo "found"; case 1; echo "not found"; case "*"; echo "grep failed"; end
grep failed

$ set n (grep -c zzz hosts.txt); echo "count $n, status $status"
count 0, status 1

$ if test (grep -c darkfi hosts.txt) -gt 1; echo "more than one"; end
more than one

$ seq 1 1000000 | grep -q 1; echo "pipestatus $pipestatus"
pipestatus 141 0

$ if [[ -s hosts.txt ]]; echo "not empty"; end
fish: Unknown command: '[[ -s hosts.txt ]]'
fish: 
if [[ -s hosts.txt ]]; echo "not empty"; end
   ^~~~~~~~~~~~~~~~~^
(exit status 0)

$ [[ -s hosts.txt ]]; echo "status $status"
fish: Unknown command: '[[ -s hosts.txt ]]'
fish: 
[[ -s hosts.txt ]]; echo "status $status"
^~~~~~~~~~~~~~~~~^
status 127
  • $status, not $?. $? is a parse error in fish, measured in the pipeline lesson.
  • if grep -q ...; ...; end has no then and no fi. and, or and not test the previous command's status, and switch $status with case "*" is the three-way answer.
  • set n (grep -c zzz hosts.txt) kept grep's status: count 0, status 1.
  • There is no [[ ]]. fish looked for a command named [[ -s hosts.txt ]] and printed Unknown command. On its own that is status 127. Inside if, the failed condition just skipped the body, and the child fish exited 0: a bash test pasted into a fish script prints an error and carries on as if the answer were no. test, as in the test (grep -c darkfi hosts.txt) -gt 1 line, works in fish. On an Ubuntu machine with the command-not-found package installed, GitHub's runners among them, fish hands an unknown name to that package's helper instead, and the first line reads [[ -s hosts.txt ]]: command not found. The example pins fish's own handler so both machines show fish's words.
bash zsh fish
last status $? $? $status
found? if grep -q p f; then ...; fi the same if grep -q p f; ...; end
not found? if ! grep -q p f the same if not grep -q p f
0, 1 or 2 case $? in 0) ... ;; 1) ... ;; *) ... ;; esac the same switch $status; case 0; ...; case 1; ...; case "*"; ...; end

If you are coming from another library

  • Rust. Standard error, and exit status ↗ is the other side of this page: the status a program leaves behind is the one number a shell's if reads, and a program that prints an error and exits 0 has reported success to everything but a person.
  • Encodings. grep on text that is not ASCII ↗ has the case where 0 is not the whole truth: in a UTF-8 locale, BSD grep skips a line it cannot decode, says nothing, and still exits 0.

See also