Shell Engineering
Comprehensive guidelines for writing production-quality shell scripts based on Google's Shell Style Guide.
When to Use Shell
-
Small utilities and simple wrapper scripts
-
Scripts calling other tools with straightforward logic
-
Rewrite in a structured language (Go, Python) when exceeding ~100 lines or using complex control flow
Shell Choice
-
Bash is the only permitted shell for executables
-
Start scripts with #!/bin/bash with minimal flags
-
Libraries must have .sh extension and not be executable
-
SUID/SGID are forbidden on shell scripts
File Structure
#!/bin/bash
Brief description of the script's purpose.
set -euo pipefail
Constants and environment variables (UPPERCASE)
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly LOG_FILE="/tmp/script.log"
Source libraries
source "${SCRIPT_DIR}/lib/utils.sh"
Function definitions (lowercase_with_underscores)
my_function() { local arg1="$1"
...
}
Main function
main() {
Script logic here
}
main "$@"
Formatting Rules
Indentation and Length
-
2 spaces for indentation (no tabs)
-
80 characters maximum line length
-
Split long pipelines with pipe at line start:
command1
| command2
| command3
Control Structures
- ; then and ; do on same line as if /while /for :
if [[ -n "${var}" ]]; then
...
fi
for file in "${files[@]}"; do
...
done
Quoting
-
Always quote strings with variables, command substitutions, or spaces
-
Use "${var}" format with braces for clarity
-
Use "$@" not $* for argument lists
-
Use arrays for lists with spaces in elements
Naming Conventions
Type Convention Example
Functions lowercase_underscores
process_file()
Variables lowercase_underscores
file_count
Constants UPPERCASE_UNDERSCORES
readonly MAX_RETRIES=3
Environment vars UPPERCASE
export PATH
Source files lowercase_underscores.sh
string_utils.sh
Preferred Syntax
Use These
Command substitution
result=$(command)
Test conditions
if [[ -n "${var}" ]]; then
Arithmetic
if (( count > 10 )); then total=$(( a + b ))
Local variables in functions
my_func() { local name="$1" }
Arrays for lists
files=("file1.txt" "file2.txt" "file with spaces.txt") for f in "${files[@]}"; do
Avoid These
Backticks (use $() instead)
result=command
Single brackets (use [[ ]] instead)
if [ -n "$var" ]; then
let, expr, $[ ] (use $(( )) instead)
let count=count+1
eval (security risk)
eval "$cmd"
Piping to while (loses variable scope)
cat file | while read line; do
alias in scripts (use functions)
alias ll='ls -la'
Unquoted wildcards
for f in ; do # Use ./ instead
Error Handling
STDERR for Errors
err() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2 }
if ! process_file "${file}"; then err "Failed to process ${file}" exit 1 fi
Check Return Values
Direct if check
if ! mv "${file}" "${dest}"; then err "Failed to move file" fi
Pipeline status
tar -cf - . | gzip > archive.tar.gz if (( PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0 )); then err "Archive creation failed" fi
Comments and Documentation
File Header (Required)
#!/bin/bash
Script description explaining purpose and usage.
Usage: script.sh [options] <input_file>
Function Documentation
#######################################
Process a data file and output results.
Globals:
OUTPUT_DIR
Arguments:
$1 - Input file path
$2 - Output format (csv|json)
Outputs:
Writes processed data to OUTPUT_DIR
Returns:
0 on success, non-zero on error
####################################### process_data() { local input_file="$1" local format="${2:-csv}"
...
}
TODO Comments
TODO(username): Handle edge case for empty input
Testing and Validation
-
Use ShellCheck to identify bugs
-
Test string emptiness explicitly:
Good
if [[ -z "${var}" ]]; then # empty if [[ -n "${var}" ]]; then # non-empty
Avoid
if [[ "${var}" ]]; then
Built-in Preference
Prefer bash builtins over external commands:
Good: parameter expansion
filename="${path##/}" extension="${filename##.}" basename="${filename%.*}"
Avoid: external commands
filename=$(basename "$path") extension=$(echo "$filename" | sed 's/.*.//')
Quick Reference
Do Don't
$(command)
command
[[ condition ]]
[ condition ]
(( arithmetic ))
let , expr
"${var}"
$var
"$@"
$*
local var
global variables in functions
./* wildcards * wildcards
functions aliases
arrays space-separated strings