shell-best-practices

Shell Scripting Best Practices

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "shell-best-practices" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-shell-best-practices

Shell Scripting Best Practices

Comprehensive guide to writing robust, maintainable, and secure shell scripts following modern best practices.

Script Foundation

Shebang Selection

Choose the appropriate shebang for your needs:

Portable bash (recommended)

#!/usr/bin/env bash

Direct bash path (faster, less portable)

#!/bin/bash

POSIX-compliant shell (most portable)

#!/bin/sh

Specific shell version

#!/usr/bin/env bash

Requires Bash 4.0+

Strict Mode

Always enable strict error handling:

#!/usr/bin/env bash set -euo pipefail

What these do:

-e: Exit immediately on command failure

-u: Treat unset variables as errors

-o pipefail: Pipeline fails if any command fails

For debugging, add:

set -x # Print commands as they execute

Script Header Template

#!/usr/bin/env bash set -euo pipefail

Script: script-name.sh

Description: Brief description of what this script does

Usage: ./script-name.sh [options] <arguments>

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"

Variable Handling

Always Quote Variables

Prevents word splitting and glob expansion:

Good

echo "$variable" cp "$source" "$destination" if [ -f "$file" ]; then

Bad - can break on spaces/special chars

echo $variable cp $source $destination if [ -f $file ]; then

Use Meaningful Names

Good

readonly config_file="/etc/app/config.yml" local user_input="$1" declare -a log_files=()

Bad

readonly f="/etc/app/config.yml" local x="$1" declare -a arr=()

Default Values

Use default if unset

name="${NAME:-default_value}"

Use default if unset or empty

name="${NAME:-}"

Assign default if unset

: "${NAME:=default_value}"

Error if unset (with message)

: "${REQUIRED_VAR:?Error: REQUIRED_VAR must be set}"

Readonly and Local

Constants

readonly MAX_RETRIES=3 readonly CONFIG_DIR="/etc/myapp"

Function-local variables

