AgentSkillsCN

shell-workflow

Shell 脚本工作流程指南。在处理 Shell 脚本(.sh)、Bash 脚本,或诸如 shellcheck、shfmt 等专为 Shell 设计的工具时启用。

SKILL.md
--- frontmatter
name: shell-workflow
description: Shell script workflow guidelines. Activate when working with shell scripts (.sh), bash scripts, or shell-specific tooling like shellcheck, shfmt.
location: user

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Shell Script Workflow

Tool Grid

TaskToolCommand
Lintshellcheckshellcheck *.sh
Formatshfmtshfmt -w *.sh
Stylebashatebashate *.sh
Securityshellhardenshellharden --check *.sh
POSIX checkcheckbashismscheckbashisms *.sh
Testbatsbats test/
Coveragebashcovbashcov ./test.sh
Coveragekcovkcov coverage/ bats test/

Tool Installation

bash
# Ubuntu/Debian
sudo apt install shellcheck shfmt devscripts  # devscripts includes checkbashisms

# macOS
brew install shellcheck shfmt bash checkbashisms

# pip (bashate)
pip install bashate

# cargo (shellharden)
cargo install shellharden

# npm (bats)
npm install -g bats

Tool Descriptions

  • shellcheck: Static analysis, catches bugs and security issues
  • shfmt: Formats shell scripts consistently
  • bashate: OpenStack style enforcer (E* error codes)
  • shellharden: Suggests safer quoting and variable usage
  • checkbashisms: Finds bash-specific syntax in scripts meant to be POSIX
  • bats: Bash Automated Testing System
  • kcov: Code coverage for bash scripts

Shebang

Scripts MUST use the portable shebang:

bash
#!/usr/bin/env bash

POSIX-only scripts MAY use #!/bin/sh when bash features are not needed.

Strict Mode

All bash scripts MUST enable strict mode at the top:

bash
set -euo pipefail
FlagMeaning
-eExit on error
-uError on undefined variables
-o pipefailFail on pipe errors

For debugging, scripts MAY temporarily add set -x for trace output.

Size Limit

Scripts SHOULD NOT exceed 100 lines of code (excluding comments and blank lines).

When a script exceeds 100 lines:

  • The script SHOULD be refactored into smaller functions
  • Complex logic SHOULD be converted to Python for maintainability

Variable Quoting

Variables MUST be quoted to prevent word splitting and glob expansion:

bash
# Correct
echo "$variable"
cp "$source" "$destination"

# Incorrect - MUST NOT use
echo $variable
cp $source $destination

Arrays MUST use proper expansion:

bash
# Correct
"${array[@]}"

# For single string with spaces
"${array[*]}"

Variable Naming

ScopeConventionExample
Environment/GlobalUPPER_CASELOG_LEVEL, CONFIG_PATH
Local/Scriptlower_casefile_count, temp_dir
ConstantsUPPER_CASEreadonly MAX_RETRIES=3

Constants SHOULD be declared with readonly:

bash
readonly CONFIG_FILE="/etc/app/config"
readonly -a VALID_OPTIONS=("start" "stop" "restart")

Test Syntax

In bash scripts, [[ ]] MUST be used over [ ]:

bash
# Correct - bash
if [[ -f "$file" ]]; then
    echo "File exists"
fi

if [[ "$string" == "value" ]]; then
    echo "Match"
fi

# Pattern matching (bash-only)
if [[ "$string" =~ ^[0-9]+$ ]]; then
    echo "Numeric"
fi

POSIX scripts MUST use [ ] for compatibility.

Functions

Functions MUST use local for internal variables:

bash
my_function() {
    local input="$1"
    local result=""

    # Process input
    result=$(process "$input")

    echo "$result"
}

Function naming conventions:

  • Use snake_case: process_file, validate_input
  • Prefix private functions with underscore: _helper_function

Temporary Files

Temporary files MUST be created with mktemp:

bash
temp_file=$(mktemp)
temp_dir=$(mktemp -d)

Cleanup MUST be ensured with trap:

bash
cleanup() {
    rm -f "$temp_file"
    rm -rf "$temp_dir"
}
trap cleanup EXIT

The EXIT trap ensures cleanup runs on normal exit, error, or interrupt.

Exit Codes

Scripts MUST use standard exit codes:

CodeMeaning
0Success
1General error
2Misuse (invalid arguments, missing dependencies)

Example usage:

bash
main() {
    if [[ $# -lt 1 ]]; then
        echo "Usage: $0 <argument>" >&2
        exit 2
    fi

    if ! process_data "$1"; then
        echo "Error: Processing failed" >&2
        exit 1
    fi

    exit 0
}

Error Handling

Continuing on Error

Use || true when a command failure SHOULD NOT stop execution:

bash
rm -f "$optional_file" || true

Explicit Failure

Use || exit 1 for critical operations:

bash
cd "$required_dir" || exit 1
source "$config_file" || exit 1

Custom Error Messages

bash
command_that_might_fail || {
    echo "Error: command failed" >&2
    exit 1
}

Portability Considerations

POSIX vs Bash

FeaturePOSIXBash
Test syntax[ ][[ ]]
ArraysNot availableSupported
localNot standardSupported
sourceUse .Both work
Process substitutionNot available<(cmd)

Portable Alternatives

When POSIX compatibility is REQUIRED:

bash
# Instead of bash arrays, use positional parameters or newline-separated strings
files=$(find . -name "*.txt")

# Instead of [[ ]], use [ ]
if [ -f "$file" ]; then
    echo "exists"
fi

# Instead of source, use .
. ./config.sh

Input Validation

User input MUST be validated:

bash
validate_input() {
    local input="$1"

    if [[ -z "$input" ]]; then
        echo "Error: Input cannot be empty" >&2
        return 1
    fi

    if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
        echo "Error: Invalid characters in input" >&2
        return 1
    fi

    return 0
}

Command Substitution

Modern syntax MUST be used:

bash
# Correct
result=$(command)
nested=$(echo $(inner_command))

# Incorrect - MUST NOT use backticks
result=`command`

Logging

Scripts SHOULD include consistent logging:

bash
log_info() {
    echo "[INFO] $*"
}

log_error() {
    echo "[ERROR] $*" >&2
}

log_debug() {
    [[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*"
}

Script Template

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

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

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

Options:
    -h, --help    Show this help message
    -v, --verbose Enable verbose output

EOF
}

main() {
    local verbose=0

    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)
                usage
                exit 0
                ;;
            -v|--verbose)
                verbose=1
                shift
                ;;
            *)
                break
                ;;
        esac
    done

    if [[ $# -lt 1 ]]; then
        usage >&2
        exit 2
    fi

    # Script logic here
}

main "$@"