shell-scripting
ユーザーの要望に応じて、シェルスクリプトやbashを用いた自動化、ファイル処理、システム管理スクリプトなどを効率的に作成するSkill。
📜 元の英語説明(参考)
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.
🇯🇵 日本人クリエイター向け解説
ユーザーの要望に応じて、シェルスクリプトやbashを用いた自動化、ファイル処理、システム管理スクリプトなどを効率的に作成するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o shell-scripting.zip https://jpskill.com/download/6126.zip && unzip -o shell-scripting.zip && rm shell-scripting.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/6126.zip -OutFile "$d\shell-scripting.zip"; Expand-Archive "$d\shell-scripting.zip" -DestinationPath $d -Force; ri "$d\shell-scripting.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
shell-scripting.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
shell-scriptingフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-17
- 取得日時
- 2026-05-17
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
シェルスクリプトとBashのベストプラクティス
堅牢で移植性が高く、保守しやすいシェルスクリプトを作成するための包括的なガイドです。Bashのイディオム、POSIX準拠、エラー処理、セキュリティ、および実用的なパターンをカバーしています。
Bashスクリプトのテンプレート
すべてのスクリプトは、しっかりとした基盤から始めるべきです。
#!/usr/bin/env bash
#
# script-name.sh - Brief description of what the script does
#
# Usage: script-name.sh [OPTIONS] <arguments>
#
# Author: Your Name
# Date: 2024-01-01
set -euo pipefail
IFS=$'\n\t'
# --- Constants ----------------------------------------------------------------
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly VERSION="1.0.0"
# --- Cleanup Trap -------------------------------------------------------------
cleanup() {
local exit_code=$?
# Remove temp files, release locks, etc.
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 Logic ---------------------------------------------------------------
main() {
parse_args "$@"
validate_dependencies
# ... your logic here ...
}
main "$@"
オプションの意味
set -e-- コマンドが失敗した場合、直ちに終了します。set -u-- 未設定の変数をエラーとして扱います。set -o pipefail-- パイプライン内のいずれかのコマンドが失敗した場合、最後のコマンドだけでなく、パイプライン全体が失敗します。IFS=$'\n\t'-- より安全な単語分割です。ファイル名にスペースが含まれることによる問題を回避します。
変数の扱い
クォーティングルール
単語分割やグロビングが明示的に必要な場合を除き、常に変数を二重引用符で囲んでください。
# CORRECT -- variables are quoted
name="world"
echo "Hello, $name"
cp "$source" "$destination"
# WRONG -- unquoted variables break on spaces
cp $source $destination # Breaks if paths have spaces
# When you DO want globbing (intentionally)
for f in *.txt; do
echo "Processing: $f"
done
変数展開とデフォルト値
# 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 length
echo "${#filename}" # 22
# Variable indirection
var_name="HOME"
echo "${!var_name}" # prints value of $HOME
削除と置換
filepath="/home/user/documents/report.tar.gz"
# Remove shortest match from front
echo "${filepath#*/}" # "home/user/documents/report.tar.gz"
# Remove longest match from front
echo "${filepath##*/}" # "report.tar.gz" (basename)
# Remove shortest match from end
echo "${filepath%.*}" # "/home/user/documents/report.tar"
# Remove longest match from end
echo "${filepath%%.*}" # "/home/user/documents/report"
# Pattern substitution
echo "${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)
条件分岐とテスト演算子
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
テスト演算子
# File tests
[[ -e "$path" ]] # Exists (file, directory, symlink, etc.)
[[ -f "$path" ]] # Regular file
[[ -d "$path" ]] # Directory
[[ -L "$path" ]] # Symlink
[[ -r "$path" ]] # Readable
[[ -w "$path" ]] # Writable
[[ -x "$path" ]] # Executable
[[ -s "$path" ]] # Non-empty file
[[ "$a" -nt "$b" ]] # a is newer than b
[[ "$a" -ot "$b" ]] # a is older than b
# String tests
[[ -z "$str" ]] # Empty string
[[ -n "$str" ]] # Non-empty string
[[ "$a" == "$b" ]] # String equality
[[ "$a" != "$b" ]] # String inequality
[[ "$a" == *.txt ]] # Glob pattern match
[[ "$a" =~ ^[0-9]+$ ]] # Regex match
# Numeric comparisons
[[ "$x" -eq "$y" ]] # Equal
[[ "$x" -ne "$y" ]] # Not equal
[[ "$x" -lt "$y" ]] # Less than
[[ "$x" -gt "$y" ]] # Greater than
[[ "$x" -le "$y" ]] # Less than or equal
[[ "$x" -ge "$y" ]] # Greater than or equal
# Logical operators inside [[ ]]
[[ -f "$f" && -r "$f" ]] # AND
[[ -f "$f" || -d "$f" ]] # OR
[[ ! -e "$path" ]] # NOT
算術演算
# Arithmetic evaluation
(( count++ ))
(( total = price * quantity ))
if (( age >= 18 )); then
echo "Adult"
fi
# Ternary-style
(( result = (a > b) ? a : b ))
ループ
for ループ
# Iterate over a list
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
# C-style for loop
for (( i = 0; i < 10; i++ )); do
echo "Iteration $i"
done
# Iterate over files safely
for file in /var/log/*.log; do
[[ -f "$file" ]] || continue # Guard against no matches
echo "Log: "$file"
done
# Iterate over command output (line by line)
while IFS= read -r line; do
echo "Line: $line"
done < <(find /tmp -maxdepth 1 -name "*.tmp" -type f)
# Iterate over array
declare -a servers=("web01" "web02" "db01")
for server in "${servers[@]}"; do
echo "Pinging $server..."
done
while と until
# while loop
counter=0
while (( counter < 5 )); do
echo "Count: $counter"
(( counter++ ))
done
# Read file line by line
while IFS= read -r line; do
echo ">> $line"
done < "$input_file"
# Read with a custom delimiter (e.g., colon-separated)
while IFS=: read -r user _ uid gid _ home shell; 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
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
#
# script-name.sh - Brief description of what the script does
#
# Usage: script-name.sh [OPTIONS] <arguments>
#
# Author: Your Name
# Date: 2024-01-01
set -euo pipefail
IFS=$'\n\t'
# --- Constants ----------------------------------------------------------------
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly VERSION="1.0.0"
# --- Cleanup Trap -------------------------------------------------------------
cleanup() {
local exit_code=$?
# Remove temp files, release locks, etc.
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 Logic ---------------------------------------------------------------
main() {
parse_args "$@"
validate_dependencies
# ... your logic here ...
}
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.
# CORRECT -- variables are quoted
name="world"
echo "Hello, $name"
cp "$source" "$destination"
# WRONG -- unquoted variables break on spaces
cp $source $destination # Breaks if paths have spaces
# When you DO want globbing (intentionally)
for f in *.txt; do
echo "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 length
echo "${#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 front
echo "${filepath#*/}" # "home/user/documents/report.tar.gz"
# Remove longest match from front
echo "${filepath##*/}" # "report.tar.gz" (basename)
# Remove shortest match from end
echo "${filepath%.*}" # "/home/user/documents/report.tar"
# Remove longest match from end
echo "${filepath%%.*}" # "/home/user/documents/report"
# Pattern substitution
echo "${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" ]]; then
source "$config_file"
elif [[ -f /etc/default/myapp ]]; then
source /etc/default/myapp
else
echo "No configuration found, using defaults."
fi
Test Operators
# File tests
[[ -e "$path" ]] # Exists (file, directory, symlink, etc.)
[[ -f "$path" ]] # Regular file
[[ -d "$path" ]] # Directory
[[ -L "$path" ]] # Symlink
[[ -r "$path" ]] # Readable
[[ -w "$path" ]] # Writable
[[ -x "$path" ]] # Executable
[[ -s "$path" ]] # Non-empty file
[[ "$a" -nt "$b" ]] # a is newer than b
[[ "$a" -ot "$b" ]] # a is older than b
# String tests
[[ -z "$str" ]] # Empty string
[[ -n "$str" ]] # Non-empty string
[[ "$a" == "$b" ]] # String equality
[[ "$a" != "$b" ]] # String inequality
[[ "$a" == *.txt ]] # Glob pattern match
[[ "$a" =~ ^[0-9]+$ ]] # Regex match
# Numeric comparisons
[[ "$x" -eq "$y" ]] # Equal
[[ "$x" -ne "$y" ]] # Not equal
[[ "$x" -lt "$y" ]] # Less than
[[ "$x" -gt "$y" ]] # Greater than
[[ "$x" -le "$y" ]] # Less than or equal
[[ "$x" -ge "$y" ]] # Greater than or equal
# Logical operators inside [[ ]]
[[ -f "$f" && -r "$f" ]] # AND
[[ -f "$f" || -d "$f" ]] # OR
[[ ! -e "$path" ]] # NOT
Arithmetic
# Arithmetic evaluation
(( count++ ))
(( total = price * quantity ))
if (( age >= 18 )); then
echo "Adult"
fi
# Ternary-style
(( result = (a > b) ? a : b ))
Loops
for loops
# Iterate over a list
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
# C-style for loop
for (( i = 0; i < 10; i++ )); do
echo "Iteration $i"
done
# Iterate over files safely
for file in /var/log/*.log; do
[[ -f "$file" ]] || continue # Guard against no matches
echo "Log: $file"
done
# Iterate over command output (line by line)
while IFS= read -r line; do
echo "Line: $line"
done < <(find /tmp -maxdepth 1 -name "*.tmp" -type f)
# Iterate over array
declare -a servers=("web01" "web02" "db01")
for server in "${servers[@]}"; do
echo "Pinging $server..."
done
while and until
# while loop
counter=0
while (( counter < 5 )); do
echo "Count: $counter"
(( counter++ ))
done
# Read file line by line
while IFS= read -r line; do
echo ">> $line"
done < "$input_file"
# Read with a custom delimiter (e.g., colon-separated)
while IFS=: read -r user _ uid gid _ home shell; do
echo "User: $user, Home: $home, Shell: $shell"
done < /etc/passwd
# until loop (runs until condition becomes true)
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 # Skip even numbers
(( i > 20 )) && break # Stop after 20
echo "$i"
done
Functions and Return Values
# Function definition
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"
}
# 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 signaling
is_valid_ip() {
local ip="$1"
local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
if [[ "$ip" =~ $regex ]]; then
return 0 # success
else
return 1 # failure
fi
}
if is_valid_ip "192.168.1.1"; then
echo "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"
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}"
}
# Defaults
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="$1"
shift
;;
esac
done
# Remaining positional arguments after --
[[ -n "$input_file" ]] || { echo "Error: input file required" >&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
# Standard redirections
command > file.txt # Redirect stdout (overwrite)
command >> file.txt # Redirect stdout (append)
command 2> errors.log # Redirect stderr
command &> all.log # Redirect both stdout and stderr
command > out.log 2>&1 # Same as above (POSIX compatible)
command 2>/dev/null # Discard stderr
# Redirect both independently
command > stdout.log 2> stderr.log
# Here document
cat <<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/bash
echo "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 checking
set -o pipefail
cat access.log | grep "500" | awk '{print $1}' | sort -u > failed_ips.txt
# tee -- write to file and stdout
command | tee output.log # Display and save
command | tee -a output.log # Display and append
command 2>&1 | tee debug.log # Capture everything
# File descriptor manipulation
exec 3> custom_output.log # Open fd 3 for writing
echo "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 process
wait "$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; do
while (( $(jobs -r | wc -l) >= max_jobs )); do
sleep 0.5
done
process_file "$file" &
done
wait
# Trap signals
shutdown() {
echo "Shutting down gracefully..."
# Kill child processes
kill -- -$$ 2>/dev/null || true
exit 0
}
trap shutdown SIGINT SIGTERM
# PID file for singleton enforcement
acquire_lock() {
local pidfile="$1"
if [[ -f "$pidfile" ]]; then
local old_pid
old_pid="$(cat "$pidfile")"
if kill -0 "$old_pid" 2>/dev/null; then
echo "Error: Already running (PID $old_pid)" >&2
return 1
fi
echo "Removing stale PID file" >&2
fi
echo $$ > "$pidfile"
}
release_lock() {
local pidfile="$1"
rm -f "$pidfile"
}
# Timeout a command
timeout 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 substring
if [[ "$str" == *"World"* ]]; then
echo "Contains 'World'"
fi
# Split string into array
IFS=',' read -ra parts <<< "one,two,three,four"
for part in "${parts[@]}"; do
echo "Part: $part"
done
# Join array into string
join_by() {
local IFS="$1"
shift
echo "$*"
}
result=$(join_by ',' "${parts[@]}")
echo "$result" # "one,two,three,four"
# Repeat a character
printf '=%.0s' {1..60}
echo
# Uppercase / lowercase first character
name="john"
echo "${name^}" # "John"
name="JOHN"
echo "${name,}" # "jOHN"
Array Handling
# Indexed arrays
declare -a fruits=("apple" "banana" "cherry")
fruits+=("date") # Append
echo "${fruits[0]}" # First element
echo "${fruits[@]}" # All elements
echo "${#fruits[@]}" # Length
echo "${!fruits[@]}" # All indices
# Slice
echo "${fruits[@]:1:2}" # "banana cherry"
# Remove element (leaves gap)
unset 'fruits[1]'
# Iterate
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Associative arrays (Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
config[debug]="true"
# Check if key exists
if [[ -v config[host] ]]; then
echo "Host: ${config[host]}"
fi
# Iterate keys and values
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# Array from command output
mapfile -t lines < <(ls -1 /tmp)
echo "Found ${#lines[@]} items in /tmp"
# Array filtering
declare -a evens=()
for n in {1..20}; do
(( n % 2 == 0 )) && evens+=("$n")
done
echo "Evens: ${evens[*]}"
Error Handling Patterns
# Custom error handler
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 function for fatal errors
die() {
echo "FATAL: $*" >&2
exit 1
}
# Retry with exponential backoff
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
sleep "$delay"
(( attempt++ ))
(( delay *= 2 ))
done
}
# Usage: retry 5 2 curl -sf https://example.com/health
# Require commands to exist
require_cmd() {
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"
done
}
require_cmd git curl jq
# Assert function
assert() {
local description="$1"
shift
if ! "$@"; 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" -exec chmod +x {} + # Make all .sh files executable
find . -type f -size +100M # Find files over 100MB
# Portable file reading
while IFS= read -r line || [[ -n "$line" ]]; do
echo "$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")"
if cat > "$tmp" && mv -f "$tmp" "$target"; then
return 0
else
rm -f "$tmp"
return 1
fi
}
echo "new content" | atomic_write /etc/myapp.conf
# Check and create directory
ensure_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir" || die "Cannot create directory: $dir"
fi
}
# Compare files
if cmp -s file1.txt file2.txt; then
echo "Files are identical"
else
echo "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 quoting
if [ -f "$file" ] && [ -r "$file" ]; then
echo "File exists and is readable"
fi
# Instead of (( )), use [ ] with -eq, -lt, etc.
if [ "$count" -gt 10 ]; then
echo "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
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
# Parse a simple key=value config file
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
# Skip blank lines and comments
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
# Extract key and value
local key="${line%%=*}"
local value="${line#*=}"
# Trim whitespace
key="${key#"${key%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
# Remove surrounding quotes from value
value="${value#\"}"
value="${value%\"}"
CONFIG["$key"]="$value"
done < "$config_file"
}
parse_config /etc/myapp.conf
echo "DB host: ${CONFIG[db_host]:-localhost}"
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 "$entry" >&2
fi
}
log INFO "Application started"
log DEBUG "Verbose debugging info"
log ERROR "Something went wrong"
Here Documents and Here Strings
# Here document with variable expansion
generate_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; do
echo "Name: $name, Age: $age, City: $city"
done <<< "Alice,30,NYC
Bob,25,LA
Charlie,35,Chicago"
# Indent-stripped here doc (use <<- with tabs)
if true; then
cat <<-'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 EVERYTHING
rm "$file" # GOOD
rm $file # BAD -- breaks on spaces, globs could expand
# Validate inputs
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
}
# Use -- to end option parsing (prevents option injection)
rm -- "$file"
grep -- "$pattern" "$file"
# Restrict PATH
export 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 files
export MYSQL_PWD="$password"
mysql -u root mydb
# Do not store secrets in shell variables that get exported
# If you must, unset them after use
unset MYSQL_PWD
# Prevent glob expansion when not needed
set -f # Disable globbing
# ... process user input ...
set +f # Re-enable globbing
# Drop privileges when running as root
if [[ "$(id -u)" -eq 0 ]]; then
exec 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 exists
command -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 echoing
read -rsp "Enter password: " password
echo
# Confirm before proceeding
confirm() {
read -rp "${1:-Are you sure?} [y/N] " response
[[ "$response" =~ ^[Yy]$ ]]
}
confirm "Delete all files?" || exit 0
# Progress indicator
spin() {
local -a frames=('|' '/' '-' '\')
while true; do
for frame in "${frames[@]}"; do
printf '\r%s %s' "$frame" "$1"
sleep 0.2
done
done
}
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 terminal
if [[ -t 0 ]]; then
echo "Interactive mode"
else
echo "Reading from pipe or file"
fi
# Coalesce empty values
result="${value1:-${value2:-${value3:-fallback}}}"
Script Debugging
# Enable debug tracing
set -x # Print each command before execution
set +x # Disable tracing
# Custom trace prompt for better readability
export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}():} '
# Debug only a section
debug_section() {
set -x
# ... commands to debug ...
set +x
}
# Conditional debugging via environment variable
if [[ "${DEBUG:-}" == "true" ]]; then
set -x
fi
# Debug function that respects verbosity
debug() {
[[ "${VERBOSE:-false}" == "true" ]] && echo "DEBUG: $*" >&2
}
# Trace function calls
trace_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
HELP
exit "${1:-0}"
}
# --- Parse Arguments ----------------------------------------------------------
dest="/backups"
retention="$DEFAULT_RETENTION"
dry_run=false
verbose=false
source_dir=""
while [[ $# -gt 0 ]]; do
case "$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 ;;
esac
done
[[ -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
fi
mkdir -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 backups
local deleted=0
while IFS= read -r old_backup; do
rm -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