Conditions and loops
The [ -f ], [ -z ] and -eq checks are in test-conditions.
A script is a text file of shell commands: the #! line picks the interpreter, arguments arrive in $1, $2 and "$@".
#!/bin/bash # first line: what runs the file
set -euo pipefail
stop at the first error
chmod u+x backup.sh
allow running it
./backup.sh "my file.txt"
run it by path
bash backup.sh
run it without the x bit
bash -n backup.sh
check syntax without running
name="value"; echo "$name"
a variable — in double quotes
for f in "$@"; do echo "$f"; done
every argument, one at a time
A script is a text file with the commands you would otherwise type in a
terminal. An interpreter reads and runs it; for a bash
script that is bash.
| Command | What it needs | Who picks the interpreter |
|---|---|---|
./backup.sh |
the x permission |
the #! line |
backup.sh |
x and the script's directory in PATH |
the #! line |
bash backup.sh |
the r permission |
you: bash |
The #! line (shebang) is a first line such as #!/bin/bash.
It tells the kernel which program to hand the file to when the file is run as a
command. With bash backup.sh the interpreter is already chosen and the line
is just a comment.
On Ubuntu sh is dash: ls -l /bin/sh shows /bin/sh -> dash. It knows only
POSIX syntax, so a bash script run through sh fails:
To run a script by name without ./, put it in ~/bin: if that directory
exists, ~/.profile adds it to PATH at login. A newly created directory
takes effect from the next login.
name=value has no spaces around =. bash reads name = "x" as the command
name with arguments: name: command not found.
$name substitutes the value. Unquoted, the shell splits it on spaces —
word splitting — and expands patterns such as *:
Inside double quotes the value stays one argument while $name and
$(command) are still substituted. Inside single quotes the text is taken
literally. $(command) substitutes the command's output:
today=$(date +%F).
A script's arguments arrive in positional parameters:
| Form | Meaning |
|---|---|
$0 |
the script name as it was called |
$1, $2… |
the first, second argument |
$# |
the number of arguments |
"$@" |
all arguments, each as a separate word |
"$*" |
all arguments as one string joined by spaces |
Without quotes, $@ and $* split my file.txt into two words again.
set -euo pipefail at the top of a script:
-e — exit as soon as a command returns non-zero; tests inside if, &&
and || do not stop the script;-u — an unset variable is an error, so a typo in a name does not turn into
an empty string; the script exits with code 1;-o pipefail — a pipeline fails if any of its stages fails.What the codes mean and how to test them is covered in exit-codes.
The [ -f ], [ -z ] and -eq checks are in test-conditions.
shellcheck goes deeper: quoting, an unchecked cd, $?.
A loop over ls or find output inside $( ) splits names on spaces. A
pattern and find -exec pass every name whole (see xargs for -print0).
bash: ./backup.sh: Permission denied, code 126: the file lacks the x
permission. Run chmod u+x backup.sh or start it with bash backup.sh.
backup.sh: command not found, code 127: a name without a path is looked up
only in the PATH directories. Run ./backup.sh or put the script in
~/bin.
bash: ./backup.sh: cannot execute: required file not found — the kernel could
not find the interpreter named on the #! line. Either the path has a typo, or
the file came from Windows with CRLF line endings: file backup.sh says
with CRLF line terminators. Strip the \r: sed -i 's/\r$//' backup.sh.
A bash script was run by sh (dash): it was started as sh backup.sh, or its
#! line says /bin/sh. Run ./backup.sh with #!/bin/bash, or
bash backup.sh.
Spaces around =: name = "x". Write name="x".
backup.sh: line 3: name: unbound variable — set -u caught a variable with
no value, often a typo. Fix the name or give it a default: "${name:-}".
A block or a quote is not closed: if without fi, do without done, a
stray ". The line number points at the end of the file — look further up.
An unquoted variable: cp $f /backup/ gets two arguments instead of one.
Write "$f" and "$@"; shellcheck finds such places (SC2086).
Cron and systemd services have a different environment: a short PATH, the
directory /, none of the variables from ~/.bashrc. Details are in
the service environment.
rm -rf "$dir/"* with an empty dir turns into rm -rf /*. Use set -u or
write "${dir:?}" — an empty value stops the script with an error.
cd /srv/tmp; rm -rf ./* deletes the files in the current directory if cd
failed. Write cd /srv/tmp || exit 1.
&&, ||, exit.PATH and environment variables.>&2.Found an inaccuracy? Report an error in the article