Complete bash-script toolkit with generation and validation capabilities
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Risky
Do not use without reviewing
Bash (Bourne Again Shell) is a powerful Unix shell and command language. This guide covers modern bash scripting practices and patterns for creating robust, maintainable scripts.
Bash-specific features (not in POSIX sh):
arr=(one two three)declare -A map=([key]=value)[[ conditional expressions$(( )) arithmetic expansion with more operators${var//pattern/replacement} parameter expansion<(command)select keyword for menus** recursive globbing with shopt -s globstarPOSIX sh compatible:
[ test command (single brackets)case statements$()Use Bash when:
Use POSIX sh when:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'Explanation:
set -e (errexit): Exit immediately if a command exits with non-zero statusset -u (nounset): Treat unset variables as an errorset -o pipefail: Return value of pipeline is status of last command to exit with non-zero statusIFS=$'\n\t': Set Internal Field Separator to newline and tab only (prevents word splitting issues)# Disable errexit for commands that are expected to fail
set +e
command_that_might_fail
exit_code=$?
set -e
# Or use || true for single commands
command_that_might_fail || true
# Or handle error explicitly
if ! command_that_might_fail; then
echo "Command failed, but continuing..."
fi# Cleanup function
cleanup() {
local exit_code=$?
echo "Cleaning up..." >&2
rm -f "${temp_file}"
exit "${exit_code}"
}
# Set traps
trap cleanup EXIT # Always run cleanup on exit
trap cleanup ERR # Run cleanup on error
trap cleanup INT TERM # Run cleanup on interrupt or termination
# Create temp file
temp_file=$(mktemp)
# Rest of script...# Pattern 1: Die function
die() {
echo "ERROR: $*" >&2
exit 1
}
[[ -f "${file}" ]] || die "File not found: ${file}"
# Pattern 2: Check function return values
if ! do_something; then
echo "do_something failed" >&2
return 1
fi
# Pattern 3: Command substitution with error handling
output=$(command 2>&1) || {
echo "Command failed: ${output}" >&2
exit 1
}
# Pattern 4: Validate prerequisites
check_command() {
command -v "$1" &> /dev/null || die "Required command not found: $1"
}
check_command "jq"
check_command "curl"# Constants - uppercase with readonly
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/myapp/config.conf"
# Environment variables - uppercase
export PATH="${HOME}/bin:${PATH}"
export LOG_LEVEL="INFO"
# Local variables - lowercase
local counter=0
local temp_file=""
# Function names - lowercase with underscores
process_data() {
local input="$1"
# ...
}# Good - properly quoted
rm "${file}"
cp "${source}" "${destination}"
echo "Value: ${variable}"
# Bad - unquoted (prone to word splitting and globbing)
rm $file
cp $source $destination
echo "Value: $variable"# Default values
${var:-default} # Use default if var is unset or empty
${var:=default} # Set var to default if unset or empty
${var:?error message} # Exit with error message if var is unset or empty
${var:+alternative} # Use alternative if var is set
# String manipulation
${var#pattern} # Remove shortest match from beginning
${var##pattern} # Remove longest match from beginning
${var%pattern} # Remove shortest match from end
${var%%pattern} # Remove longest match from end
${var/pattern/replacement} # Replace first match
${var//pattern/replacement} # Replace all matches
${var^} # Uppercase first character
${var^^} # Uppercase all characters
${var,} # Lowercase first character
${var,,} # Lowercase all characters
# Length and substring
${#var} # Length of var
${var:offset} # Substring from offset to end
${var:offset:length} # Substring from offset with length
# Examples
file="/path/to/file.txt"
${file##*/} # file.txt (basename)
${file%.*} # /path/to/file (remove extension)
${file##*.} # txt (extension only)
${file%/*} # /path/to (dirname)# POSIX style (portable)
function_name() {
# function body
}
# Bash-specific (not portable to sh)
function function_name {
# function body
}
# Recommended: POSIX style with local variables
process_file() {
local input_file="$1"
local output_file="$2"
# Process file
grep "pattern" "${input_file}" > "${output_file}"
}# Global variable
GLOBAL_VAR="global"
my_function() {
# Local variable - only visible in function
local local_var="local"
# Modifying global variable
GLOBAL_VAR="modified"
# Function parameter access
local param1="$1"
local param2="$2"
echo "Params: ${param1} ${param2}"
}
my_function "arg1" "arg2"# Functions return exit status (0-255)
check_file() {
local file="$1"
[[ -f "${file}" ]] && return 0 || return 1
}
# Use function return status
if check_file "data.txt"; then
echo "File exists"
fi
# Return data via stdout
get_value() {
echo "computed value"
}
# Capture output
result=$(get_value)
# Return data via variable (using nameref in bash 4.3+)
get_data() {
local -n result_var=$1
result_var="computed value"
}
get_data my_result
echo "${my_result}"# Array creation
arr=() # Empty array
arr=(one two three) # Initialize with values
arr[0]="first" # Assign to specific index
# Array operations
arr+=("four") # Append
${arr[0]} # Access element
${arr[@]} # All elements (as separate words)
${arr[*]} # All elements (as single word)
${#arr[@]} # Number of elements
${!arr[@]} # Indices
# Iterating over array
for item in "${arr[@]}"; do
echo "${item}"
done
# Iterating with indices
for i in "${!arr[@]}"; do
echo "Index $i: ${arr[i]}"
done
# Array slicing
${arr[@]:offset:length} # Slice array
# Remove element
unset 'arr[1]' # Remove specific element# Declaration required
declare -A map
# Assignment
map[key1]="value1"
map[key2]="value2"
# Or initialize
declare -A map=([key1]="value1" [key2]="value2")
# Access
${map[key1]} # Get value
${map[@]} # All values
${!map[@]} # All keys
${#map[@]} # Number of elements
# Check if key exists
if [[ -v map[key1] ]]; then
echo "key1 exists"
fi
# Iterate over keys and values
for key in "${!map[@]}"; do
echo "${key}: ${map[${key}]}"
done# Use positional parameters
set -- one two three
# Access
echo "$1" # one
echo "$2" # two
echo "$#" # count: 3
# Iterate
for item in "$@"; do
echo "${item}"
done
# Add item
set -- "$@" "four"
# Remove first item
shift# Bash [[ ... ]] (recommended for bash)
if [[ -f "${file}" ]]; then
echo "File exists"
fi
if [[ "${var}" == "value" ]]; then
echo "Match"
fi
if [[ "${var}" =~ ^[0-9]+$ ]]; then
echo "Numeric"
fi
# POSIX [ ... ] (portable)
if [ -f "${file}" ]; then
echo "File exists"
fi
# File tests
[[ -e file ]] # Exists
[[ -f file ]] # Regular file
[[ -d file ]] # Directory
[[ -L file ]] # Symbolic link
[[ -r file ]] # Readable
[[ -w file ]] # Writable
[[ -x file ]] # Executable
[[ -s file ]] # Not empty
# String tests
[[ -z "${var}" ]] # Empty string
[[ -n "${var}" ]] # Non-empty string
[[ "${a}" == "${b}" ]] # Equal
[[ "${a}" != "${b}" ]] # Not equal
[[ "${a}" < "${b}" ]] # Lexicographically less (bash only)
# Numeric tests
[[ "${a}" -eq "${b}" ]] # Equal
[[ "${a}" -ne "${b}" ]] # Not equal
[[ "${a}" -lt "${b}" ]] # Less than
[[ "${a}" -le "${b}" ]] # Less than or equal
[[ "${a}" -gt "${b}" ]] # Greater than
[[ "${a}" -ge "${b}" ]] # Greater than or equal
# Logical operators
[[ condition1 && condition2 ]] # AND
[[ condition1 || condition2 ]] # OR
[[ ! condition ]] # NOTcase "${var}" in
pattern1)
# commands
;;
pattern2|pattern3)
# Multiple patterns
;;
*)
# Default case
;;
esac
# Example with patterns
case "${file}" in
*.txt)
echo "Text file"
;;
*.jpg|*.png)
echo "Image file"
;;
*)
echo "Unknown type"
;;
esac# while loop
while condition; do
# commands
done
# until loop
until condition; do
# commands
done
# for loop (C-style, bash only)
for ((i=0; i<10; i++)); do
echo "${i}"
done
# for loop (iterating over values)
for item in one two three; do
echo "${item}"
done
# for loop (iterating over files)
for file in *.txt; do
echo "${file}"
done
# for loop (iterating over command output)
while IFS= read -r line; do
echo "${line}"
done < file.txt
# Or with command substitution (avoid for large output)
for file in $(find . -name "*.txt"); do
echo "${file}"
done# Recommended: $( ... )
result=$(command)
result=$(command arg1 arg2)
# Nested command substitution
outer=$(echo "Inner: $(echo "value")")
# Not recommended: backticks (legacy)
result=`command`# <( ... ) creates a named pipe/file descriptor
# Treat command output as a file
# Compare output of two commands
diff <(ls dir1) <(ls dir2)
# Use multiple inputs
paste <(cut -f1 file1) <(cut -f2 file2)
# Output redirection with process substitution
command > >(tee stdout.log) 2> >(tee stderr.log >&2)#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# ============================================================================
# Script Name: example.sh
# Description: Brief description
# Author: Your Name
# Created: 2025-01-23
# ============================================================================
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Global variables
VERBOSE=false
DRY_RUN=false
# Functions
usage() {
# ...
}
cleanup() {
# ...
}
main() {
# ...
}
# Signal handlers
trap cleanup EXIT ERR INT TERM
# Execute main
main "$@"# Good
echo "${variable}"
cp "${source}" "${dest}"
[[ -f "${file}" ]]
# Bad (unsafe)
echo $variable
cp $source $dest
[[ -f $file ]]readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/config"# Good
output=$(command)
result=$(first $(second))
# Bad
output=`command`
result=`first \`second\`` # Hard to readif ! command -v required_cmd &> /dev/null; then
echo "Error: required_cmd not found" >&2
exit 1
fi# Check argument count
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <file>" >&2
exit 1
fi
# Validate file exists
[[ -f "${file}" ]] || { echo "File not found: ${file}" >&2; exit 1; }
# Validate numeric input
[[ "${count}" =~ ^[0-9]+$ ]] || { echo "Count must be numeric" >&2; exit 1; }# Problem: Filename with spaces
file="my file.txt"
rm $file # Tries to remove "my" and "file.txt"
# Solution: Quote variables
rm "${file}" # Correctly removes "my file.txt"# Problem: Pattern in variable
pattern="*.txt"
echo $pattern # Expands to list of .txt files
# Solution: Quote to prevent globbing
echo "${pattern}" # Prints "*.txt"# Bad: Unnecessary cat
cat file.txt | grep "pattern"
# Good: Direct input
grep "pattern" file.txt
# Bad: cat in loop
cat file.txt | while read line; do
echo "${line}"
done
# Good: redirect to while
while read -r line; do
echo "${line}"
done < file.txt# Bad: Will break on filenames with spaces
for file in $(find . -name "*.txt"); do
process "${file}"
done
# Good: Use while read
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
process "${file}"
done
# Or use globbing
for file in ./**/*.txt; do
process "${file}"
done# Bad: Ignoring failure
command_that_might_fail
next_command
# Good: Check exit status
if command_that_might_fail; then
next_command
else
echo "Command failed" >&2
exit 1
fi
# Or with errexit
command_that_might_fail || { echo "Failed" >&2; exit 1; }generator
validator