Computing and the Command Line

Exit Status and Chaining

Every command ends with an exit status in $?: 0 for success, 1 to 255 for some kind of failure. Use it to chain commands: ; always continues, && only after success, || only after failure, and why a && b || c isn't if-then-else. Pipelines report their last command's status unless pipefail is set; PIPESTATUS shows every stage. grep -q and test as yes-or-no questions, and if as the way scripts branch on success.

  • 5 min
  • 7 steps
  • 2 questions
  • Lesson 18 of 80

In this lesson

  1. Every command reports back
  2. Chaining commands
  3. The a && b || c trap
  4. Pipelines and pipefail
  5. Asking yes-or-no questions
  6. Your turn
  7. So

Every command reports back

When a command finishes, it hands the shell a number from 0 to 255: its exit status. By convention 0 means success and anything else means failure. The shell keeps the last one in $? 1:

me@linuxbox:~$ ls -d /usr/bin
/usr/bin
me@linuxbox:~$ echo $?
0
me@linuxbox:~$ ls -d /bin/usr
ls: cannot access '/bin/usr': No such file or directory
me@linuxbox:~$ echo $?
2

Many programs return 1 for any failure; others use different numbers for different problems, listed in the EXIT STATUS section of their man page 1. grep, for instance, returns 0 if it found a match, 1 if it didn’t, and 2 if something went wrong, which makes it useful as a yes-or-no question.

$? is replaced by every command, including echo, so check it immediately. true and false are commands that do nothing but succeed and fail 1.

Top left, a terminal: ls -d /usr/bin prints /usr/bin, and echo $? prints 0; ls /nope; echo $? prints 2. Top right, exit status: 0 is success, 1-255 is some kind of failure, and man pages list the meanings under EXIT STATUS. Below, four ways to chain: semicolon, cd /tmp; ls, always runs the next; double ampersand, mkdir backup && cp notes.txt backup/, only if the first succeeded; double bar, grep -q tornado notes.txt || echo not found, only if the first failed; pipe, sort data | uniq -c, output into input, with the status of the last command, or of any failure with pipefail. Careful: a && b || c is not if/then/else; if b fails, c runs too.
Zero is success. The chaining operators read the zero. Credit: StudyCorner diagram · CC BY 4.0 · Source

Chaining commands

Four ways to put commands on one line 1 2:

  • ; runs the next command always: cd /tmp; ls
  • && runs it only if the previous one succeeded: mkdir backup && cp notes.txt backup/
  • || runs it only if the previous one failed: ping -c1 router || echo "network down"
  • | runs both at the same time, feeding the first one’s output to the second: sort data.txt | uniq -c

&& is the one you’ll use most: “do this, and if it worked, do that.” It stops a sequence the moment something goes wrong, so you don’t copy files into a folder that failed to be created, or install from a list that failed to download:

me@linuxbox:~$ sudo apt update && sudo apt upgrade
me@linuxbox:~$ cd ~/projects/shed && make

|| supplies a fallback or a complaint:

me@linuxbox:~$ grep -q tornado notes.txt || echo "no mention of tornadoes"
me@linuxbox:~$ cd /mnt/backup || echo "backup drive isn't mounted"

Quick check

What does mkdir backup && cp notes.txt backup/ do if mkdir fails?

The a && b || c trap

It’s tempting to read a && b || c as “if a, then b, else c.” It isn’t: c also runs if b fails 2.

me@linuxbox:~$ test -d backup && cp big.iso backup/ || echo "no backup folder"
cp: error writing 'backup/big.iso': No space left on device
no backup folder

The message is wrong: the folder exists, but the copy failed. When you mean if-then-else, write if (below and in the scripting module).

Pipelines and pipefail

A pipeline’s exit status is the status of its last command 2. Failures earlier in the pipe are hidden:

me@linuxbox:~$ cat /no/such/file | sort
cat: /no/such/file: No such file or directory
me@linuxbox:~$ echo $?
0

sort happily sorted nothing, and succeeded. Two fixes 2:

  • set -o pipefail makes the pipeline fail if any stage fails. Scripts should almost always turn it on (module 8).
  • ${PIPESTATUS[@]} lists the status of every stage of the last pipeline:
