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.
Instrucciones de origen · Vista previa de solo lectura
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.
# --- Main Logic ---------------------------------------------------------------
main
"$@"
# ... your logic here ...
"$@"
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.
# CORRECT -- variables are quoted
name="world"echo"Hello, $name"cp"$source""$destination"# WRONG -- unquoted variables break on spacescp$source$destination# Breaks if paths have spaces# When you DO want globbing (intentionally)for f in *.txt; doecho"Processing: $f"done
Variable Expansion and Defaults
# Default value if unset or empty
db_host="${DB_HOST:-localhost}"
db_port="${DB_PORT:-5432}"# Assign default if unset or empty
: "${LOG_LEVEL:=info}"# Error if variable is unset
: "${API_KEY:?ERROR: API_KEY must be set}"# Substring extraction
filename="report-2024-01-15.csv"echo"${filename:0:6}"# "report"echo"${filename: -3}"# "csv" (note the space before -)# String lengthecho"${#filename}"# 22# Variable indirection
var_name="HOME"echo"${!var_name}"# prints value of $HOME
Removal and Replacement
filepath="/home/user/documents/report.tar.gz"# Remove shortest match from frontecho"${filepath#*/}"# "home/user/documents/report.tar.gz"# Remove longest match from frontecho"${filepath##*/}"# "report.tar.gz" (basename)# Remove shortest match from endecho"${filepath%.*}"# "/home/user/documents/report.tar"# Remove longest match from endecho"${filepath%%.*}"# "/home/user/documents/report"# Pattern substitutionecho"${filepath/user/admin}"# "/home/admin/documents/report.tar.gz"# Replace all occurrences
msg="foo-bar-baz"echo"${msg//-/_}"# "foo_bar_baz"# Case conversion (Bash 4+)
text="Hello World"echo"${text,,}"# "hello world" (lowercase)echo"${text^^}"# "HELLO WORLD" (uppercase)echo"${text~}"# "hELLO WORLD" (toggle first char)
Conditionals and Test Operators
if/elif/else
if [[ -f "$config_file" ]]; thensource"$config_file"elif [[ -f /etc/default/myapp ]]; thensource /etc/default/myapp
elseecho"No configuration found, using defaults."fi
# Arithmetic evaluation
(( count++ ))
(( total = price * quantity ))
if (( age >= 18 )); thenecho"Adult"fi# Ternary-style
(( result = (a > b) ? a : b ))
Loops
for loops
# Iterate over a listfor fruit in apple banana cherry; doecho"Fruit: $fruit"done# C-style for loopfor (( i = 0; i < 10; i++ )); doecho"Iteration $i"done# Iterate over files safelyfor file in /var/log/*.log; do
[[ -f "$file" ]] || continue# Guard against no matchesecho"Log: $file"done# Iterate over command output (line by line)while IFS= read -r line; doecho"Line: $line"done < <(find /tmp -maxdepth 1 -name "*.tmp" -type f)
# Iterate over arraydeclare -a servers=("web01""web02""db01")
for server in"${servers[@]}"; doecho"Pinging $server..."done
while and until
# while loop
counter=0
while (( counter < 5 )); doecho"Count: $counter"
(( counter++ ))
done# Read file line by linewhile IFS= read -r line; doecho">> $line"done < "$input_file"# Read with a custom delimiter (e.g., colon-separated)while IFS=: read -r user _ uid gid _ home shell; doecho"User: $user, Home: $home, Shell: $shell"done < /etc/passwd
# until loop (runs until condition becomes true)until ping -c1 -W1 "$host" &>/dev/null; doecho"Waiting for $host to come online..."sleep 5
doneecho"$host is reachable."
Loop Control
for i in {1..100}; do
(( i % 2 == 0 )) && continue# Skip even numbers
(( i > 20 )) && break# Stop after 20echo"$i"done
Functions and Return Values
# Function definitionlog() {
local level="$1"shiftlocal message="$*"local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"printf'[%s] [%-5s] %s\n'"$timestamp""$level""$message"
}
# Using local variables (always use local in functions)calculate_sum() {
local -i a="$1"local -i b="$2"local -i result
result=$(( a + b ))
echo"$result"# Return value via stdout
}
sum=$(calculate_sum 10 20)
echo"Sum: $sum"# "Sum: 30"# Return codes for success/failure signalingis_valid_ip() {
local ip="$1"local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'if [[ "$ip" =~ $regex ]]; thenreturn 0 # successelsereturn 1 # failurefi
}
if is_valid_ip "192.168.1.1"; thenecho"Valid IP"fi# Function with nameref (Bash 4.3+)get_result() {
local -n ref="$1"
ref="computed value"
}
get_result my_var
echo"$my_var"# "computed value"
# Standard redirectionscommand > file.txt # Redirect stdout (overwrite)command >> file.txt # Redirect stdout (append)command 2> errors.log # Redirect stderrcommand &> all.log # Redirect both stdout and stderrcommand > out.log 2>&1 # Same as above (POSIX compatible)command 2>/dev/null # Discard stderr# Redirect both independentlycommand > stdout.log 2> stderr.log
# Here documentcat <<EOF > /etc/myapp.conf
# Configuration generated on $(date)
server_name=${SERVER_NAME}
port=${PORT:-8080}
EOF# Here document without variable expansion (note the quotes)cat <<'EOF' > script_template.sh
#!/bin/bashecho"This $variable is literal, not expanded"
EOF
# Here string
grep "error" <<< "$log_contents"# Process substitution
diff <(sort file1.txt) <(sort file2.txt)
# Pipeline with error checkingset -o pipefail
cat access.log | grep "500" | awk '{print $1}' | sort -u > failed_ips.txt
# tee -- write to file and stdoutcommand | tee output.log # Display and savecommand | tee -a output.log # Display and appendcommand 2>&1 | tee debug.log # Capture everything# File descriptor manipulationexec 3> custom_output.log # Open fd 3 for writingecho"Custom log entry" >&3
exec 3>&- # Close fd 3
Process Management
# Run in background
long_running_task &
pid=$!
echo"Started background task with PID: $pid"# Wait for specific processwait"$pid"echo"Task exited with status: $?"# Wait for all background jobs
job1 &
job2 &
job3 &
wait# Wait for all# Parallel execution with controlled concurrency
max_jobs=4
for file in /data/*.csv; dowhile (( $(jobs -r | wc -l) >= max_jobs )); dosleep 0.5
done
process_file "$file" &
donewait# Trap signalsshutdown() {
echo"Shutting down gracefully..."# Kill child processeskill -- -$$ 2>/dev/null || trueexit 0
}
trap shutdown SIGINT SIGTERM
# PID file for singleton enforcementacquire_lock() {
local pidfile="$1"if [[ -f "$pidfile" ]]; thenlocal old_pid
old_pid="$(cat "$pidfile")"ifkill -0 "$old_pid" 2>/dev/null; thenecho"Error: Already running (PID $old_pid)" >&2
return 1
fiecho"Removing stale PID file" >&2
fiecho $$ > "$pidfile"
}
release_lock() {
local pidfile="$1"rm -f "$pidfile"
}
# Timeout a commandtimeout 30 long_running_command || {
echo"Command timed out after 30 seconds"exit 1
}
String Manipulation with Parameter Expansion
No need for sed or awk for simple string operations.
str=" Hello, World! "# Trim leading/trailing whitespace (Bash trick)
trimmed="${str#"${str%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"# Check if string contains substringif [[ "$str" == *"World"* ]]; thenecho"Contains 'World'"fi# Split string into array
IFS=','read -ra parts <<< "one,two,three,four"for part in"${parts[@]}"; doecho"Part: $part"done# Join array into stringjoin_by() {
local IFS="$1"shiftecho"$*"
}
result=$(join_by ','"${parts[@]}")
echo"$result"# "one,two,three,four"# Repeat a characterprintf'=%.0s' {1..60}
echo# Uppercase / lowercase first character
name="john"echo"${name^}"# "John"
name="JOHN"echo"${name,}"# "jOHN"
Array Handling
# Indexed arraysdeclare -a fruits=("apple""banana""cherry")
fruits+=("date") # Appendecho"${fruits[0]}"# First elementecho"${fruits[@]}"# All elementsecho"${#fruits[@]}"# Lengthecho"${!fruits[@]}"# All indices# Sliceecho"${fruits[@]:1:2}"# "banana cherry"# Remove element (leaves gap)unset'fruits[1]'# Iteratefor fruit in"${fruits[@]}"; doecho"$fruit"done# Associative arrays (Bash 4+)declare -A config
config[host]="localhost"
config[port]="8080"
config[debug]="true"# Check if key existsif [[ -v config[host] ]]; thenecho"Host: ${config[host]}"fi# Iterate keys and valuesfor key in"${!config[@]}"; doecho"$key = ${config[$key]}"done# Array from command outputmapfile -t lines < <(ls -1 /tmp)
echo"Found ${#lines[@]} items in /tmp"# Array filteringdeclare -a evens=()
for n in {1..20}; do
(( n % 2 == 0 )) && evens+=("$n")
doneecho"Evens: ${evens[*]}"
Error Handling Patterns
# Custom error handlererr_handler() {
local line_no="$1"localcommand="$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 function for fatal errorsdie() {
echo"FATAL: $*" >&2
exit 1
}
# Retry with exponential backoffretry() {
local max_attempts="${1:-3}"local delay="${2:-1}"shift 2
local attempt=1
until"$@"; doif (( attempt >= max_attempts )); thenecho"Command failed after $max_attempts attempts: $*" >&2
return 1
fiecho"Attempt $attempt failed. Retrying in ${delay}s..." >&2
sleep"$delay"
(( attempt++ ))
(( delay *= 2 ))
done
}
# Usage: retry 5 2 curl -sf https://example.com/health# Require commands to existrequire_cmd() {
for cmd in"$@"; docommand -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"done
}
require_cmd git curl jq
# Assert functionassert() {
local description="$1"shiftif ! "$@"; then
die "Assertion failed: $description"fi
}
assert "Config file exists"test -f /etc/myapp.conf
File Operations
# Safe temporary files
tmpfile="$(mktemp)"
tmpdir="$(mktemp -d)"trap'rm -rf "$tmpfile" "$tmpdir"' EXIT
# Find files with various criteria
find /var/log -name "*.log" -mtime +30 -type f -delete # Delete logs older than 30 days
find . -name "*.sh" -execchmod +x {} + # Make all .sh files executable
find . -type f -size +100M # Find files over 100MB# Portable file readingwhile IFS= read -r line || [[ -n "$line" ]]; doecho"$line"done < "$file"# Note: || [[ -n "$line" ]] handles files without trailing newline# Atomic file write (write to temp, then move)atomic_write() {
local target="$1"local tmp
tmp="$(mktemp "${target}.XXXXXX")"ifcat > "$tmp" && mv -f "$tmp""$target"; thenreturn 0
elserm -f "$tmp"return 1
fi
}
echo"new content" | atomic_write /etc/myapp.conf
# Check and create directoryensure_dir() {
localdir="$1"if [[ ! -d "$dir" ]]; thenmkdir -p "$dir" || die "Cannot create directory: $dir"fi
}
# Compare filesif cmp -s file1.txt file2.txt; thenecho"Files are identical"elseecho"Files differ"fi# Basename and dirname without external commands
path="/home/user/docs/report.pdf"echo"${path##*/}"# "report.pdf" (basename)echo"${path%/*}"# "/home/user/docs" (dirname)
Portable Scripting (POSIX Compliance)
# Use #!/bin/sh for POSIX scripts, #!/usr/bin/env bash for Bash scripts# POSIX-compatible alternatives:# Instead of [[ ]], use [ ] with proper quotingif [ -f "$file" ] && [ -r "$file" ]; thenecho"File exists and is readable"fi# Instead of (( )), use [ ] with -eq, -lt, etc.if [ "$count" -gt 10 ]; thenecho"Count exceeds 10"fi# Instead of $() for arithmetic, use expr or $(( ))
total=$((a + b))
# Instead of arrays (not POSIX), use positional parameters or IFS splitting# Instead of local (not strictly POSIX), most shells support it anyway# Instead of Bash-specific string manipulation, use cut, sed, or tr# Bash: echo "${var,,}"# POSIX: echo "$var" | tr '[:upper:]' '[:lower:]'# Use printf instead of echo -e (echo behavior varies across shells)printf'Line 1\nLine 2\n'# Check your scripts with shellcheck# shellcheck disable=SC2034 -- Inline suppression# Run: shellcheck -s bash script.sh
# Here document with variable expansiongenerate_html() {
local title="$1"local body="$2"cat <<-EOF
<!DOCTYPE html>
<html>
<head><title>${title}</title></head>
<body>${body}</body>
</html>
EOF
}
# Here document passed to a command's stdin
mysql -u root <<SQL
CREATE DATABASE IF NOT EXISTS myapp;
GRANT ALL ON myapp.* TO 'appuser'@'localhost';
SQL# Here string (Bash extension)while IFS=, read -r name age city; doecho"Name: $name, Age: $age, City: $city"done <<< "Alice,30,NYC
Bob,25,LA
Charlie,35,Chicago"# Indent-stripped here doc (use <<- with tabs)iftrue; thencat <<-'USAGE'
Usage: command [options]
-h Show help
-v Verbose mode
USAGE
fi
Security Best Practices
# NEVER use eval with user input# BAD: eval "$user_input"# BAD: eval "echo $untrusted"# GOOD: Use arrays and direct execution# Quote EVERYTHINGrm"$file"# GOODrm$file# BAD -- breaks on spaces, globs could expand# Validate inputsvalidate_filename() {
local name="$1"if [[ "$name" =~ [^a-zA-Z0-9._-] ]]; then
die "Invalid filename: $name (contains special characters)"fiif [[ "$name" == ..* || "$name" == */* ]]; then
die "Invalid filename: $name (path traversal attempt)"fi
}
# Use -- to end option parsing (prevents option injection)rm -- "$file"
grep -- "$pattern""$file"# Restrict PATHexport PATH="/usr/local/bin:/usr/bin:/bin"# Use secure temp files
tmpfile="$(mktemp)" || die "Failed to create temp file"chmod 600 "$tmpfile"# Avoid writing secrets to the command line (visible in ps)# BAD: mysql -p"$password" ...# GOOD: Use environment variables or config filesexport MYSQL_PWD="$password"
mysql -u root mydb
# Do not store secrets in shell variables that get exported# If you must, unset them after useunset MYSQL_PWD
# Prevent glob expansion when not neededset -f # Disable globbing# ... process user input ...set +f # Re-enable globbing# Drop privileges when running as rootif [[ "$(id -u)" -eq 0 ]]; thenexec su -s /bin/bash nobody -- "$0""$@"fi
Useful One-Liners and Idioms
# Check if running as root
(( EUID == 0 )) || die "Must run as root"# Check if a command existscommand -v docker >/dev/null 2>&1 || die "Docker is not installed"# Portable way to get the script's directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"# Default variable using :- vs -# ${var:-default} uses default if var is unset OR empty# ${var-default} uses default only if var is unset# Read password without echoingread -rsp "Enter password: " password
echo# Confirm before proceedingconfirm() {
read -rp "${1:-Are you sure?} [y/N] " response
[[ "$response" =~ ^[Yy]$ ]]
}
confirm "Delete all files?" || exit 0
# Progress indicatorspin() {
local -a frames=('|''/''-''\')
whiletrue; dofor frame in"${frames[@]}"; doprintf'\r%s %s'"$frame""$1"sleep 0.2
donedone
}
spin "Working..." &
spinner_pid=$!
# ... do work ...kill"$spinner_pid" 2>/dev/null
printf'\rDone. \n'# Measure execution time
start_time="$(date +%s)"# ... do work ...
end_time="$(date +%s)"echo"Elapsed: $(( end_time - start_time )) seconds"# Generate random string
random_string=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16)
# Check if stdin is a terminalif [[ -t 0 ]]; thenecho"Interactive mode"elseecho"Reading from pipe or file"fi# Coalesce empty values
result="${value1:-${value2:-${value3:-fallback}}}"
Script Debugging
# Enable debug tracingset -x # Print each command before executionset +x # Disable tracing# Custom trace prompt for better readabilityexport PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}():} '# Debug only a sectiondebug_section() {
set -x
# ... commands to debug ...set +x
}
# Conditional debugging via environment variableif [[ "${DEBUG:-}" == "true" ]]; thenset -x
fi# Debug function that respects verbositydebug() {
[[ "${VERBOSE:-false}" == "true" ]] && echo"DEBUG: $*" >&2
}
# Trace function callstrace_calls() {
echo"TRACE: ${FUNCNAME[1]} called from ${FUNCNAME[2]:-main} (line ${BASH_LINENO[1]})" >&2
}
# Dump all variables (useful for debugging)dump_vars() {
echo"=== Variable Dump ===" >&2
declare -p 2>/dev/null | grep -v ' -[aAirx]' >&2
echo"=== End Dump ===" >&2
}
# Run script in debug mode from the command line:# bash -x script.sh# bash -xv script.sh (also shows the script lines being read)
Complete Example: Backup Script
#!/usr/bin/env bash## backup.sh - Incremental backup script with rotation#set -euo pipefail
IFS=$'\n\t'readonly SCRIPT_NAME="$(basename "$0")"readonly VERSION="1.0.0"readonly DEFAULT_RETENTION=7
# --- Logging ------------------------------------------------------------------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 ------------------------------------------------------------------cleanup() {
local ec=$?
[[ -n "${tmpdir:-}" ]] && rm -rf "$tmpdir"
(( ec != 0 )) && error "Backup failed with exit code $ec"exit"$ec"
}
trap cleanup EXIT
# --- Usage --------------------------------------------------------------------usage() {
cat <<HELP
Usage: ${SCRIPT_NAME} [OPTIONS] <source-directory>
Creates a compressed, timestamped backup of the given directory.
Options:
-d, --dest DIR Destination directory (default: /backups)
-r, --retention DAYS Delete backups older than DAYS (default: ${DEFAULT_RETENTION})
-n, --dry-run Show what would be done
-v, --verbose Verbose output
-h, --help Show this help
--version Show version
Examples:
${SCRIPT_NAME} /etc
${SCRIPT_NAME} -d /mnt/nas/backups -r 30 /var/www
HELPexit"${1:-0}"
}
# --- Parse Arguments ----------------------------------------------------------
dest="/backups"
retention="$DEFAULT_RETENTION"
dry_run=false
verbose=false
source_dir=""while [[ $# -gt 0 ]]; docase"$1"in
-d|--dest) dest="${2:?--dest requires a value}"; shift 2 ;;
-r|--retention) retention="${2:?--retention requires a value}"; shift 2 ;;
-n|--dry-run) dry_run=true; shift ;;
-v|--verbose) verbose=true; shift ;;
-h|--help) usage 0 ;;
--version) echo"${SCRIPT_NAME} v${VERSION}"; exit 0 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) source_dir="$1"; shift ;;
esacdone
[[ -n "$source_dir" ]] || { error "Source directory required"; usage 1; }
[[ -d "$source_dir" ]] || die "Source is not a directory: $source_dir"command -v tar >/dev/null || die "tar is required but not found"# --- Main Logic ---------------------------------------------------------------main() {
local timestamp
timestamp="$(date '+%Y%m%d-%H%M%S')"local archive_name
archive_name="backup-$(basename "$source_dir")-${timestamp}.tar.gz"local archive_path="${dest}/${archive_name}"
info "Backing up: $source_dir -> $archive_path"if"$dry_run"; then
info "[DRY RUN] Would create: $archive_path"
info "[DRY RUN] Would remove backups older than $retention days"return 0
fimkdir -p "$dest"
tmpdir="$(mktemp -d)"local tmp_archive="${tmpdir}/${archive_name}"
tar -czf "$tmp_archive" -C "$(dirname "$source_dir")""$(basename "$source_dir")"mv"$tmp_archive""$archive_path"chmod 600 "$archive_path"local size
size="$(du -sh "$archive_path" | cut -f1)"
info "Backup complete: $archive_path ($size)"# Rotate old backupslocal deleted=0
while IFS= read -r old_backup; dorm -f "$old_backup"
(( deleted++ ))
"$verbose" && info "Deleted old backup: $old_backup"done < <(find "$dest" -name "backup-$(basename "$source_dir")-*.tar.gz" -mtime "+${retention}" -type f)
(( deleted > 0 )) && info "Removed $deleted old backup(s)"
info "Done."
}
main