Bash scripting
Fundamentals for writing and reading bash scripts. For a command reference (not scripting), see the bash cheat sheet.
Shebang and permissions
| #!/usr/bin/env bash
# The shebang tells the shell which interpreter to use.
# #!/bin/bash is also common, but #!/usr/bin/env bash resolves bash from $PATH.
chmod +x script.sh
./script.sh
|
Variables
| name="offsec"
echo "$name"
# No spaces around the = sign, or bash treats it as a command.
readonly PI=3.14 # constant
unset name # remove a variable
# Command substitution
current_user=$(whoami)
files=$(ls /tmp)
|
Quoting
| var="hello world"
echo $var # word-splits: hello world (two args to echo)
echo "$var" # preserves as a single string: hello world
# Single quotes disable all expansion
echo '$var' # prints literally: $var
|
Special variables and parameters
| $0 # script name
$1..$9 # positional arguments
$# # number of arguments
$@ # all arguments, each as a separate word
$* # all arguments, as one word
$? # exit code of the last command
$$ # PID of the current shell
$! # PID of the last background job
|
Arrays
| hosts=(10.10.10.1 10.10.10.2 10.10.10.3)
echo "${hosts[0]}" # first element
echo "${hosts[@]}" # all elements
echo "${#hosts[@]}" # array length
for host in "${hosts[@]}"; do
echo "$host"
done
|
Conditionals
| if [ -f "/etc/passwd" ]; then
echo "file exists"
elif [ -d "/etc" ]; then
echo "it's a directory instead"
else
echo "not found"
fi
# Common test operators
# -f regular file exists -d directory exists
# -e path exists -r/-w/-x readable/writable/executable
# -z string is empty -n string is not empty
# -eq -ne -lt -le -gt -ge numeric comparisons
# == != (string comparisons, use [[ ]] for these)
if [[ "$1" == "admin" ]]; then
echo "welcome, admin"
fi
|
Loops
| # for loop
for i in 1 2 3; do
echo "$i"
done
# C-style for loop
for ((i = 0; i < 5; i++)); do
echo "$i"
done
# while loop
count=0
while [ "$count" -lt 5 ]; do
echo "$count"
((count++))
done
# until loop
until ping -c 1 10.10.10.10 &>/dev/null; do
echo "waiting for host..."
sleep 1
done
# reading a file line by line
while IFS= read -r line; do
echo "$line"
done < targets.txt
|
Functions
| greet() {
local name="$1" # local restricts scope to the function
echo "hello, $name"
return 0 # exit status, not a return value
}
greet "amanda"
echo "exit code: $?"
# Capturing "output" (functions don't return values, only exit codes)
result=$(greet "amanda")
|
Command-line argument parsing with getopts
| #!/usr/bin/env bash
usage() {
echo "Usage: $0 -t <target> -p <port>"
exit 1
}
while getopts ":t:p:h" opt; do
case "$opt" in
t) target="$OPTARG" ;;
p) port="$OPTARG" ;;
h) usage ;;
\?) echo "Invalid option: -$OPTARG"; usage ;;
:) echo "Option -$OPTARG requires an argument"; usage ;;
esac
done
echo "target=$target port=$port"
|
Exit codes and error handling
| set -e # exit immediately if a command fails
set -u # error on unset variables
set -o pipefail # a pipeline fails if any command in it fails
set -x # print each command before running it (debugging)
command || echo "command failed, but we continue"
command && echo "command succeeded"
trap 'echo "cleaning up..."; rm -f /tmp/scratch.$$' EXIT
|
| command > out.txt # stdout to file (overwrite)
command >> out.txt # stdout to file (append)
command 2> err.txt # stderr to file
command &> all.txt # stdout and stderr to file
command 2>&1 # redirect stderr to wherever stdout is going
command < in.txt # stdin from file
# Discard output entirely
command &>/dev/null
|
Debugging scripts
| bash -x script.sh # trace execution
bash -n script.sh # syntax check only, don't run
|
Last update: 2026-07-27
Created: July 27, 2026 16:17:23