mcpField
S
shell-scripting

shell-scripting

github.com/sammcj开发167160 次安装2 天前更新
代码仓库

关于 shell-scripting

shell-scripting 是编写可靠、易维护 Bash 脚本的指导技能,帮助开发者判断适合使用 Shell 的任务,并采用防御性模式、自动检查和复杂度边界。

主要功能

  1. 01使用严格模式和可移植 shebang
  2. 02遵循 ShellCheck 检查结果
  3. 03引用变量并验证输入
  4. 04安全处理数组和清理操作
  5. 05优先使用内置功能和进程替换
  6. 06判断何时改用 Python 或 Go

适用场景

  1. 01编写小型自动化或包装脚本
  2. 02检查 Bash 脚本中的常见安全问题
  3. 03添加清理逻辑和输入验证
  4. 04判断脚本是否已超出 Shell 的适用范围

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sammcj/agentic-coding --skill shell-scripting

安装后在 Claude Code 中输入/shell-scripting确认已加载。

详细介绍

Bash 脚本最佳实践

编写可靠、易维护 Bash 脚本的指南,遵循现代最佳实践。强调简洁、自动化工具和防御式编程,避免过度设计。

何时使用 Shell(以及何时不使用)

适用于 Shell 的场景:
  • 小型实用程序和简单的包装脚本(<100 行)
  • 编排其他程序和工具
  • 简单的自动化任务
  • 逻辑直接的构建/部署脚本
  • 快速的数据转换流水线
不适用于 Shell 的场景:
  • 复杂的业务逻辑或数据结构
  • 对性能要求极高的代码
  • 需要大量错误处理的脚本
  • 超过约 100 行,或控制流不简单的任何脚本
  • 需要数组以外的正规数据结构时

如果脚本超过约 100 行或变得复杂,应在它变得难以维护之前,提出用 Python 或 Go 重写。

必备基础

每个 Bash 脚本都必须包含以下要素:

1. 正确的 Shebang
#!/usr/bin/env bash

原因:在 Bash 不一定位于 /bin/bash 的系统(例如 macOS、BSD、NixOS)上具有可移植性。

替代方案:如果确定脚本只在 Linux 上运行,且倾向于使用明确路径,可以使用 #!/bin/bash。

2. 严格模式
set -euo pipefail

各标志的作用:

  • -e:任何命令失败(退出码非零)时立即退出
  • -u:将未设置的变量视为错误
  • -o pipefail:管道中的任何命令失败时,管道都视为失败(而不只是最后一条命令)

何时添加 -x:只用于调试,不要用于生产脚本(会产生过多输出)。

3. 符合 ShellCheck 规范

提交前对每个脚本运行 ShellCheck:

shellcheck script.sh

修复所有警告。ShellCheck 能发现:

  • 未加引号的变量
  • 已弃用的语法
  • 常见错误和陷阱
  • 可移植性问题
4. 基本脚本结构
#!/usr/bin/env bash
set -euo pipefail

# Brief description of what this script does

# Simple error reporting
die() {
    echo "Error: ${1}" >&2
    exit 1
}

# Your code here

核心安全模式

始终为变量加引号

原因:防止单词拆分和通配符扩展导致的问题。

# Wrong - dangerous
cp $source $destination
rm -rf $prefix/bin

# Correct - safe
cp "${source}" "${destination}"
rm -rf "${prefix}/bin"

# Special case: Always use braces with variables
echo "${var}"      # Good
echo "$var"        # Acceptable but less consistent
echo $var          # Bad - unquoted
检查必需变量
# Fail fast if required variables aren't set
: "${REQUIRED_VAR:?REQUIRED_VAR must be set}"

# Or with custom message
: "${DATABASE_URL:?DATABASE_URL is required. Set it in .env}"
验证输入
# Check file exists before operating on it
[[ -f "${config_file}" ]] || die "Config file not found: ${config_file}"

# Check command exists before using it
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"

# Validate directory before cd
[[ -d "${target_dir}" ]] || die "Directory does not exist: ${target_dir}"

必备模式

模式 1:简单脚本模板

用于逻辑直接的脚本:

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

# Description: Process log files and extract errors

die() {
    echo "Error: ${1}" >&2
    exit 1
}

# Check dependencies
command -v jq >/dev/null 2>&1 || die "jq required"

