| name | shell-best-practices |
| user-invocable | false |
| description | Use when writing shell scripts following modern best practices. Covers portable scripting, Bash patterns, error handling, and secure coding. |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob"] |
Shell Scripting Best Practices
Comprehensive guide to writing robust, maintainable, and secure shell scripts following modern best practices.
Script Foundation
Shebang Selection
Choose the appropriate shebang for your needs:
Strict Mode
Always enable strict error handling:
#!/usr/bin/env bash
set -euo pipefail
For debugging, add:
set -x
Script Header Template
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
Variable Handling
Always Quote Variables
Prevents word splitting and glob expansion:
echo "$variable"
cp "$source" "$destination"
if [ -f "$file" ]; then
echo $variable
cp $source $destination
if [ -f $file ]; then
Use Meaningful Names
readonly config_file="/etc/app/config.yml"
local user_input="$1"
declare -a log_files=()
readonly f="/etc/app/config.yml"
local x="$1"
declare -a arr=()
Default Values
name="${NAME:-default_value}"
name="${NAME:-}"
: "${NAME:=default_value}"
: "${REQUIRED_VAR:?Error: REQUIRED_VAR must be set}"
Readonly and Local
readonly MAX_RETRIES=3
readonly CONFIG_DIR="/etc/myapp"
my_function() {
local input="$1"
local result=""
}
Error Handling
Exit Codes
Use meaningful exit codes:
readonly EXIT_SUCCESS=0
readonly EXIT_FAILURE=1
readonly EXIT_INVALID_ARGS=2
readonly EXIT_NOT_FOUND=3
exit "$EXIT_FAILURE"
Trap for Cleanup
cleanup() {
local exit_code=$?
rm -f "${temp_file:-}"
exit "$exit_code"
}
trap cleanup EXIT
temp_file=$(mktemp)
Error Messages
error() {
echo "ERROR: $*" >&2
}
warn() {
echo "WARNING: $*" >&2
}
die() {
error "$@"
exit 1
}
[[ -f "$config_file" ]] || die "Config file not found: $config_file"
Validate Inputs
validate_args() {
if [[ $# -lt 1 ]]; then
die "Usage: $SCRIPT_NAME <input_file>"
fi
local input_file="$1"
[[ -f "$input_file" ]] || die "File not found: $input_file"
[[ -r "$input_file" ]] || die "File not readable: $input_file"
}
Functions
Function Definition
process_log() {
local log_file="$1"
local output_dir="${2:-./output}"
[[ -f "$log_file" ]] || return 1
grep -i "error" "$log_file" > "$output_dir/errors.log"
}
Return Values
is_valid() {
[[ -n "$1" && "$1" =~ ^[0-9]+$ ]]
}
if is_valid "$input"; then
echo "Valid"
fi
get_config_value() {
local key="$1"
grep "^${key}=" "$config_file" | cut -d= -f2
}
value=$(get_config_value "database_host")
Conditionals
Use [[ ]] for Tests
if [[ -f "$file" ]]; then
if [[ "$string" == "value" ]]; then
if [[ "$string" =~ ^[0-9]+$ ]]; then
if [ -f "$file" ]; then
if [ "$string" = "value" ]; then
Numeric Comparisons
if (( count > 10 )); then
if (( a == b )); then
if (( x >= 0 && x <= 100 )); then
if [[ "$count" -gt 10 ]]; then
String Comparisons
if [[ "$str" == "value" ]]; then
if [[ "$str" == *.txt ]]; then
if [[ "$str" =~ ^[a-z]+$ ]]; then
if [[ -z "$str" ]]; then
if [[ -n "$str" ]]; then
Loops
Iterate Over Files
for file in *.txt; do
[[ -e "$file" ]] || continue
process "$file"
done
while IFS= read -r -d '' file; do
process "$file"
done < <(find . -name "*.txt" -print0)
for file in $(ls *.txt); do
Read Lines from File
while IFS= read -r line; do
echo "$line"
done < "$filename"
while IFS= read -r line; do
echo "$line"
done < <(some_command)
Iterate with Index
files=("one.txt" "two.txt" "three.txt")
for i in "${!files[@]}"; do
echo "Index $i: ${files[i]}"
done
Arrays
Declaration and Usage
declare -a files=()
files+=("file1.txt")
files+=("file2.txt")
for f in "${files[@]}"; do
echo "$f"
done
echo "${#files[@]}"
declare -A config
config[host]="localhost"
config[port]="8080"
echo "${config[host]}"
Array Best Practices
"${array[@]}"
"${array[*]}"
if [[ ${#array[@]} -eq 0 ]]; then
echo "Empty array"
fi
if [[ -v config[key] ]]; then
echo "Key exists"
fi
Command Execution
Check Command Existence
if command -v docker &>/dev/null; then
echo "Docker is installed"
fi
require_command() {
command -v "$1" &>/dev/null || die "Required command not found: $1"
}
require_command git
require_command docker
Capture Output and Status
output=$(some_command)
if output=$(some_command 2>&1); then
echo "Success: $output"
else
echo "Failed: $output" >&2
fi
if some_command &>/dev/null; then
echo "Command succeeded"
fi
Safe Command Substitution
result=$(command)
result=`command`
result=$(echo $(date))
Portability
POSIX vs Bash
| Feature | POSIX | Bash |
|---|
| Test syntax | [ ] | [[ ]] |
| Arrays | No | Yes |
$() | Yes | Yes |
${var//pat/rep} | No | Yes |
[[ =~ ]] regex | No | Yes |
(( )) arithmetic | No | Yes |
Portable Alternatives
if [ -f "$file" ]; then
if [ "$str" = "value" ]; then
if [ "$count" -gt 10 ]; then
echo "$var" | sed 's/pat/rep/g'
files="one.txt two.txt three.txt"
for f in $files; do
echo "$f"
done
Security
Avoid Eval
eval "$user_input"
cmd=("grep" "-r" "$pattern" "$directory")
"${cmd[@]}"
Sanitize Inputs
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
die "Invalid input format"
fi
escaped=$(printf '%q' "$input")
Temporary Files
temp_file=$(mktemp) || die "Failed to create temp file"
trap 'rm -f "$temp_file"' EXIT
temp_dir=$(mktemp -d) || die "Failed to create temp dir"
trap 'rm -rf "$temp_dir"' EXIT
Logging
Basic Logging
readonly LOG_FILE="/var/log/myapp.log"
log() {
local level="$1"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@" >&2; }
log_error() { log "ERROR" "$@" >&2; }
log_info "Starting process"
log_error "Failed to connect"
Verbose Mode
VERBOSE="${VERBOSE:-false}"
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo "DEBUG: $*" >&2
fi
}
Complete Script Template
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly EXIT_SUCCESS=0
readonly EXIT_FAILURE=1
readonly EXIT_INVALID_ARGS=2
log_info() { echo "[INFO] $*"; }
log_error() { echo "[ERROR] $*" >&2; }
die() {
log_error "$@"
exit "$EXIT_FAILURE"
}
cleanup() {
local exit_code=$?
rm -f "${temp_file:-}"
exit "$exit_code"
}
trap cleanup EXIT
() {
<<
}
() {
OPTIND opt
opt;
h) usage; ;;
v) VERBOSE= ;;
o) OUTPUT_DIR= ;;
-)
) usage; ;;
verbose) VERBOSE= ;;
output=*) OUTPUT_DIR= ;;
*) die ;;
;;
:) die ;;
\?) die ;;
$((OPTIND - ))
[[ -lt 1 ]];
usage
INPUT_FILE=
}
() {
[[ -f ]] || die
[[ -r ]] || die
-p || die
}
() {
VERBOSE=
OUTPUT_DIR=
parse_args
validate
log_info
log_info
}
main
When to Use This Skill
- Writing new shell scripts from scratch
- Reviewing shell scripts for issues
- Refactoring legacy shell code
- Debugging script failures
- Improving script security
- Making scripts more portable
- Setting up proper error handling