| name | bash |
| description | Bash Shell Script Development Guidelines |
Bash Shell Script Development Guidelines
High-quality, maintainable bash shell scripts following industry best practices.
Critical Requirements
- ALWAYS include shebang
#!/usr/bin/env bash at the top of every script
- ALWAYS use
set -euo pipefail for error handling
- ALWAYS quote variables to prevent word splitting:
"$variable" not $variable
- ALWAYS check if a command exists before using it with
command -v
- ALWAYS run
shellcheck and shfmt before completing any script
- NEVER use
eval unless absolutely necessary and after careful security review
- NEVER parse
ls output - use globs or find instead
- NO EMOJIS in scripts or comments
Quality Gates (NON-NEGOTIABLE)
Before completing any bash script, ALL must pass:
shellcheck script.sh
shfmt -d script.sh
To auto-format a script:
shfmt -w script.sh
Fix all shellcheck errors and warnings. Do not use # shellcheck disable= directives unless absolutely necessary and justified.
Script Structure
Standard Template
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_NAME
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
usage() {
cat <<EOF
Usage: ${SCRIPT_NAME} [OPTIONS] [ARGUMENTS]
Description of what this script does.
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
-d, --debug Enable debug mode
ARGUMENTS:
arg1 Description of argument 1
arg2 Description of argument 2
EXAMPLES:
${SCRIPT_NAME} --verbose file.txt
${SCRIPT_NAME} -d input.txt output.txt
EOF
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $*" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
cleanup() {
:
}
main() {
:
}
trap cleanup EXIT
trap 'log_error "Script failed at line $LINENO"' ERR
main "$@"
Best Practices
Error Handling
-
Use set -euo pipefail:
set -e: Exit on error
set -u: Exit on undefined variable
set -o pipefail: Exit on pipe failure
-
Check command exit codes explicitly when needed:
if ! command_that_might_fail; then
log_error "Command failed"
exit 1
fi
-
Use trap for cleanup:
trap cleanup EXIT
trap 'log_error "Error at line $LINENO"' ERR
Variable Quoting
-
Always quote variables:
cp "$source" "$destination"
cp $source $destination
-
Quote command substitutions:
result="$(some_command)"
result=$(some_command)
-
Use arrays for lists:
files=("file1.txt" "file 2.txt" "file3.txt")
for file in "${files[@]}"; do
process "$file"
done
files="file1.txt file 2.txt file3.txt"
for file in $files; do
process $file
done
Conditionals and Comparisons
-
Use [[ ]] instead of [ ]:
if [[ "$var" == "value" ]]; then
echo "Match"
fi
-
Check if variable is set:
if [[ -n "${var:-}" ]]; then
echo "Variable is set"
fi
if [[ -z "${var:-}" ]]; then
echo "Variable is not set"
fi
-
File tests:
if [[ -f "$file" ]]; then echo "Regular file exists"; fi
if [[ -d "$dir" ]]; then echo "Directory exists"; fi
if [[ -x "$script" ]]; then echo "File is executable"; fi
if [[ -r "$file" ]]; ;
Command Substitution
-
Use $() instead of backticks:
result="$(command)"
result=`command`
-
Check if command exists before using:
if ! command -v jq &> /dev/null; then
log_error "jq is not installed"
exit 1
fi
Functions
-
Use local variables:
process_file() {
local file="$1"
local output="${2:-output.txt}"
cat "$file" > "$output"
}
-
Return values properly:
check_status() {
if [[ -f "$1" ]]; then
return 0
else
return 1
fi
}
get_filename() {
local path="$1"
echo "$(basename "$path")"
}
Argument Parsing
-
Use getopts for simple cases:
while getopts "hvd:" opt; do
case $opt in
h) usage; exit 0 ;;
v) verbose=1 ;;
d) directory="$OPTARG" ;;
\?) log_error "Invalid option: -$OPTARG"; exit 1 ;;
esac
done
shift $((OPTIND - 1))
-
Manual parsing for long options:
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
verbose=1
shift
;;
-o|--output)
output="$2"
shift 2
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
Common Patterns
Working with Files
-
Read file line by line:
while IFS= read -r line; do
echo "Line: $line"
done < "$file"
-
Process files matching pattern:
for file in *.txt; do
[[ -f "$file" ]] || continue
process "$file"
done
while IFS= read -r -d '' file; do
process "$file"
done < <(find . -name "*.txt" -type f -print0)
-
Create temporary files/directories:
temp_file="$(mktemp)"
temp_dir="$(mktemp -d)"
cleanup() {
rm -f "$temp_file"
rm -rf "$temp_dir"
}
trap cleanup EXIT
String Manipulation
-
Parameter expansion:
filename="${path##*/}"
directory="${path%/*}"
extension="${filename##*.}"
basename="${filename%.*}"
value="${var:-default}"
value="${var:=default}"
new="${old/pattern/replacement}"
new="${old//pattern/replacement}"
-
String length and substrings:
length="${#string}"
substring="${string:0:5}"
substring="${string: -5}"
Arrays
-
Array operations:
array=("item1" "item2" "item3")
echo "${array[0]}"
echo "${array[@]}"
echo "${#array[@]}"
for item in "${array[@]}"; do
echo "$item"
done
array+=("item4")
-
Associative arrays (bash 4+):
declare -A map
map["key1"]="value1"
map["key2"]="value2"
for key in "${!map[@]}"; do
echo "$key: ${map[$key]}"
done
Security Considerations
-
Never use user input directly in commands:
eval "rm $user_input"
if [[ "$user_input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
rm -f "$user_input"
fi
-
Use full paths for commands in scripts run as root:
/usr/bin/rm -f "$file"
rm -f "$file"
-
Set secure permissions on sensitive scripts:
chmod 700 sensitive_script.sh
chmod 755 public_script.sh
-
Avoid exposing secrets in process list:
mysql -p"$password" -e "SELECT * FROM users"
mysql --defaults-file="$config_file" -e "SELECT * FROM users"
Common Anti-Patterns to Avoid
-
Parsing ls output:
for file in $(ls *.txt); do
process "$file"
done
for file in *.txt; do
[[ -f "$file" ]] || continue
process "$file"
done
-
Unquoted variables:
if [ $var == $other ]; then
if [[ "$var" == "$other" ]]; then
-
Using echo for output that might contain flags:
echo "$user_input"
printf '%s\n' "$user_input"
-
Cat abuse (UUOC - Useless Use of Cat):
cat file.txt | grep pattern
grep pattern file.txt
-
Not checking if commands exist:
jq '.field' file.json
! -v jq &> /dev/null;
log_error
1
jq file.json
Testing and Validation
ShellCheck (REQUIRED)
Always run shellcheck on scripts - all errors and warnings must be fixed:
shellcheck script.sh
Common issues and fixes:
| Error | Fix |
|---|
| SC2155 | Declare and assign separately: VAR=$(cmd) then readonly VAR |
| SC2086 | Quote the variable: "$var" not $var |
| SC2046 | Quote command substitution: "$(cmd)" not $(cmd) |
| SC2164 | Add ` |
| SC2071 | Use -lt/-gt for numbers: [[ $a -lt $b ]] not [[ $a < $b ]] |
Avoid using disable directives - fix the underlying issue instead.
shfmt (REQUIRED)
Always format scripts with shfmt:
shfmt -d script.sh
shfmt -w script.sh
Code Review Checklist