# Validate arguments
[[ $# -eq 1 ]] || die "Usage: ${0} <logfile>"
logfile="${1}"
[[ -f "${logfile}" ]] || die "File not found: ${logfile}"

# Main logic
grep ERROR "${logfile}" | jq -r '.message'
模式 2:退出时清理

使用 trap 确保执行清理:

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

# Create temp directory and ensure cleanup
tmpdir=$(mktemp -d)
trap 'rm -rf "${tmpdir}"' EXIT

# Now use tmpdir safely - cleanup happens automatically
echo "Working in: ${tmpdir}"
模式 3:安全的函数定义

函数应简洁且专注于单一职责:

# Good: Simple, single-purpose function
check_dependency() {
    local cmd="${1}"
    command -v "${cmd}" >/dev/null 2>&1 || die "${cmd} not installed"
}

# Good: Local variables, clear purpose
process_file() {
    local file="${1}"
    local output="${2}"

    [[ -f "${file}" ]] || die "Input file missing: ${file}"

    # Do processing
    sed 's/foo/bar/g' "${file}" > "${output}"
}

重要:将命令替换得到的变量分开声明和赋值,以便捕获错误:

# Wrong - hides errors
local result="$(failing_command)"

# Correct - catches errors
local result
result="$(failing_command)"  # Will fail properly with set -e
模式 4:安全处理数组

数组适合处理包含空格的列表:

# Create array
declare -a files=("file one.txt" "file two.txt" "file three.txt")

# Iterate safely - always quote with [@]
for file in "${files[@]}"; do
    echo "Processing: ${file}"
done

# Build command arguments safely
declare -a flags=(--verbose --output "${output_file}")
mycommand "${flags[@]}" "${input}"

# Read command output into array
mapfile -t lines < <(grep pattern "${file}")
模式 5:条件测试

在 Bash 中使用 [[ ]](更安全、功能更丰富):

# File tests
[[ -f "${file}" ]]          # File exists
[[ -d "${dir}" ]]           # Directory exists
[[ -r "${file}" ]]          # File readable
[[ -w "${file}" ]]          # File writable
[[ -x "${binary}" ]]        # File executable

# String tests
[[ -z "${var}" ]]           # String is empty
[[ -n "${var}" ]]           # String is not empty
[[ "${a}" == "${b}" ]]      # String equality (use ==, not =)

# Numeric comparison (use (( )) for numbers)
(( count > 0 ))
(( total >= minimum ))

# Combined conditions
[[ -f "${file}" && -r "${file}" ]] || die "File not readable: ${file}"
模式 6:简单的参数处理

对于简单脚本,优先使用位置参数:

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

# For 1-3 arguments, just use positional parameters
[[ $# -eq 2 ]] || die "Usage: ${0} <source> <dest>"

source="${1}"
dest="${2}"

[[ -f "${source}" ]] || die "Source not found: ${source}"

对于需要标志的脚本,保持简单:

# Use environment variables instead of complex flag parsing
VERBOSE="${VERBOSE:-false}"
DRY_RUN="${DRY_RUN:-false}"

# Run like: VERBOSE=true DRY_RUN=true ./script.sh input.txt
模式 7:优先使用进程替换,而非临时文件

尽可能避免创建临时文件:

# Instead of:
first_command > /tmp/output.txt
second_command < /tmp/output.txt
rm /tmp/output.txt

# Use process substitution:
second_command <(first_command)

# For multiple inputs:
diff <(sort file1.txt) <(sort file2.txt)
模式 8:优先使用内置命令,而非外部命令

内置命令更快、更可靠:

# Use bash parameter expansion over sed/awk for simple cases
filename="${path##*/}"           # basename
dirname="${path%/*}"             # dirname
extension="${filename##*.}"      # get extension
name="${filename%.*}"            # remove extension

# Use (( )) for arithmetic over expr
count=$(( count + 1 ))           # Not: count=$(expr ${count} + 1)

# Use [[ ]] over [ ] or test
[[ -f "${file}" ]]               # Not: test -f "${file}"

# Use ${#var} for string length
length="${#string}"              # Not: length=$(echo "${string}" | wc -c)

中级模式

模式 9:结构化日志记录

保持日志记录简单且一致:

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ${1}" >&2
}

error() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: ${1}" >&2
}

# Usage
log "Starting process"
error "Failed to connect to database"
模式 10:主函数模式

对于较长的脚本(50 行以上),使用主函数:

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

setup() {
    # Dependency checks, variable initialisation
    command -v jq >/dev/null 2>&1 || die "jq required"
}

process() {
    # Main logic here
    log "Processing data"
}

cleanup() {
    # Cleanup if needed
    log "Cleanup complete"
}

main() {
    setup
    process
    cleanup
}

# Call main with all script arguments
main "${@}"
模式 11:幂等操作

脚本应支持重复运行且安全无虞:

# Check before creating
if [[ ! -d "${target_dir}" ]]; then
    mkdir -p "${target_dir}"
fi

# Check before writing config
if [[ ! -f "${config_file}" ]]; then
    echo "DEFAULT_VALUE=true" > "${config_file}"
fi

# Use atomic operations
mv "${source}" "${dest}"  # Atomic on same filesystem
模式 12:安全地使用 while 循环读取

不要将内容通过管道传给 while(会创建子 Shell):

# Wrong - variables modified in subshell are lost
count=0
cat file.txt | while read -r line; do
    (( count++ ))
done
echo "${count}"  # Will be 0!

# Correct - use process substitution
count=0
while read -r line; do
    (( count++ ))
done < <(cat file.txt)
echo "${count}"  # Correct count

# Or use mapfile for simple cases
mapfile -t lines <file.txt
count="${#lines[@]}"

风格指南

格式
  • 缩进:4 个空格,绝不使用制表符
  • 行长度:最多 120 个字符
  • 长字符串:使用 here-document 或嵌入换行符
# Long command - break at logical points
docker run \
    --name my-container \
    --volume "${PWD}:/data" \
    --env "FOO=bar" \
    my-image:latest

# Long string - use here-doc
cat <<EOF
This is a long message
that spans multiple lines
and is more readable this way.
EOF
命名约定
# Functions: lowercase with underscores
check_dependencies() { ... }
process_files() { ... }

# Local variables: lowercase with underscores
local input_file="${1}"
local line_count=0

# Constants/environment variables: UPPERCASE with underscores
readonly MAX_RETRIES=3
readonly CONFIG_DIR="/etc/myapp"

# Source files: lowercase with underscores
# my_library.sh
文件扩展名
  • 可执行文件:使用 .sh 扩展名或不使用扩展名(面向用户的命令优先不使用扩展名)
  • 库:始终使用 .sh 扩展名,且不可执行

函数文档

为不言自明的函数以外的函数添加文档:

# Good: Simple function, no comment needed (name says it all)
check_file_exists() {
    [[ -f "${1}" ]]
}

# Good: Complex function, documented
# Processes log files and extracts error messages
# Arguments:
#   $1 - Input log file path
#   $2 - Output directory
# Returns:
#   0 on success, 1 on failure
process_logs() {
    local logfile="${1}"
    local output_dir="${2}"
    # Implementation
}

应避免的做法

不要使用这些做法
# Don't use backticks - use $()
output=`command`          # Old style
output=$(command)         # Correct

# Don't use eval - almost always wrong
eval "${user_input}"      # Dangerous!

# Don't use expr - use (( ))
result=$(expr 5 + 3)      # Old style
result=$(( 5 + 3 ))       # Correct

# Don't use [ ] when [[ ]] is available
[ -f "${file}" ]          # POSIX compatible, less features
[[ -f "${file}" ]]        # Bash, safer and more features

# Don't use $[ ] for arithmetic - deprecated
result=$[5 + 3]           # Deprecated
result=$(( 5 + 3 ))       # Correct

# Don't use function keyword unnecessarily
function foo() { ... }    # Redundant
foo() { ... }             # Cleaner
反模式
# Don't glob or split unquoted
rm ${files}               # DANGEROUS
rm "${files}"             # Safe

# Don't use ls output in scripts
for file in $(ls); do     # Breaks with spaces
for file in *; do         # Correct

# Don't pipe yes to commands
yes | risky-command       # Bypasses important prompts

# Don't ignore error codes
make build                # Did it work?
make build || die "Build failed"  # Better

复杂度警示信号

如果脚本符合以下任一情况,请考虑改用 Python/Go 重写:

  • 超过 100 行
  • 使用了简单数组之外的复杂数据结构
  • 嵌套循环遍历数组中的数组
  • 包含大量字符串处理逻辑
  • 状态管理复杂
  • 涉及基本算术之外的数学计算
  • 需要对单个函数进行单元测试
  • JSON/YAML 解析超出简单的 jq 查询范围

进阶:试运行模式

对于会修改内容的脚本:

DRY_RUN="${DRY_RUN:-false}"

run() {
    if [[ "${DRY_RUN}" == "true" ]]; then
        echo "[DRY RUN] ${*}" >&2
        return 0
    fi
    "${@}"
}

# Usage
run cp "${source}" "${dest}"
run rm -f "${old_file}"

# Run script: DRY_RUN=true ./script.sh

快速参考清单

在确认 Bash 脚本完成前,请检查:

  • ShellCheck 检查通过,且没有警告
  • 具有正确的 shebang(#!/usr/bin/env bash)
  • 启用了严格模式(set -euo pipefail)
  • 所有变量都加了引号("${var}")
  • 已检查所需依赖
  • 错误信息正确输出到 stderr
  • 使用临时文件时设置了清理 trap
  • 尽可能保证脚本具备幂等性
  • 不超过 100 行(或有充分理由)
  • 使用 command -v,而不是 which
  • 用数组存放含空格的列表
  • 不使用 eval、解析 ls 输出或反引号
  • 函数使用局部变量

总结

  1. 从简单开始:不要过度设计。大多数脚本应少于 50 行。
  2. 使用 ShellCheck:它能自动发现大多数问题。
  3. 所有内容都加引号:使用 "${var}",而不是 $var。
  4. 尽早失败:使用 set -euo pipefail 并验证输入。
  5. 知道何时停止:如果脚本开始变复杂,就改用真正的编程语言。
  6. 组合而非复杂化:使用管道和进程替换。
  7. 保持幂等:脚本应能安全地多次运行。
  8. 测试错误路径:确保脚本能安全地失败。

请记住:Shell 脚本用于将事物串联起来,而不是构建复杂逻辑。保持简单、安全且专注。

评论

支持 Markdown