Variables & Arguments
NAME="value" # no spaces around =
echo "$NAME" # always quote expansions
readonly PI=3.14159 # constant
echo "$1" # first positional argument
echo "$#" # argument count
echo "$@" # all arguments, each as a separate word
echo "$0" # script nameAlways quote "$var"
An unquoted $var is subject to word splitting and globbing — a value with a space or a * in it will silently
break your script. Quote every expansion unless you specifically need splitting.
Conditions
#!/bin/bash
if [[ -f "$1" ]]; then
echo "File exists"
elif [[ -d "$1" ]]; then
echo "It's a directory"
else
echo "File not found"
fi[[ -f FILE ]]True if FILE exists and is a regular file.
[[ -d DIR ]]True if DIR exists and is a directory.
[[ -z "$VAR" ]]True if VAR is empty or unset.
[[ "$A" == "$B" ]]String equality. Use -eq/-ne/-lt/-gt for numeric comparison instead.
[[ ]] vs. [ ]
Prefer [[ ... ]] (a Bash keyword) over [ ... ] (the POSIX test command) in Bash scripts — it doesn't
word-split unquoted variables, supports &&/|| directly inside it, and gives clearer syntax errors.
Loops
for f in *.log; do
echo "Processing $f"
done
for i in {1..5}; do
echo "$i"
done
while read -r line; do
echo "$line"
done < input.txt
count=0
until [[ $count -ge 3 ]]; do
echo "$count"
((count++))
doneFunctions
deploy() {
local env="$1"
local version="${2:-latest}"
echo "Deploying $version to $env"
return 0
}
deploy staging 1.4.2
echo "Exit code: $?"local scoping
Declare function-internal variables with local — without it, every variable is global, and a helper function
can silently clobber a variable the caller was relying on.
Arrays
services=("api" "worker" "scheduler")
echo "${services[0]}" # first element
echo "${services[@]}" # every element
echo "${#services[@]}" # array length
for svc in "${services[@]}"; do
echo "Restarting $svc"
done
declare -A ports=([api]=8080 [worker]=9090)
echo "${ports[api]}"String Operations
str="Hello-World"
echo "${str,,}" # lowercase: hello-world
echo "${str^^}" # uppercase: HELLO-WORLD
echo "${str/World/Bash}" # replace: Hello-Bash
echo "${str:0:5}" # substring: Hello
echo "${#str}" # length: 11
echo "${str%%-*}" # remove longest match from the end: HelloPipes & Redirection
command1 | command2Pipes command1's stdout into command2's stdin.
command > out.txtRedirects stdout to a file, overwriting it.
command >> out.txtRedirects stdout to a file, appending.
command 2> err.txtRedirects stderr only, leaving stdout on the terminal.
command > out.txt 2>&1Redirects stdout to a file, then redirects stderr to wherever stdout now points — combines both into the same file.
command < input.txtFeeds a file's contents to a command's stdin.
Exit Codes
some_command
if [[ $? -ne 0 ]]; then
echo "Command failed" >&2
exit 1
fi
# equivalent, and more idiomatic:
if ! some_command; then
echo "Command failed" >&2
exit 1
fi0 means success, everything else is failure
By convention, exit code 0 means success and any non-zero code means failure — the specific non-zero value is program-defined (127 means "command not found," 126 means "found but not executable," 130 means killed by Ctrl-C/SIGINT).
Debugging
bash -x script.shRuns a script printing every command after expansion, before execution — the fastest way to see what a script is actually doing versus what you think it's doing.
set -euo pipefailStandard safety header for scripts: -e exits on any unhandled error, -u errors on unset variables, -o pipefail makes a pipeline fail if any stage fails, not just the last one.
Put set -euo pipefail at the top of every script
Without it, a failed command in the middle of a script is silently ignored and the script keeps running against bad state — one of the most common sources of scripts that "worked in testing" and corrupted something in production.