| name | shell-scripting |
| description | Shell scripting and bash automation. Use when user asks to "write a bash script", "create a shell script", "parse command line args", "write a deploy script", "automate with bash", "process files with bash", "create an install script", "write a backup script", "handle signals in bash", "parse CSV in bash", "error handling in bash", "functions in bash", "arrays in bash", "string manipulation", "loop patterns", or mentions shell scripting, bash scripting, POSIX shell, script automation, bash best practices, or shell utilities. |
| license | MIT |
| metadata | {"author":"1mangesh1","version":"1.0.0","tags":["bash","shell","scripting","automation","posix","cli"]} |
Shell Scripting & Bash Best Practices
Comprehensive guide to writing robust, portable, and maintainable shell scripts. Covers Bash idioms, POSIX compliance, error handling, security, and real-world patterns.
Bash Script Template
Every script should start with a solid foundation.
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly VERSION="1.0.0"
cleanup() {
local exit_code=$?
if [[ -n "${TMPDIR_CUSTOM:-}" && -d "$TMPDIR_CUSTOM" ]]; then
rm -rf "$TMPDIR_CUSTOM"
fi
exit "$exit_code"
}
trap cleanup EXIT
trap 'echo "Interrupted."; exit 130' INT
trap 'echo "Terminated."; exit 143' TERM
main() {
parse_args "$@"
validate_dependencies
}
main "$@"
What the options mean
set -e -- Exit immediately on any command failure.
set -u -- Treat unset variables as an error.
set -o pipefail -- A pipeline fails if any command in it fails, not just the last.
IFS=$'\n\t' -- Safer word splitting; avoids problems with spaces in filenames.
Variable Handling
Quoting Rules
Always double-quote variables unless you explicitly need word splitting or globbing.
name="world"
echo "Hello, $name"
cp "$source" "$destination"
cp $source $destination
for f in *.txt; do
echo "Processing: $f"
done
Variable Expansion and Defaults
db_host="${DB_HOST:-localhost}"
db_port="${DB_PORT:-5432}"
: "${LOG_LEVEL:=info}"
: "${API_KEY:?ERROR: API_KEY must be set}"
filename="report-2024-01-15.csv"
echo "${filename:0:6}"
echo "${filename: -3}"
echo "${#filename}"
var_name="HOME"
echo "${!var_name}"
Removal and Replacement
filepath="/home/user/documents/report.tar.gz"
echo "${filepath#*/}"
echo "${filepath##*/}"
echo "${filepath%.*}"
echo "${filepath%%.*}"
echo "${filepath/user/admin}"
msg="foo-bar-baz"
echo "${msg//-/_}"
text="Hello World"
echo "${text,,}"
echo "${text^^}"
echo "${text~}"
Conditionals and Test Operators
if/elif/else
if [[ -f "$config_file" ]]; then
source "$config_file"
elif [[ -f /etc/default/myapp ]]; then
source /etc/default/myapp
else
echo "No configuration found, using defaults."
fi
Test Operators
[[ -e "$path" ]]
[[ -f "$path" ]]
[[ -d "$path" ]]
[[ -L "$path" ]]
[[ -r "$path" ]]
[[ -w "$path" ]]
[[ -x "$path" ]]
[[ -s "$path" ]]
[[ "$a" -nt "$b" ]]
[[ "$a" -ot "$b" ]]
[[ -z "$str" ]]
[[ -n "$str" ]]
[[ "$a" == "$b" ]]
[[ "$a" != "$b" ]]
[[ "$a" == *.txt ]]
[[ =~ ^[0-9]+$ ]]
[[ -eq ]]
[[ -ne ]]
[[ -lt ]]
[[ -gt ]]
[[ -le ]]
[[ -ge ]]
[[ -f && -r ]]
[[ -f || -d ]]
[[ ! -e ]]
Arithmetic
(( count++ ))
(( total = price * quantity ))
if (( age >= 18 )); then
echo "Adult"
fi
(( result = (a > b) ? a : b ))
Loops
for loops
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
for (( i = 0; i < 10; i++ )); do
echo "Iteration $i"
done
for file in /var/log/*.log; do
[[ -f "$file" ]] || continue
echo "Log: $file"
done
while IFS= read -r line; do
echo "Line: $line"
done < <(find /tmp -maxdepth 1 -name "*.tmp" -type f)
declare -a servers=("web01" "web02" "db01")
for server in "${servers[@]}"; do
echo "Pinging $server..."
done
while and until
counter=0
while (( counter < 5 )); do
echo "Count: $counter"
(( counter++ ))
done
while IFS= read -r line; do
echo ">> $line"
done < "$input_file"
while IFS=: read -r user _ uid gid _ home shell; do
echo "User: $user, Home: $home, Shell: $shell"
done < /etc/passwd
until ping -c1 -W1 "$host" &>/dev/null; do
echo "Waiting for $host to come online..."
sleep 5
done
echo "$host is reachable."
Loop Control
for i in {1..100}; do
(( i % 2 == 0 )) && continue
(( i > 20 )) && break
echo "$i"
done
Functions and Return Values
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
printf '[%s] [%-5s] %s\n' "$timestamp" "$level" "$message"
}
calculate_sum() {
local -i a="$1"
local -i b="$2"
local -i result
result=$(( a + b ))
echo "$result"
}
sum=$(calculate_sum 10 20)
echo "Sum: $sum"
is_valid_ip() {
local ip="$1"
local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
if [[ "$ip" =~ $regex ]]; then
return 0
1
}
is_valid_ip ;
() {
-n ref=
ref=
}
get_result my_var
Command-Line Argument Parsing
Manual Parsing (Flexible, handles long options)
usage() {
cat <<USAGE
Usage: ${SCRIPT_NAME} [OPTIONS] <input-file>
Options:
-o, --output FILE Output file (default: stdout)
-v, --verbose Enable verbose output
-n, --dry-run Show what would be done
-h, --help Show this help message
--version Show version
Examples:
${SCRIPT_NAME} -v --output result.txt data.csv
${SCRIPT_NAME} --dry-run input.log
USAGE
exit "${1:-0}"
}
output=""
verbose=false
dry_run=false
input_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
[[ -n "${2:-}" ]] || { echo "Error: --output requires a value"; usage 1; }
output="$2"
shift 2
;;
-v|--verbose)
verbose=true
shift
;;
-n|--dry-run)
dry_run=true
shift
;;
-h|--help)
usage 0
;;
--version)
echo "${SCRIPT_NAME} v${VERSION}"
exit 0
;;
--)
shift
break
;;
-*)
echo "Error: Unknown option: $1" >&2
usage 1
;;
*)
input_file=
;;
[[ -n ]] || { >&2; usage 1; }
getopts (POSIX compatible, short options only)
verbose=0
output=""
while getopts ":vo:h" opt; do
case "$opt" in
v) verbose=1 ;;
o) output="$OPTARG" ;;
h) usage 0 ;;
:) echo "Error: -${OPTARG} requires an argument" >&2; exit 1 ;;
*) echo "Error: Unknown option -${OPTARG}" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
Input/Output Redirection and Pipes
command > file.txt
command >> file.txt
command 2> errors.log
command &> all.log
command > out.log 2>&1
command 2>/dev/null
command > stdout.log 2> stderr.log
cat <<EOF > /etc/myapp.conf
# Configuration generated on $(date)
server_name=${SERVER_NAME}
port=${PORT:-8080}
EOF
cat <<'EOF' > script_template.sh
echo "This $variable is literal, not expanded"
EOF
grep "error" <<< "$log_contents"
diff <(sort file1.txt) <(sort file2.txt)
set -o pipefail
cat access.log | grep "500" | awk '{print $1}' | sort -u > failed_ips.txt
command | tee output.log
command | tee -a output.log
2>&1 | debug.log
3> custom_output.log
>&3
3>&-
Process Management
long_running_task &
pid=$!
echo "Started background task with PID: $pid"
wait "$pid"
echo "Task exited with status: $?"
job1 &
job2 &
job3 &
wait
max_jobs=4
for file in /data/*.csv; do
while (( $(jobs -r | wc -l) >= max_jobs )); do
sleep 0.5
done
process_file "$file" &
done
wait
shutdown() {
echo "Shutting down gracefully..."
kill -- -$$ 2>/dev/null || true
exit 0
}
trap shutdown SIGINT SIGTERM
acquire_lock() {
local pidfile="$1"
if [[ -f "$pidfile" ]]; then
local old_pid
old_pid="$(cat "$pidfile")"
-0 2>/dev/null;
>&2
1
>&2
$$ >
}
() {
pidfile=
-f
}
30 long_running_command || {
1
}
String Manipulation with Parameter Expansion
No need for sed or awk for simple string operations.
str=" Hello, World! "
trimmed="${str#"${str%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
if [[ "$str" == *"World"* ]]; then
echo "Contains 'World'"
fi
IFS=',' read -ra parts <<< "one,two,three,four"
for part in "${parts[@]}"; do
echo "Part: $part"
done
join_by() {
local IFS="$1"
shift
echo "$*"
}
result=$(join_by ',' "${parts[@]}")
echo "$result"
printf '=%.0s' {1..60}
echo
name="john"
echo
name=
Array Handling
declare -a fruits=("apple" "banana" "cherry")
fruits+=("date")
echo "${fruits[0]}"
echo "${fruits[@]}"
echo "${#fruits[@]}"
echo "${!fruits[@]}"
echo "${fruits[@]:1:2}"
unset 'fruits[1]'
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
declare -A config
config[host]="localhost"
config[port]="8080"
config[debug]="true"
if [[ -v config[host] ]]; then
echo "Host: ${config[host]}"
key ;
-t lines < <( -1 /tmp)
-a evens=()
n {1..20};
(( n % == )) && evens+=()
Error Handling Patterns
err_handler() {
local line_no="$1"
local command="$2"
local exit_code="$3"
echo "ERROR: Command '${command}' failed at line ${line_no} with exit code ${exit_code}" >&2
}
trap 'err_handler ${LINENO} "${BASH_COMMAND}" $?' ERR
die() {
echo "FATAL: $*" >&2
exit 1
}
retry() {
local max_attempts="${1:-3}"
local delay="${2:-1}"
shift 2
local attempt=1
until "$@"; do
if (( attempt >= max_attempts )); then
echo "Command failed after $max_attempts attempts: $*" >&2
return 1
fi
echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
(( attempt++ ))
(( delay *= ))
}
() {
cmd ;
-v >/dev/null 2>&1 || die
}
require_cmd git curl jq
() {
description=
! ;
die
}
assert -f /etc/myapp.conf
File Operations
tmpfile="$(mktemp)"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpfile" "$tmpdir"' EXIT
find /var/log -name "*.log" -mtime +30 -type f -delete
find . -name "*.sh" -exec chmod +x {} +
find . -type f -size +100M
while IFS= read -r line || [[ -n "$line" ]]; do
echo "$line"
done < "$file"
atomic_write() {
local target="$1"
local tmp
tmp="$(mktemp "${target}.XXXXXX")"
if cat > "$tmp" && mv -f "$tmp" "$target"; then
0
-f
1
}
| atomic_write /etc/myapp.conf
() {
=
[[ ! -d ]];
-p || die
}
cmp -s file1.txt file2.txt;
path=
Portable Scripting (POSIX Compliance)
if [ -f "$file" ] && [ -r "$file" ]; then
echo "File exists and is readable"
fi
if [ "$count" -gt 10 ]; then
echo "Count exceeds 10"
fi
total=$((a + b))
printf 'Line 1\nLine 2\n'
Common Patterns
Lockfile Pattern
LOCKFILE="/var/run/${SCRIPT_NAME}.lock"
acquire_lock() {
if ( set -o noclobber; echo $$ > "$LOCKFILE" ) 2>/dev/null; then
trap 'rm -f "$LOCKFILE"' EXIT
return 0
fi
local lock_pid
lock_pid="$(cat "$LOCKFILE" 2>/dev/null)"
if [[ -n "$lock_pid" ]] && kill -0 "$lock_pid" 2>/dev/null; then
echo "Script is already running (PID $lock_pid)" >&2
return 1
fi
echo "Removing stale lock file" >&2
rm -f "$LOCKFILE"
acquire_lock
}
Configuration File Parsing
declare -A CONFIG
parse_config() {
local config_file="$1"
[[ -f "$config_file" ]] || die "Config file not found: $config_file"
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
local key="${line%%=*}"
local value="${line#*=}"
key="${key#"${key%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
value="${value#\"}"
value="${value%\"}"
CONFIG["$key"]="$value"
<
}
parse_config /etc/myapp.conf
Logging Framework
LOG_LEVEL="${LOG_LEVEL:-INFO}"
LOG_FILE="${LOG_FILE:-/var/log/myapp.log}"
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)
log() {
local level="$1"
shift
local message="$*"
local current_level="${LOG_LEVELS[${LOG_LEVEL}]:-1}"
local msg_level="${LOG_LEVELS[${level}]:-1}"
(( msg_level < current_level )) && return
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local entry
entry="$(printf '[%s] [%-5s] [%s:%s] %s' "$timestamp" "$level" "${FUNCNAME[1]:-main}" "${BASH_LINENO[0]}" "$message")"
echo "$entry" >> "$LOG_FILE"
if [[ "$level" == "ERROR" || "$level" == "FATAL" ]]; then
echo "" >&2
}
INFO
DEBUG
ERROR
Here Documents and Here Strings
generate_html() {
local title="$1"
local body="$2"
cat <<-EOF
<!DOCTYPE html>
<html>
<head><title>${title}</title></head>
<body>${body}</body>
</html>
EOF
}
mysql -u root <<SQL
CREATE DATABASE IF NOT EXISTS myapp;
GRANT ALL ON myapp.* TO 'appuser'@'localhost';
SQL
while IFS=, read -r name age city; do
echo "Name: $name, Age: $age, City: $city"
done <<< "Alice,30,NYC
Bob,25,LA
Charlie,35,Chicago"
if true; then
cat <<-'USAGE'
Usage: command [options]
-h Show help
-v Verbose mode
USAGE
fi
Security Best Practices
rm "$file"
rm $file
validate_filename() {
local name="$1"
if [[ "$name" =~ [^a-zA-Z0-9._-] ]]; then
die "Invalid filename: $name (contains special characters)"
fi
if [[ "$name" == ..* || "$name" == */* ]]; then
die "Invalid filename: $name (path traversal attempt)"
fi
}
rm -- "$file"
grep -- "$pattern" "$file"
export PATH="/usr/local/bin:/usr/bin:/bin"
tmpfile="$(mktemp)" || die "Failed to create temp file"
chmod 600
MYSQL_PWD=
mysql -u root mydb
MYSQL_PWD
-f
+f
[[ -eq 0 ]];
su -s /bin/bash nobody --
Useful One-Liners and Idioms
(( EUID == 0 )) || die "Must run as root"
command -v docker >/dev/null 2>&1 || die "Docker is not installed"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
read -rsp "Enter password: " password
echo
confirm() {
read -rp "${1:-Are you sure?} [y/N] " response
[[ "$response" =~ ^[Yy]$ ]]
}
confirm "Delete all files?" || exit 0
spin() {
local -a frames=('|' '/' '-' '\')
while true; do
for frame in "${frames[@]}"; do
printf '\r%s %s' "$frame"
0.2
}
spin &
spinner_pid=$!
2>/dev/null
start_time=
end_time=
random_string=$( -c 32 /dev/urandom | | -dc | -c 16)
[[ -t 0 ]];
result=
Script Debugging
set -x
set +x
export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}():} '
debug_section() {
set -x
set +x
}
if [[ "${DEBUG:-}" == "true" ]]; then
set -x
fi
debug() {
[[ "${VERBOSE:-false}" == "true" ]] && echo "DEBUG: $*" >&2
}
trace_calls() {
echo "TRACE: ${FUNCNAME[1]} called from ${FUNCNAME[2]:-main} (line ${BASH_LINENO[1]})" >&2
}
dump_vars() {
echo "=== Variable Dump ===" >&2
declare -p 2>/dev/null | grep -v ' -[aAirx]' >&2
echo "=== End Dump ===" >&2
}
Complete Example: Backup Script
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="$(basename "$0")"
readonly VERSION="1.0.0"
readonly DEFAULT_RETENTION=7
log() { printf '[%s] [%-5s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "${*:2}"; }
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@" >&2; }
die() { error "$@"; exit 1; }
cleanup() {
local ec=$?
[[ -n "${tmpdir:-}" ]] && rm -rf "$tmpdir"
(( ec != )) && error
}
cleanup EXIT
() {
<<
}
dest=
retention=
dry_run=
verbose=
source_dir=
[[ -gt 0 ]];
-d|--dest) dest=; 2 ;;
-r|--retention) retention=; 2 ;;
-n|--dry-run) dry_run=; ;;
-v|--verbose) verbose=; ;;
-h|--) usage 0 ;;
--version) ; 0 ;;
--) ; ;;
-*) die ;;
*) source_dir=; ;;
[[ -n ]] || { error ; usage 1; }
[[ -d ]] || die
-v tar >/dev/null || die
() {
timestamp
timestamp=
archive_name
archive_name=
archive_path=
info
;
info
info
0
-p
tmpdir=
tmp_archive=
tar -czf -C
600
size
size=
info
deleted=0
IFS= -r old_backup;
-f
(( deleted++ ))
&& info
< <(find -name -mtime - f)
(( deleted > )) && info
info
}
main
References