Shell Scripting
Turning a sequence of commands into a reusable, reliable script - shebangs, variables, conditionals, loops, and the habits that keep a script from failing silently.
Everything covered in Bash Basics works identically inside a script file - the difference is that a script also needs to handle cases you'd notice and correct interactively, like a missing argument or a failed command, without a human watching.
The shebang and making a script executable
#!/usr/bin/env bash
The first line tells the kernel which interpreter to run the file with.
#!/usr/bin/env bash finds bash via PATH rather than assuming it's at
/bin/bash, which is more portable across systems. Then make it
executable and run it:
chmod +x deploy.sh
./deploy.sh
See File Permissions and Ownership
for what chmod +x is actually changing.
Variables and arguments
#!/usr/bin/env bash
name="$1" # first argument
echo "Hello, $name"
echo "Called with $# argument(s): $*"
$1, $2, … are positional arguments; $# is the argument count; $@
and $* both expand to all arguments, but "$@" (quoted) preserves
argument boundaries when one contains spaces, while "$*" collapses them
into one string - prefer "$@" when forwarding arguments to another
command.
Conditionals
if [[ -f "$file" ]]; then
echo "exists"
elif [[ -d "$file" ]]; then
echo "is a directory"
else
echo "not found"
fi
[[ ]] is a Bash keyword with more forgiving syntax and no word-splitting
surprises; [ ] (a synonym for the test command) is the POSIX-portable
form needed if the script must also run under sh. Common tests: -f
(regular file exists), -d (directory exists), -z/-n (string is
empty/non-empty), -eq/-lt/-gt (numeric comparison - [[ ]] doesn't
overload </> for numbers the way it does for strings).
Loops
for f in *.log; do
echo "processing $f"
done
while read -r line; do
echo "got: $line"
done < input.txt
while read -r line is the standard idiom for processing a file line by
line; -r prevents backslashes in the input from being interpreted.
Functions
log() {
echo "[$(date '+%H:%M:%S')] $1"
}
log "starting deploy"
Functions in Bash don't return values the way most languages do - return
sets the function's exit status (0–255), not a value. To get data out of a
function, either echo it and capture with $(...), or write to a
variable directly.
Exit codes and error handling
Every command sets $? to its exit status: 0 for success, non-zero for
failure. Scripts should check this rather than assuming a command
succeeded:
if ! curl -sf https://example.com/health; then
echo "health check failed" >&2
exit 1
fi
For anything beyond a trivial script, start with these three options, which turn silent failures into loud ones:
set -euo pipefail
-e- exit immediately if any command fails (without this, a script keeps running after an error by default, which is rarely what you want)-u- treat an unset variable as an error, instead of silently expanding to an empty string-o pipefail- make a pipeline (cmd1 | cmd2) fail if any command in it fails, not just the last one
See Pipes and Redirection for what a
pipeline is, and Signals for handling SIGINT/
SIGTERM in longer-running scripts.
A minimal complete example
#!/usr/bin/env bash
set -euo pipefail
log_dir="${1:?usage: $0 <log-dir>}"
for f in "$log_dir"/*.log; do
[[ -f "$f" ]] || continue
echo "compressing $f"
gzip "$f"
done
${1:?message} exits with message printed to stderr if $1 is unset -
a compact way to validate required arguments up front instead of failing
confusingly later in the script.