shell scripts
This guide establishes standards for shell scripts in the lintro project to ensure consistency, reliability, and maintainability.
Shell Script Style Guide
This guide establishes standards for shell scripts in the lintro project to ensure consistency, reliability, and maintainability.
Standard Preamble
Every shell script must start with this preamble:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../utils/utils.sh" # Adjust path as needed
Explanation
#!/usr/bin/env bash: Portable shebang that finds bash in PATHset -e: Exit immediately if a command failsset -u: Treat unset variables as errorsset -o pipefail: Pipeline fails if any command failsSCRIPT_DIR: Reliable way to get the script’s directorysource utils.sh: Import shared logging and utility functions
Help Message Pattern
All scripts should support --help and -h flags:
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
cat <<'EOF'
Brief description of what the script does.
Usage: script-name.sh <required-arg> [optional-arg]
Arguments:
required-arg Description of required argument
optional-arg Description of optional argument (default: value)
Environment:
SOME_VAR Description of environment variable
Examples:
script-name.sh foo
script-name.sh foo bar
EOF
exit 0
fi
Or use the show_help function from utils.sh:
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
show_help "script-name.sh" "Brief description" "<required-arg> [optional-arg]"
exit 0
fi
Logging Standards
Use the logging functions from utils.sh:
log_info "Starting process..." # Blue info message
log_success "Operation completed" # Green success message
log_warning "Something unexpected" # Yellow warning message
log_error "Operation failed" # Red error message
log_verbose "Debug details" # Only shown when VERBOSE=1
When to Use Each Level
| Level | Use Case | Example |
|---|---|---|
log_info | Progress updates, status | “Processing 50 files…” |
log_success | Completed operations | “Build completed successfully” |
log_warning | Non-fatal issues | “File not found, using default” |
log_error | Failures (usually before exit) | “Required tool not installed” |
log_verbose | Debug info (VERBOSE=1) | “Checking file: /path/to/file” |
Variable Handling
Required Variables
Use parameter expansion with error messages:
REQUIRED_VAR="${1:?Usage: script.sh <required-arg>}"
Optional Variables with Defaults
OPTIONAL_VAR="${2:-default_value}"
Environment Variables
Document and provide defaults:
# Configuration (can be overridden via environment)
MAX_RETRIES="${MAX_RETRIES:-3}"
TIMEOUT="${TIMEOUT:-30}"
Error Handling
Check Command Success
if ! some_command; then
log_error "some_command failed"
exit 1
fi
Cleanup on Exit
Use traps for cleanup:
cleanup() {
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
TEMP_DIR=$(mktemp -d)
Or use the create_temp_dir function:
TEMP_DIR=$(create_temp_dir) # Auto-cleanup on exit
Validate Prerequisites
Check for required tools early:
for cmd in git curl jq; do
if ! command -v "$cmd" &> /dev/null; then
log_error "Required command not found: $cmd"
exit 1
fi
done
GitHub Actions Integration
Setting Outputs
Use the helper function:
set_github_output "key" "value"
Or directly:
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "key=value" >> "$GITHUB_OUTPUT"
fi
Setting Environment Variables
set_github_env "MY_VAR" "my_value"
Multiline Values
{
echo "key<<EOF"
echo "line 1"
echo "line 2"
echo "EOF"
} >> "$GITHUB_OUTPUT"
Git Configuration for CI
configure_git_ci_user # Sets github-actions[bot] identity
File Operations
Check File Existence
if [[ -f "$file" ]]; then
log_info "Processing $file"
else
log_warning "File not found: $file"
fi
Or use the helper:
check_file_exists "$file" "Configuration file"
Safe File Writing
# Write to temp file first, then move
tmp_file=$(mktemp)
echo "content" > "$tmp_file"
mv "$tmp_file" "$target_file"
Quoting Rules
- Always quote variables:
"$var"not$var - Quote command substitutions:
"$(command)" - Arrays need special handling:
"${array[@]}"
# Good
file_path="$HOME/documents/my file.txt"
result="$(some_command "$file_path")"
# Bad
file_path=$HOME/documents/my file.txt
result=$(some_command $file_path)
Conditionals
String Comparisons
if [[ "$var" == "value" ]]; then
# ...
fi
Numeric Comparisons
if [[ "$count" -gt 10 ]]; then
# ...
fi
File Tests
[[ -f "$file" ]] # File exists and is regular file
[[ -d "$dir" ]] # Directory exists
[[ -x "$cmd" ]] # File is executable
[[ -n "$var" ]] # Variable is non-empty
[[ -z "$var" ]] # Variable is empty
Loops
Iterate Over Files
for file in *.txt; do
[[ -f "$file" ]] || continue # Skip if no matches
process "$file"
done
Read Lines from File
while IFS= read -r line; do
process "$line"
done < "$file"
Functions
Definition
# Brief description of function
# Arguments:
# $1 - description
# $2 - description (optional, default: value)
# Returns:
# 0 on success, 1 on failure
my_function() {
local arg1="$1"
local arg2="${2:-default}"
# Function body
}
Local Variables
Always use local for function variables:
my_function() {
local result
result=$(some_command)
echo "$result"
}
Script Organization
#!/usr/bin/env bash
set -euo pipefail
# ==============================================================================
# Script Name: my-script.sh
# Description: Brief description
# ==============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../utils/utils.sh"
# ------------------------------------------------------------------------------
# Constants
# ------------------------------------------------------------------------------
readonly VERSION="1.0.0"
readonly DEFAULT_TIMEOUT=30
# ------------------------------------------------------------------------------
# Functions
# ------------------------------------------------------------------------------
show_usage() {
# ...
}
main() {
# Parse arguments
# Validate prerequisites
# Execute main logic
}
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
show_usage
exit 0
fi
main "$@"
Common Patterns
Retry Logic
retry() {
local max_attempts="${1:-3}"
local delay="${2:-5}"
shift 2
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if "$@"; then
return 0
fi
log_warning "Attempt $attempt/$max_attempts failed, retrying in ${delay}s..."
sleep "$delay"
((attempt++))
done
log_error "All $max_attempts attempts failed"
return 1
}
# Usage
retry 3 5 curl -sf "$url"
Progress Indicator
total=${#files[@]}
current=0
for file in "${files[@]}"; do
((current++))
log_info "Processing [$current/$total]: $file"
process "$file"
done
Linting
All scripts should pass shellcheck:
shellcheck scripts/**/*.sh
Common shellcheck directives when needed:
# shellcheck disable=SC2034 # Variable appears unused
# shellcheck source=scripts/utils/utils.sh
source "$SCRIPT_DIR/../utils/utils.sh"
Testing
For complex scripts, consider adding tests:
# scripts/tests/test_my_script.sh
test_function_returns_expected() {
result=$(my_function "input")
[[ "$result" == "expected" ]] || {
echo "FAIL: expected 'expected', got '$result'"
return 1
}
echo "PASS: test_function_returns_expected"
}
test_function_returns_expected