Bash Basics: How Bash Reads and Runs a Command Line
How Bash turns a line into a command: command types, exit codes, the order of expansions, quoting, pipelines, subshells and redirection.
You type cp *.log "$backup_dir" and press Enter. Before cp starts, Bash has already split the line into words, replaced $backup_dir with its value, turned *.log into a list of file names and removed the quotes. cp never sees the * or the $. Knowing what Bash does in those steps, and in which order, is the difference between guessing why a command misbehaves and knowing.
This part follows a command line from the moment you press Enter to the moment the command exits: what kinds of commands exist, how exit codes work, the expansion order, quoting, lists and pipelines, subshells and redirection.
Prerequisites
- The practice directory from Part 1. Run the examples from
~/bash-practice. - If
cd,ls,cpandmvare new to you, read Linux command line basics first. This part is about Bash itself, not about individual commands.
The Shape of a Command Line
A simple command is a command name followed by words, which the command receives as arguments. Options are just arguments that start with - by convention; Bash does not treat them specially.
grep -c ' 404 ' logs/access.log > count.txt
│ │ │ │ └── redirection: handled by Bash, not passed to grep
│ │ │ └── argument 3
│ │ └── argument 2 (the quotes are removed before grep sees it)
│ └── argument 1 (an option)
└── command name
Bash also recognizes control operators that join commands: | (pipe), ; and newline (one after another), && and || (conditional), and & (run in the background). Everything else is built from these pieces.
Exit Status
Every command ends with an exit status, a number from 0 to 255. Zero means success, anything else means failure, and $? holds the status of the last command. if, while, && and || all decide based on it.
The conventions behind those numbers:
| Exit status | Meaning |
|---|---|
| 0 | Success |
| 1 | General failure, or "false" (grep found nothing, a test failed) |
| 2 | Misuse: a bad option or argument (grep uses it for real errors too) |
| 126 | The file was found but could not be executed |
| 127 | Command not found |
| 128 + N | Killed by signal N: 130 for Ctrl+C (SIGINT), 143 for SIGTERM |
Your own scripts should follow them: exit 0 on success, 1 on failure, 2 for usage errors.
What Kind of Command Is It?
The first word of a command can be one of five kinds of thing, and Bash looks for them in this order: an alias, a keyword (if, [[, while), a function, a builtin (part of Bash itself, such as cd, echo, read) or a program file found in $PATH. type tells you which one you are getting:
Two practical consequences:
echois both a builtin and/usr/bin/echo, and they can behave differently. A bareechoruns the builtin, unless a function with the same name overrides it or the builtin was turned off withenable -n echo. The same is true forprintf,test,kill,pwdand others, so read their documentation withhelp echo, notman echo, which describes the external program.- To check whether a command exists in a script, use
command -v name, notwhich.whichis a separate program, its output format differs between systems, and it cannot see builtins, functions or aliases. See how to check if a program exists.
cd has to be a builtin: a separate program would change its own directory and exit, leaving your shell where it was.
The Order of Expansions
After splitting a line into words, Bash expands them in a fixed order. Each step works on the result of the previous one:
Step 3 covers parameter expansion ($name, ${name}), command substitution ($(...)) and arithmetic ($((...))), done left to right. Step 4 splits only the results of step 3 that are not inside double quotes. Step 5 expands every unquoted word that contains *, ? or [, whether you typed it or it came out of step 3. Together they are why quoting matters so much. Here is each step on its own:
set -x makes Bash print each command after expansion, prefixed with +, so you can see exactly what a program receives:
The order explains some surprises:
Because operators are recognized before expansion, a command stored in a string cannot contain |, >, && or quotes that work. Store commands in a function or an array instead (Part 4 and Part 8 show both). For ranges with a variable end, use seq or a C-style for (( i = 1; i <= n; i++ )) loop (Part 5).
Quoting
Quoting turns off special meanings. Bash has four kinds:
| Form | What it does | Example |
|---|---|---|
\x |
The next character is literal | echo \$HOME |
'single' |
Everything is literal; you cannot put a ' inside |
echo '$HOME and *' |
"double" |
$, ` and \ still work; no word splitting or globbing of the result |
echo "$HOME and *" |
$'ANSI-C' |
Like single quotes, but \n, \t, \e and \' are interpreted |
echo $'tab:\there' |
The rule that prevents most bugs: put double quotes around every expansion ("$var", "$(cmd)", "${array[@]}") unless you want word splitting and globbing, and use single quotes for literal text. Quotes also join words: "monthly report.txt" is one argument, with the space inside it.
Lists, Pipelines and Grouping
A pipeline reports only its last command's status, so a failure on the left side goes unnoticed: above, grep failed with status 2, but the pipeline returned 0. ${PIPESTATUS[@]} holds every status of the last pipeline, and set -o pipefail makes a pipeline fail when any part fails (Part 10 covers it and its traps).
a && b || c is not an if-then-else. c also runs when b fails:
Use a real if when the middle command can fail.
Grouping runs several commands as one unit. Braces run them in the current shell; parentheses start a subshell, a copy of the shell whose changes vanish when it ends:
Note the syntax of braces: a space after { and a ; or newline before }. Subshells are also created for every part of a pipeline, for $(...) and for &. That is why a variable set inside cmd | while read ... is gone after the loop, a trap Part 5 shows how to avoid.
Redirection
Every process starts with three open file descriptors: 0 is standard input, 1 is standard output and 2 is standard error. Redirections, processed from left to right, point them somewhere else before the command runs:
| Syntax | Effect |
|---|---|
> file |
Standard output to file, truncating it first |
>> file |
Standard output appended to file |
< file |
Standard input from file |
2> file |
Standard error to file |
2>&1 |
Standard error to wherever standard output points right now |
&> file |
Both to file (Bash only; the same as > file 2>&1) |
&>> file |
Both appended to file |
cmd1 |& cmd2 |
Both into the pipe (Bash only; the same as 2>&1 |) |
> /dev/null |
Discard the output |
>| file |
Overwrite even when set -o noclobber is on |
The order of 2>&1 matters, because it copies where standard output points at that moment:
In the second command, 2>&1 pointed standard error at the terminal (where standard output still pointed), and only then did > out.txt move standard output. The error went to the screen.
To send everything a script prints to a log file from a given line on, redirect the shell itself with exec:
#!/usr/bin/env bash
exec >> /var/tmp/nightly.log 2>&1 # every later command writes here
echo "started at $(date -u +%FT%TZ)"
Redirect all output to a file, send output to a file and the screen with tee and what 2>&1 means go further. Here documents (<<EOF) and here strings (<<<), which feed text to standard input, are in Part 9.
History Expansion and Line Editing
In an interactive shell, Bash keeps a history and lets you reuse it. These are the shortcuts worth learning first:
| Type this | Bash does |
|---|---|
| Ctrl+R | Search backwards through history; press again for older matches |
!! |
The previous command, for example sudo !! |
!$ |
The last argument of the previous command (or press Alt+.) |
^old^new |
Rerun the previous command with old replaced by new |
| Ctrl+A / Ctrl+E | Jump to the start / end of the line |
| Ctrl+W | Delete the word before the cursor |
fc |
Open the previous command in your editor, run it when you save |
Two settings in ~/.bashrc make history more useful: HISTCONTROL=ignoreboth skips duplicates and commands that start with a space (handy for commands that contain a secret), and HISTTIMEFORMAT='%F %T ' adds a timestamp to history output. History expansion with ! only happens in interactive shells, so ! in a script is an ordinary character. If it gets in your way interactively, for example in echo "Hello!", use single quotes or turn it off with set +H.
Getting Help
helplists the builtins;help readshows the options of one. Bash builtins are not documented in their own man pages.man bashis the complete reference, more than 6,000 lines long. Search it with/followed by a word, for example/Parameter Expansion.type nameshows what a name is;man nameis for program files.
Common Mistakes
- Expecting a variable to set a brace range (
{1..$n}). Braces expand first; useseqor a C-style loop. - Storing a command with pipes or quotes in a string and running
$cmd. Use a function or an array. - Unquoted expansions.
$varand$(cmd)without double quotes are split on whitespace and globbed. - Writing
2>&1 > filewhen you meant> file 2>&1. - Using
a && b || cas if/else.cruns whenbfails. - Reading
man echofor the builtin. Usehelp echo.
Summary
- A command line is split into words and operators, then expanded in order: braces, tilde,
$expansions, word splitting, globbing, quote removal. - Word splitting applies to unquoted expansion results, and globbing to every unquoted pattern, including ones that came from a variable. Double-quote your expansions.
typeshows whether a name is an alias, keyword, function, builtin or file; usecommand -vin scripts.- Exit status 0 is success. Pipelines report the last command's status unless
pipefailis on. - Braces group commands in the current shell, parentheses in a subshell whose changes are lost.
- Redirections are processed left to right, so
> file 2>&1sends both streams to the file.
Next: Working with Files, where globbing, safe file names and temporary files get the attention they need.
Keep Learning
Tags
Found an issue?