| name | shell |
| description | Shell scripting standards for Scenescape — shebang, style, and Bash guidelines. |
Shell Scripting Standards for Scenescape
Shebang
Use #!/usr/bin/env bash for portability:
#!/usr/bin/env bash
Not #!/bin/bash (less portable) or #!/bin/sh (POSIX only, limiting).
Code Style
Linting
- Linter: shellcheck with style warnings
- Command:
make lint-shell
Indentation
- Use 2 spaces (never tabs)
- Consistent with project standards
Line Length
- Target: 80-100 characters for readability
- Break long commands with
\ line continuation
if [[ condition ]]; then
echo "Indented with 2 spaces"
if [[ nested ]]; then
echo "Nested indentation"
fi
fi
Best Practices
Exit on Error
Use set -e to exit on any error:
#!/usr/bin/env bash
set -e
set -o pipefail
command1
command2
command3
Strict Mode
For critical scripts, use strict mode:
set -euo pipefail
Trap Errors
Clean up on exit or error:
cleanup() {
echo "Cleaning up..."
rm -f /tmp/tempfile
}
trap cleanup EXIT
trap 'echo "Error on line $LINENO"' ERR
Variables
Naming
- Local variables:
lowercase_with_underscores
- Environment/Global:
UPPERCASE_WITH_UNDERSCORES
- Read-only:
readonly CONSTANT_VALUE
local temp_file="/tmp/data"
local count=0
WORKSPACE_DIR="/workspace"
export DATABASE_PASSWORD
readonly MAX_RETRIES=3
Quoting
Always quote variables to prevent word splitting:
file_path="/path/with spaces/file.txt"
cat "$file_path"
cat $file_path
Default Values
config_file="${CONFIG_FILE:-/etc/default.conf}"
database="${DATABASE_NAME:=scenescape}"
required="${REQUIRED_VAR:?Error: REQUIRED_VAR must be set}"
Conditionals
Use [[ ]] for Tests
Prefer [[ ]] over [ ] or test:
if [[ -f "$file" ]]; then
echo "File exists"
fi
if [[ "$value" == "expected" ]]; then
echo "Match"
fi
if [[ "$filename" == *.txt ]]; then
echo "Text file"
fi
Common Test Operators
[[ -f "$file" ]]
[[ -d "$dir" ]]
[[ -r "$file" ]]
[[ -w "$file" ]]
[[ -x "$file" ]]
[[ -z "$string" ]]
[[ -n "$string" ]]
[[ "$a" == "$b" ]]
[[ "$a" != "$b" ]]
[[ "$a" -eq "$b" ]]
[[ "$a" -ne "$b" ]]
[[ "$a" -lt "$b" ]]
[[ "$a" -gt "$b" ]]
Short-Circuit Logic
command1 && command2
command1 || command2
[[ -f "$file" ]] && [[ -r "$file" ]] && cat "$file"
Functions
Declaration
function_name() {
local arg1="$1"
local arg2="$2"
echo "Processing: $arg1, $arg2"
return 0
}
function_name "value1" "value2"
Return Values
check_status() {
if [[ condition ]]; then
return 0
else
return 1
fi
}
if check_status; then
echo "Check passed"
else
echo "Check failed"
fi
Local Variables
Always use local for function variables:
process_data() {
local input="$1"
local temp_file="/tmp/temp_$$"
}
Command Substitution
Use $() instead of backticks:
current_date=$(date +%Y-%m-%d)
file_count=$(ls | wc -l)
current_date=`date +%Y-%m-%d`
Loops
For Loops
for item in item1 item2 item3; do
echo "$item"
done
for file in /path/*.txt; do
[[ -f "$file" ]] && process "$file"
done
for ((i = 0; i < 10; i++)); do
echo "Iteration $i"
done
While Loops
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
count=0
while [[ $count -lt 10 ]]; do
echo "$count"
((count++))
done
Arrays
Declaration and Access
declare -a services=("controller" "manager" "autocalibration")
echo "${services[0]}"
echo "${services[@]}"
echo "${#services[@]}"
for service in "${services[@]}"; do
echo "Service: $service"
done
Adding Elements
services+=("mapping")
Error Handling
Check Command Success
if command_that_might_fail; then
echo "Success"
else
echo "Failed with exit code: $?"
exit 1
fi
Error Messages to stderr
error_exit() {
echo "Error: $1" >&2
exit 1
}
[[ -f "$config_file" ]] || error_exit "Config file not found: $config_file"
Verbose Error Output
set -x
set +x
File Operations
Reading Files
content=$(cat file.txt)
while IFS= read -r line; do
echo "$line"
done < file.txt
Writing Files
echo "content" > file.txt
echo "more content" >> file.txt
cat > config.txt << EOF
setting1=value1
setting2=value2
EOF
Temporary Files
temp_file=$(mktemp)
trap "rm -f '$temp_file'" EXIT
echo "data" > "$temp_file"
Path Handling
Absolute Paths
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
config_file="$script_dir/config.txt"
Path Components
filepath="/path/to/file.txt"
dirname=$(dirname "$filepath")
basename=$(basename "$filepath")
filename="${basename%.*}"
extension="${basename##*.}"
Process Management
Background Jobs
long_running_command &
pid=$!
wait $pid
wait
Process Substitution
diff <(command1) <(command2)
while read -r line; do
echo "$line"
done < <(command)
Scenescape-Specific Patterns
Docker Commands
env BUILDKIT_PROGRESS=plain docker build \
--build-arg VERSION="$VERSION" \
-t "scenescape-service:$VERSION" \
.
Environment Variables
: "${SUPASS:?SUPASS environment variable must be set}"
: "${DATABASE_PASSWORD:?DATABASE_PASSWORD must be set}"
Makefile Integration
VERSION=$(cat version.txt)
BUILD_DIR="${BUILD_DIR:-build}"
Common Patterns
Retry Logic
retry_command() {
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if command_to_retry; then
return 0
fi
echo "Attempt $attempt failed, retrying..." >&2
((attempt++))
sleep 2
done
return 1
}
Input Validation
validate_input() {
local input="$1"
if [[ -z "$input" ]]; then
echo "Error: Input cannot be empty" >&2
return 1
fi
if [[ ! "$input" =~ ^[0-9]+$ ]]; then
echo "Error: Input must be numeric" >&2
return 1
fi
return 0
}
Logging
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
log "Starting process"
log_error "Something went wrong"
Anti-Patterns to Avoid
❌ Don't use eval (security risk):
eval "$user_input"
args=("$arg1" "$arg2")
command "${args[@]}"
❌ Don't parse ls output:
files=$(ls *.txt)
for file in $files; do
process "$file"
done
for file in *.txt; do
[[ -f "$file" ]] && process "$file"
done
❌ Don't use cat unnecessarily:
cat file.txt | grep pattern
grep pattern file.txt
❌ Don't ignore errors:
command_that_might_fail
if ! command_that_might_fail; then
echo "Command failed" >&2
exit 1
fi
Testing
Manual Testing
Test scripts with:
bash -n script.sh
shellcheck script.sh
bash -x script.sh
Test Different Inputs
test_script() {
./script.sh ""
./script.sh "normal"
./script.sh "with spaces"
./script.sh "$long_string"
}
Documentation
Script Header
#!/usr/bin/env bash
set -euo pipefail
Function Documentation
process_data() {
local input_file="$1"
local output_dir="$2"
}
Performance
Avoid Subshells
count=$(expr $count + 1)
((count++))
Use Built-ins
[[ "$string" == *pattern* ]]
echo "$string" | grep pattern
Portability
Bash-specific Features
Scenescape uses bash, not POSIX sh. These are OK:
[[ ]] tests
- Arrays
$() command substitution
(( )) arithmetic
[[ =~ ]] regex matching
Avoid Bashisms When Possible
But prefer portable constructs when they work equally well.