me@linuxbox:~$ cat /no/such/file | sort; echo "${PIPESTATUS[@]}"
1 0

Quick check

false | true; echo $? prints 0. Why?

Asking yes-or-no questions

Because success is 0, any command can be a question:

me@linuxbox:~$ grep -q "^me:" /etc/passwd && echo "user exists"
me@linuxbox:~$ test -f ~/.bashrc && echo "found it"
me@linuxbox:~$ [ -d ~/photos ] || mkdir ~/photos
  • grep -q prints nothing, just sets the status 1.
  • test, and its other spelling [ ... ], checks files, strings, and numbers: -f (is a file), -d (is a directory), -e (exists), -z "$s" (string is empty), "$a" = "$b", "$n" -gt 10 1. The spaces inside the brackets are required.

if runs a command and branches on its exit status. That’s all if does 1:

if grep -q tornado notes.txt; then
    echo "found a tornado note"
else
    echo "none yet"
fi

You’ll write plenty of these in module 8.

Your turn

Exercises

  1. Run grep root /etc/passwd, then echo $?. Do the same with a word that isn’t in the file. What are the two statuses?
  2. In one line: make a folder logs and, only if that worked, write today’s date into logs/start.txt.
  3. In one line: try to cd into /mnt/backup, and if that fails, print a warning.
  4. Show that false | true succeeds, then run set -o pipefail and try again. Turn it off with set +o pipefail.
  5. Write an if that prints whether ~/.bash_aliases exists.
Answers
  1. 0 when it finds a match, 1 when it doesn’t.
  2. mkdir logs && date > logs/start.txt.
  3. cd /mnt/backup || echo "backup drive not available".
  4. false | true; echo $? prints 0; after set -o pipefail, it prints 1.
  5. if [ -f ~/.bash_aliases ]; then echo "yes"; else echo "no"; fi.

So

Every command leaves an exit status in $?, 0 for success. Chain with ; (always), && (on success), and || (on failure), and use if rather than a && b || c for real either-or decisions. Pipelines report only their last stage unless pipefail is on. grep -q and test turn questions into exit statuses.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 4 min

nano: Quick Edits

Next lesson
Sources for this lesson
  1. 1
    William Shotts. The Linux Command Line, Seventh Internet Edition (25.12A). LinuxCommand.org (print edition by No Starch Press). 2026. verifiedFree CC BY-NC-ND 3.0 book, release 25.12A of July 18, 2026. Part 1, Learning the Shell: the shell and terminal emulators, prompts ($ vs. # for the superuser), command history (most distributions keep the last 1,000 commands), Shift-Ctrl-C/V for copy and paste; navigation and the directory tree; exploring the system (ls options and the long listing, file, less, the guided tour of /, symbolic links); manipulating files (wildcards and character classes, mkdir, cp, mv, rm, ln; no undelete, test wildcards with ls first); working with commands (four kinds of commands, type, which, help, --help, man and its sections, apropos, whatis, info, alias); redirection; expansion and quoting; Readline keyboard tricks, completion, history search; permissions; processes. Later parts cover the environment, vi, packages, storage, networking, find, archiving, regular expressions, text processing, and shell scripting.
  2. 2
    Chet Ramey, Brian Fox. Bash Reference Manual, Edition 5.3. GNU Project, Free Software Foundation. 2025. verifiedThe reference for Bash 5.3 (May 18, 2025). Redirections (3.6) are processed left to right and order matters: ls > dirlist 2>&1 sends both streams to dirlist, while ls 2>&1 > dirlist sends only standard output there. With set -o noclobber, > fails on an existing regular file and >| overrides it. &> word is equivalent to > word 2>&1 and &>> word to >> word 2>&1. Here documents (<<word, with <<- stripping leading tabs; quoting word disables expansion) and here strings (<<<). Expansions (3.5) happen in a fixed order: brace; tilde, parameter, arithmetic, and command substitution left to right; word splitting; filename expansion; quote removal last. Startup files (6.2): an interactive login shell reads /etc/profile then the first of ~/.bash_profile, ~/.bash_login, ~/.profile; an interactive non-login shell reads ~/.bashrc. HISTCONTROL (ignorespace, ignoredups, ignoreboth), HISTSIZE, HISTFILESIZE; set -x traces expanded commands; shell functions and variables, export.