Comprehensive shell/bash engineering guidelines based on Google's Shell Style Guide. This skill should be used when writing shell scripts, reviewing bash code, or answering questions about shell scripting best practices. Applies to .sh files, bash scripts, and any shell programming tasks.
Install
npx skillscat add gonzaloserrano/dotfiles/shell-engineering Install via the SkillsCat registry.
SKILL.md
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/bashwith minimal flags - Libraries must have
.shextension 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 \
| command3Control Structures
; thenand; doon same line asif/while/for:
if [[ -n "${var}" ]]; then
# ...
fi
for file in "${files[@]}"; do
# ...
doneQuoting
- 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[@]}"; doAvoid 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 ./* insteadError 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
fiCheck 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"
fiComments 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 inputTesting 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}" ]]; thenBuilt-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 |