my_function() { local input="$1" local result="" # ... }

Error Handling

Exit Codes

Use meaningful exit codes:

Standard codes

readonly EXIT_SUCCESS=0 readonly EXIT_FAILURE=1 readonly EXIT_INVALID_ARGS=2 readonly EXIT_NOT_FOUND=3

Exit with code

exit "$EXIT_FAILURE"

Trap for Cleanup

cleanup() { local exit_code=$? # Clean up temporary files rm -f "${temp_file:-}" # Restore state if needed exit "$exit_code" }

trap cleanup EXIT

Script continues...

temp_file=$(mktemp)

Error Messages

error() { echo "ERROR: $*" >&2 }

warn() { echo "WARNING: $*" >&2 }

die() { error "$@" exit 1 }

Usage

[[ -f "$config_file" ]] || die "Config file not found: $config_file"

Validate Inputs

validate_args() { if [[ $# -lt 1 ]]; then die "Usage: $SCRIPT_NAME <input_file>" fi

local input_file="$1"
[[ -f "$input_file" ]] || die "File not found: $input_file"
[[ -r "$input_file" ]] || die "File not readable: $input_file"

}

Functions

Function Definition

Document functions

Process a log file and extract errors

Arguments:

$1 - Path to log file

$2 - Output directory (optional, default: ./output)

Returns:

0 on success, 1 on failure

process_log() { local log_file="$1" local output_dir="${2:-./output}"

[[ -f "$log_file" ]] || return 1

grep -i "error" "$log_file" > "$output_dir/errors.log"

}

Return Values

Return status

is_valid() { [[ -n "$1" && "$1" =~ ^[0-9]+$ ]] }

if is_valid "$input"; then echo "Valid" fi

Capture output

get_config_value() { local key="$1" grep "^${key}=" "$config_file" | cut -d= -f2 }

value=$(get_config_value "database_host")

Conditionals

Use [[ ]] for Tests

Good - [[ ]] is more powerful and safer

if [[ -f "$file" ]]; then if [[ "$string" == "value" ]]; then if [[ "$string" =~ ^[0-9]+$ ]]; then

Avoid - [ ] has limitations

if [ -f "$file" ]; then if [ "$string" = "value" ]; then

Numeric Comparisons

Use (( )) for arithmetic

if (( count > 10 )); then if (( a == b )); then if (( x >= 0 && x <= 100 )); then

Or -eq/-lt/-gt in [[ ]]

if [[ "$count" -gt 10 ]]; then

String Comparisons

Equality

if [[ "$str" == "value" ]]; then

Pattern matching

if [[ "$str" == *.txt ]]; then

Regex matching

if [[ "$str" =~ ^[a-z]+$ ]]; then

Empty/non-empty

if [[ -z "$str" ]]; then # empty if [[ -n "$str" ]]; then # non-empty

Loops

Iterate Over Files

Good - handles spaces in filenames

for file in *.txt; do [[ -e "$file" ]] || continue # Skip if no matches process "$file" done

With find for recursive

while IFS= read -r -d '' file; do process "$file" done < <(find . -name "*.txt" -print0)

Bad - breaks on spaces

for file in $(ls *.txt); do # Don't do this

Read Lines from File

Correct - preserves whitespace

while IFS= read -r line; do echo "$line" done < "$filename"

With process substitution

while IFS= read -r line; do echo "$line" done < <(some_command)

Iterate with Index

files=("one.txt" "two.txt" "three.txt")

for i in "${!files[@]}"; do echo "Index $i: ${files[i]}" done

Arrays

Declaration and Usage

Indexed array

declare -a files=() files+=("file1.txt") files+=("file2.txt")

Access all elements

for f in "${files[@]}"; do echo "$f" done

Array length

echo "${#files[@]}"

Associative array (Bash 4+)

declare -A config config[host]="localhost" config[port]="8080"

echo "${config[host]}"

Array Best Practices

Quote expansions

"${array[@]}" # All elements, word-split "${array[*]}" # All elements, single string

Check if empty

if [[ ${#array[@]} -eq 0 ]]; then echo "Empty array" fi

Check for key (associative)

if [[ -v config[key] ]]; then echo "Key exists" fi

Command Execution

Check Command Existence

Preferred method

if command -v docker &>/dev/null; then echo "Docker is installed" fi

In conditionals

require_command() { command -v "$1" &>/dev/null || die "Required command not found: $1" }

require_command git require_command docker

Capture Output and Status

Capture output

output=$(some_command)

Capture output and status

if output=$(some_command 2>&1); then echo "Success: $output" else echo "Failed: $output" >&2 fi

Check status without output

if some_command &>/dev/null; then echo "Command succeeded" fi

Safe Command Substitution

Use $() not backticks

result=$(command) # Good result=command # Avoid

Nested substitution

result=$(echo $(date)) # Works with $()

Portability

POSIX vs Bash

Feature POSIX Bash

Test syntax [ ]

[[ ]]

Arrays No Yes

$()

Yes Yes

${var//pat/rep}

No Yes

[[ =~ ]] regex No Yes

(( )) arithmetic No Yes

Portable Alternatives

Instead of [[ ]], use [ ] with quotes

if [ -f "$file" ]; then if [ "$str" = "value" ]; then

Instead of (( )), use [ ] with -eq

if [ "$count" -gt 10 ]; then

Instead of ${var//pat/rep}

echo "$var" | sed 's/pat/rep/g'

Instead of arrays, use space-separated strings

files="one.txt two.txt three.txt" for f in $files; do echo "$f" done

Security

Avoid Eval

Bad - code injection risk

eval "$user_input"

Better - use arrays for command building

cmd=("grep" "-r" "$pattern" "$directory") "${cmd[@]}"

Sanitize Inputs

Validate expected format

if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then die "Invalid input format" fi

Escape for use in commands

escaped=$(printf '%q' "$input")

Temporary Files

Secure temp file creation

temp_file=$(mktemp) || die "Failed to create temp file" trap 'rm -f "$temp_file"' EXIT

Secure temp directory

temp_dir=$(mktemp -d) || die "Failed to create temp dir" trap 'rm -rf "$temp_dir"' EXIT

Logging

Basic Logging

readonly LOG_FILE="/var/log/myapp.log"

log() { local level="$1" shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE" }

log_info() { log "INFO" "$@"; } log_warn() { log "WARN" "$@" >&2; } log_error() { log "ERROR" "$@" >&2; }

Usage

log_info "Starting process" log_error "Failed to connect"

Verbose Mode

VERBOSE="${VERBOSE:-false}"

debug() { if [[ "$VERBOSE" == "true" ]]; then echo "DEBUG: $*" >&2 fi }

Enable with: VERBOSE=true ./script.sh

Complete Script Template

#!/usr/bin/env bash set -euo pipefail

=============================================================================

Script: example.sh

Description: Template demonstrating shell best practices

Usage: ./example.sh [options] <input_file>

=============================================================================

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"

Exit codes

readonly EXIT_SUCCESS=0 readonly EXIT_FAILURE=1 readonly EXIT_INVALID_ARGS=2

Logging functions

log_info() { echo "[INFO] $"; } log_error() { echo "[ERROR] $" >&2; }

Error handling

die() { log_error "$@" exit "$EXIT_FAILURE" }

cleanup() { local exit_code=$? rm -f "${temp_file:-}" exit "$exit_code" } trap cleanup EXIT

Argument parsing

usage() { cat <<EOF Usage: $SCRIPT_NAME [options] <input_file>

Options: -h, --help Show this help message -v, --verbose Enable verbose output -o, --output Output directory (default: ./output)

Examples: $SCRIPT_NAME input.txt $SCRIPT_NAME -v -o /tmp/output input.txt EOF }

parse_args() { local OPTIND opt while getopts ":hvo:-:" opt; do case "$opt" in h) usage; exit "$EXIT_SUCCESS" ;; v) VERBOSE=true ;; o) OUTPUT_DIR="$OPTARG" ;; -) case "$OPTARG" in help) usage; exit "$EXIT_SUCCESS" ;; verbose) VERBOSE=true ;; output=) OUTPUT_DIR="${OPTARG#=}" ;; *) die "Unknown option: --$OPTARG" ;; esac ;; :) die "Option -$OPTARG requires an argument" ;; ?) die "Unknown option: -$OPTARG" ;; esac done shift $((OPTIND - 1))

if [[ $# -lt 1 ]]; then
    usage
    exit "$EXIT_INVALID_ARGS"
fi

INPUT_FILE="$1"

}

Validate inputs

validate() { [[ -f "$INPUT_FILE" ]] || die "File not found: $INPUT_FILE" [[ -r "$INPUT_FILE" ]] || die "File not readable: $INPUT_FILE" mkdir -p "$OUTPUT_DIR" || die "Cannot create output directory" }

Main logic

main() { # Defaults VERBOSE="${VERBOSE:-false}" OUTPUT_DIR="${OUTPUT_DIR:-./output}"

parse_args "$@"
validate

log_info "Processing $INPUT_FILE"
# ... main logic here ...
log_info "Done"

}

main "$@"

When to Use This Skill

  • Writing new shell scripts from scratch

  • Reviewing shell scripts for issues

  • Refactoring legacy shell code

  • Debugging script failures

  • Improving script security

  • Making scripts more portable

  • Setting up proper error handling

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

atomic-design-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review