Script header
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'| Setting | What it does |
|---|---|
set -e | Exit when a command fails |
set -u | Using an unset variable is an error |
set -o pipefail | A pipe fails if any part of it fails |
set -x | Print each command before running it (debugging) |
IFS=$'\n\t' | Split words on newlines and tabs, not spaces |
set -e doesn’t trigger inside if, &&, || or a command whose status you test, so check those yourself.
Variables
name="web-1"
echo "$name" # always quote expansions
echo "${name}_backup" # braces when text follows
today=$(date +%F) # command output
readonly CONFIG=/etc/app.conf
export PATH="$HOME/bin:$PATH"
echo "${1:-default}" # first argument or a default
echo "$#" # number of arguments
echo "$@" # all arguments, each kept whole
echo "$?" # exit status of the last command
echo "$$" # this script's PIDUnquoted $var is split on spaces and expanded as a glob, which is how rm $file deletes the wrong thing.
Tests
| Test | True when |
|---|---|
[[ -f path ]] | A regular file exists |
[[ -d path ]] | A directory exists |
[[ -e path ]] | Anything exists |
[[ -s path ]] | File exists and isn’t empty |
[[ -x path ]] | Executable |
[[ -z "$v" ]] / [[ -n "$v" ]] | Empty / not empty |
[[ "$a" == "$b" ]] | Strings equal |
[[ "$a" == web-* ]] | Glob match |
[[ "$a" =~ ^[0-9]+$ ]] | Regex match |
(( n > 10 )) | Arithmetic comparison |
if [[ ! -f "$CONFIG" ]]; then
echo "missing $CONFIG" >&2
exit 1
fi
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }Loops
for host in web1 web2 web3; do
ssh "alice@$host.example.com" uptime
done
for f in /var/log/app/*.log; do
[[ -e "$f" ]] || continue # no matches: skip the literal pattern
gzip "$f"
done
for ((i = 1; i <= 5; i++)); do echo "$i"; done
while IFS= read -r line; do
echo "line: $line"
done < hosts.txt
until curl -fsS http://localhost:8080/health >/dev/null; do sleep 2; doneArrays
hosts=(web1 web2 web3)
hosts+=(web4)
echo "${hosts[0]}"
echo "${#hosts[@]}" # count
for h in "${hosts[@]}"; do echo "$h"; done
declare -A port=([http]=80 [https]=443)
echo "${port[https]}"
for k in "${!port[@]}"; do echo "$k=${port[$k]}"; done
mapfile -t lines < file.txt # file into an arrayFunctions
log() { printf '%s %s\n' "$(date +%FT%T)" "$*" >&2; }
backup() {
local src=$1 dst=$2
rsync -a "$src" "$dst" || return 1
}
backup /etc /backup/etc || log "backup failed"Use local for every variable inside a function, or it changes the global one.
Cleanup with trap
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT # runs on any exit, including errors
trap 'echo "failed at line $LINENO" >&2' ERR
trap 'echo "interrupted"; exit 130' INT TERMFlags with getopts
usage() { echo "usage: $0 [-v] [-n count] file" >&2; exit 2; }
verbose=0 count=1
while getopts ':vn:h' opt; do
case $opt in
v) verbose=1 ;;
n) count=$OPTARG ;;
h|*) usage ;;
esac
done
shift $((OPTIND - 1))
[[ $# -eq 1 ]] || usage
file=$1Strings and maths
echo $(( 7 * 6 ))
count=$(( count + 1 )) # not (( count++ )): it returns 1 when count is 0, and set -e exits
printf '%-10s %5d\n' "$name" "$count" # aligned output
read -rp "Continue? [y/N] " answer
[[ "$answer" == [yY] ]] || exit 0Keep in mind
- Run
shellcheck script.shon every script. It catches most quoting bugs. - Send errors to stderr (
>&2) and exit non-zero, so cron and CI notice. - Past about 200 lines, or once you need real data structures, switch to Python.
Related
- Linux pipes cheat sheet: redirects and chaining commands
- tee cheat sheet: saving output mid-pipe, sudo tee
- /dev/null, /dev/zero and /dev/urandom: discarding output and random data
- regex cheat sheet: patterns for grep, sed and awk
Comments
Sign in with Google, GitHub or Facebook to comment.
Loading comments…
