Practical Bash scripting reference — variables, conditionals, loops, functions, arrays, and error handling for real automation scripts.
The basics that trip people up most often.
NAME="orders-api"
readonly MAX_RETRIES=3 ## cannot be reassigned later
echo "Service: $NAME"
echo "Service: ${NAME}_prod" ## braces needed when concatenating
unset NAME
## Quoting rules
echo "$NAME has spaces preserved" ## double quotes expand variables
echo '$NAME stays literal' ## single quotes do not expand
Parameter Breakdown:
${NAME}: Braces are required whenever the variable name runs into other textBranch logic based on tests and exit codes.
if [ "$ENV" = "production" ]; then
echo "Deploying to production"
elif [ "$ENV" = "staging" ]; then
echo "Deploying to staging"
else
echo "Unknown environment: $ENV"
exit 1
fi
## File and string test operators
[ -f "$CONFIG_FILE" ] && echo "config exists"
[ -d "/opt/orders-api" ] && echo "dir exists"
[ -z "$NAME" ] && echo "NAME is empty"
[ -n "$NAME" ] && echo "NAME is set"
Command Parameter Table:
| Operator | Description |
|---|---|
-f |
True if path exists and is a regular file |
-d |
True if path exists and is a directory |
-z / -n |
True if string is empty / non-empty |
Iterate over lists, files, and ranges.
for service in orders-api payments-api auth-api; do
echo "Restarting $service"
systemctl restart "$service"
done
for i in {1..5}; do
echo "Attempt $i"
done
while read -r line; do
echo "Processing: $line"
done < input.txt
until ping -c1 db.internal.razorpay.net &>/dev/null; do
echo "Waiting for database..."
sleep 2
done
Parameter Breakdown:
read -r: Prevents backslashes in input from being interpreted as escapes{1..5}: Bash brace expansion — generates a numeric range inlineuntil: Loops until the condition becomes true, opposite of whileReusable blocks with arguments and return values.
deploy_service() {
local service_name="$1"
local version="$2"
echo "Deploying $service_name version $version"
if [ -z "$version" ]; then
echo "Error: version required" >&2
return 1
fi
## ... deployment logic here
return 0
}
deploy_service "orders-api" "v2.3.1"
if [ $? -eq 0 ]; then
echo "Deploy succeeded"
fi
Parameter Breakdown:
local: Scopes a variable to the function only — always use it to avoid leaking globals$1, $2: Positional arguments passed into the function$?: Exit code of the last command — 0 means success by conventionStore and loop over lists of values.
SERVICES=("orders-api" "payments-api" "auth-api")
echo "${SERVICES[0]}" ## first element
echo "${SERVICES[@]}" ## all elements
echo "${#SERVICES[@]}" ## array length
SERVICES+=("notifications-api") ## append
for service in "${SERVICES[@]}"; do
echo "$service"
done
## Associative arrays (Bash 4+)
declare -A ENV_PORTS=( ["staging"]=8080 ["prod"]=9090 )
echo "${ENV_PORTS[prod]}"
Parameter Breakdown:
"${SERVICES[@]}": Quoted, expands each element as a separate word — always quote this${#SERVICES[@]}: The # prefix returns the count, not a valuedeclare -A: Required to create an associative (key-value) arrayCommon patterns for parsing and formatting text.
FILE="backup-2026-06-19.tar.gz"
echo "${FILE%.tar.gz}" ## strip suffix -> backup-2026-06-19
echo "${FILE#backup-}" ## strip prefix -> 2026-06-19.tar.gz
echo "${FILE/2026/2027}" ## replace first match
echo "${#FILE}" ## string length
NAME="orders-api"
echo "${NAME^^}" ## uppercase -> ORDERS-API
echo "${NAME,,}" ## lowercase -> orders-api
Command Parameter Table:
| Syntax | Description |
|---|---|
${VAR%pattern} |
Strip shortest match from the end |
${VAR#pattern} |
Strip shortest match from the start |
${VAR/old/new} |
Replace first occurrence of old with new |
Control where output and errors go.
./deploy.sh > deploy.log ## stdout to file, overwrite
./deploy.sh >> deploy.log ## stdout to file, append
./deploy.sh 2> errors.log ## stderr only
./deploy.sh > all.log 2>&1 ## both stdout and stderr to one file
./deploy.sh &> all.log ## shorthand for the line above
./deploy.sh < input.txt ## stdin from a file
command1 | command2 ## pipe stdout of one into another
Parameter Breakdown:
2>&1: Redirects file descriptor 2 (stderr) to wherever 1 (stdout) currently points> file 2>&1 works, 2>&1 > file does not merge correctly&>: Bash shorthand that does both redirections safely in one tokenMake scripts fail loudly instead of silently.
#!/bin/bash
set -e ## exit immediately on any error
set -u ## error on undefined variables
set -o pipefail ## fail if any command in a pipe fails
set -euo pipefail ## all three combined, common at the top of scripts
trap 'echo "Error on line $LINENO"' ERR
trap 'echo "Cleaning up..."; rm -f /tmp/lock' EXIT
cleanup() {
rm -f /tmp/deploy.lock
}
trap cleanup EXIT
Parameter Breakdown:
set -e: Stops the script on the first command that fails — essential for deploy scriptsset -o pipefail: Without this, false | true reports success because the last command succeededtrap ... EXIT: Runs cleanup code no matter how the script exits, including on errorHandle script inputs properly.
echo "Script name: $0"
echo "First arg: $1"
echo "All args: $@"
echo "Arg count: $#"
while getopts "e:v:h" opt; do
case $opt in
e) ENVIRONMENT="$OPTARG" ;;
v) VERSION="$OPTARG" ;;
h) echo "Usage: $0 -e <env> -v <version>"; exit 0 ;;
*) echo "Invalid option"; exit 1 ;;
esac
done
## Usage: ./deploy.sh -e production -v 2.3.1
Parameter Breakdown:
getopts: Standard way to parse -flag value style arguments$@ vs $*: $@ preserves each argument as a separate word when quoted, $* does notOPTARG: Holds the value passed after a flag that expects onePatterns worth keeping in a snippets file.
## Retry a command up to 3 times with a delay
for i in 1 2 3; do ./flaky-script.sh && break || sleep 5; done
## Check if a command exists before using it
command -v jq >/dev/null 2>&1 || { echo "jq is required"; exit 1; }
## Run a command with a timeout
timeout 30s ./long-running-task.sh
## Get the directory the running script lives in
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"