关于 bash
bash 是一项指导 AI Agent 编写、编辑、审查和重构 Shell 脚本的技能。它区分简单脚本与复杂脚本,并提供结构、错误处理和测试方面的规范。
主要功能
- 01区分简单与常规脚本
- 02脚本结构模板
- 03参数解析与依赖检查
- 04显式错误处理与退出码
- 05测试与 source 检查
适用场景
- 01编写部署或备份脚本
- 02审查或重构 Bash 代码
- 03编写 CI 或数据转换小脚本
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bentsolheim/ public-skills --skill bash安装后在 Claude Code 中输入/bash确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CLAUDE.mdREADME.mdscripts/scaffold.shtemplates/script-template.shsimple-script-template.sh详细介绍
Bash 最佳实践技能
此技能可确保所有 Bash 脚本遵循企业级最佳实践,提升可维护性、可靠性和易用性。
适用场景
在以下情况中,应自动调用此技能:
- 创建新的 Bash 脚本或 Shell 脚本
- 编辑现有的 .sh 或 .bash 文件
- 用户请求用于自动化、部署、备份或系统任务的脚本
- 审查或重构 Bash 代码
- 用户提及:Bash、Shell 脚本、main 函数、参数解析、脚本结构
脚本复杂度指南
此技能将脚本分为两类,并为其规定不同的结构要求:
简单脚本
以简单直接的方式做好一件事,不添加不必要的形式。
特点:
- 实际逻辑少于约 30 行(不包括注释和空白)
- 不需要命令行参数
- 目的单一、明确
- 流程线性或仅有少量分支
- 输出通常由其他程序或脚本使用
**示例:**版本检测、环境检查、数据格式转换、简单计算
结构要求:
- Main 函数和守卫子句:可选
- Usage 函数:不需要(无需记录参数)
- 参数解析:不适用
- 彩色输出:可选(通常不需要)
- 可以直接执行,无需用函数封装
普通脚本
范围较广,需要完整结构以便维护。
特点:
- 逻辑超过约 30 行,或
- 接受命令行参数,或
- 包含多个函数或复杂分支,或
- 供人交互使用,或
- 需要详细文档
**示例:**部署工具、备份工具、系统维护脚本、开发工具
结构要求:
- Main 函数和守卫子句:必需
- Usage 函数:必需
- 参数解析:必需(如果接受参数)
- 完整的错误处理:必需
- 用户友好的输出:必需(适用于交互式脚本)
应用的核心原则
普通脚本
编写普通脚本时,请确保:
- Main 函数模式和守卫子句——每个脚本都必须有一个 main() 函数
- Usage 函数——提供清晰的帮助文档
- 参数解析——在 main 中以结构化方式处理选项
- 依赖项检查——明确验证所需工具
- 明确的错误处理——绝不使用
set -e,应显式处理错误 - 函数组织——顺序恰当,职责单一
- 用户友好的输出——提供带状态指示的彩色结构化输出
简单脚本
编写简单脚本时,请确保:
- 清晰的用途注释——在脚本顶部说明脚本的功能
- 明确的错误处理——绝不使用
set -e,应显式处理错误 - 正确的退出码——成功时为 0,失败时为非零值
- 将错误输出到 stderr——使用
>&2输出错误消息 - 变量安全——给变量加引号,并处理未定义的情况
脚本结构模板
普通脚本模板
每个新 Bash 脚本都应遵循此结构:
#!/usr/bin/env bash
# Global declarations
DEPENDENCIES=(jq curl git) # List external tools needed
SCRIPT_NAME=$(basename "$0")
VERSION="1.0.0"
# Color definitions (if outputting to terminal)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Allow users to disable colors
if [[ -n "${NO_COLOR:-}" ]] || [[ "${TERM:-}" == "dumb" ]]; then
RED=""
GREEN=""
YELLOW=""
BLUE=""
NC=""
fi
function usage() {
cat <<EOM
Brief description of what this script does.
usage: ${SCRIPT_NAME} [options]
options:
-i|--input <file> Input file to process (required)
-o|--output <file> Output file (optional, defaults to stdout)
-v|--verbose Enable verbose output
-h|--help Show this help message
--version Show version information
dependencies: ${DEPENDENCIES[@]}
examples:
${SCRIPT_NAME} -i data.txt -o report.json
${SCRIPT_NAME} --input data.txt --verbose
EOM
exit 1
}
function main() {
# Default values
local input_file=""
local output_file=""
local verbose=false
# Parse arguments
while [ "$1" != "" ]; do
case $1 in
-i | --input)
shift
input_file="$1"
;;
-o | --output)
shift
output_file="$1"
;;
-v | --verbose)
verbose=true
;;
--version)
echo "${SCRIPT_NAME} version ${VERSION}"
exit 0
;;
-h | --help)
usage
;;
*)
echo "Error: Unknown option '$1'"
usage
;;
esac
shift
done
# Validate required arguments
if [ -z "$input_file" ]; then
echo "Error: Input file is required" >&2
usage
fi
# Check dependencies
exit_on_missing_tools "${DEPENDENCIES[@]}"
# Main script logic
process_file "$input_file" "$output_file" "$verbose"
}
# Business logic functions
function process_file() {
local input="$1"
local output="$2"
local verbose="$3"
# Implementation here
echo "Processing ${input}..."
}
# Utility functions
function exit_on_missing_tools() {
for cmd in "$@"; do
if command -v "$cmd" &>/dev/null; then
continue
fi
printf "Error: Required tool '%s' is not installed or not in PATH\n" "$cmd"
exit 1
done
}
# Guard clause - only execute main if script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
exit 0
fi
简单脚本模板
对于只做一件简单事情且不带参数的脚本:
#!/usr/bin/env bash
# Purpose: Determine version for CI builds based on git state
# Usage: determine-version.sh
# Output: Version string to stdout
# For simple scripts, direct execution is fine - no main() required
# Check if we're on a tag
TAG=$(git describe --exact-match --tags HEAD 2>/dev/null)
if [ -n "$TAG" ]; then
# On a tag - use the tag as version
echo "${TAG#v}"
else
# Not on tag - determine from branch
if [ -n "$GITHUB_HEAD_REF" ]; then
# Pull request
BRANCH="$GITHUB_HEAD_REF"
elif [ -n "$GITHUB_REF_NAME" ]; then
# Push to branch
BRANCH="$GITHUB_REF_NAME"
else
# Fallback to current branch
BRANCH=$(git symbolic-ref -q --short HEAD 2>/dev/null)
if [ -z "$BRANCH" ]; then
echo "Error: Unable to determine branch" >&2
exit 1
fi
fi
# Sanitize branch name for version string
SANITIZED=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/_/g')
echo "${SANITIZED}-SNAPSHOT"
fi
简单脚本的要点:
- 在顶部添加清晰的用途注释
- 直接执行,无需用 main() 封装
- 错误仍应通过 >&2 输出到 stderr
- 使用有意义的退出码(成功时为 0,失败时为非零值)
- 仍要正确地给变量加引号
- 没有参数时,无需 usage() 函数
- 输出为数据时,无需彩色输出
详细最佳实践
1. 脚本引导
Main 函数模式
- 始终使用
main函数作为入口点 - 这样其他脚本就能安全地加载此脚本
- 结构清晰,便于测试
守卫子句
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
exit 0
fi
- 必须位于脚本末尾
- 允许在不执行脚本的情况下将其加载
- 在 main 后始终显式添加
exit 0
2. 用法函数与参数解析
用法函数要求
- 必须在 main() 函数之前定义
- 使用 heredoc 以便清晰排版
- 包含:描述、选项、依赖项、示例
- 以状态码 1 退出
参数解析最佳实践
function main() {
# Set defaults first
local input_file=""
local verbose=false
# Parse with while loop and case
while [ "$1" != "" ]; do
case $1 in
-i | --input)
shift
input_file="$1"
;;
-v | --verbose)
verbose=true
;;
-h | --help)
usage
;;
*)
echo "Error: Unknown option '$1'"
usage
;;
esac
shift
done
# Validate after parsing
if [ -z "$input_file" ]; then
echo "Error: Input file is required" >&2
usage
fi
}
3. 依赖项声明与检查
始终声明依赖项
DEPENDENCIES=(jq curl git docker)
使用前检查
function exit_on_missing_tools() {
for cmd in "$@"; do
if command -v "$cmd" &>/dev/null; then
continue
fi
printf "Error: Required tool '%s' is not installed or not in PATH\n" "$cmd"
exit 1
done
}
在用法说明中包含依赖项
在用法函数中列出依赖项,让用户提前了解所需条件。
4. 函数组织
正确的函数顺序(强制)
usage()- 全局声明之后的第一个函数main()- 紧接 usage 之后- 业务逻辑函数 - 核心功能
- 工具函数 - 通用辅助函数
绝不要添加类似“# 工具函数”的分节注释标题。代码结构本身就能说明问题。
函数设计原则
- 单一职责:每个函数只做一件事
- 输入/输出清晰:使用局部变量,返回有意义的状态
- 名称明确:使用动词-名词组合(create_backup、validate_input)
- 保持精简:将超过 50 行的函数拆分
- 使用局部变量:始终使用
local声明
5. 错误处理
避免使用 set -e
绝不要使用 set -e。应改用显式错误处理:
# Good - Explicit handling
if ! command; then
echo "Error: Command failed" >&2
exit 1
fi
# Good - Chain with ||
cd /some/directory || {
echo "Error: Cannot change directory" >&2
exit 1
}
# Good - Capture and check
output=$(command 2>&1)
if [ $? -ne 0 ]; then
echo "Error: Command failed: $output" >&2
exit 1
fi
错误处理规则
- 始终使用
>&2将错误重定向到 stderr - 说明失败原因及相关上下文
- 使用有意义的退出码
- 失败时清理临时文件
6. 用户友好的输出
彩色输出(适用时)
# Define colors at script top
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Respect NO_COLOR environment
if [[ -n "${NO_COLOR:-}" ]] || [[ "${TERM:-}" == "dumb" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" NC=""
fi
输出函数
function print_header() {
echo -e "${BLUE}=======================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}=======================${NC}"
}
function print_success() {
echo -e "${GREEN}✅ $1${NC}"
}
function print_error() {
echo -e "${RED}❌ Error: $1${NC}" >&2
}
function print_warning() {
echo -e "${YELLOW}⚠️ Warning: $1${NC}"
}
function print_step() {
local current="$1"
local total="$2"
local message="$3"
echo -e "${YELLOW}[${current}/${total}] ${message}${NC}"
}
7. 保持 main 函数精简
main 只应执行以下操作:
- 解析参数
- 验证输入
- 检查依赖项
- 调用业务函数
- 处理高层级错误
业务逻辑应放在独立函数中。
8. 代码注释
避免过多注释
- 命名清晰的函数本身就能说明用途
- 只注释“为什么”,不要注释“做什么”
- 在以下情况下使用注释:
- 复杂算法
- 不明显的变通方案
- 业务逻辑规则
- 外部 API 的特殊行为
# Bad - Obvious comment
# Increment counter
counter=$((counter + 1))
# Good - Explains why
# Exponential backoff with jitter to prevent thundering herd
delay=$((2 ** attempt * 1000 + RANDOM % 1000))
脚本生成检查清单
生成新脚本时,请确认:
- Shebang 行:
#!/usr/bin/env bash - 已声明 DEPENDENCIES 数组
- 已首先定义 usage() 函数
- main() 函数负责处理参数解析
- 使用 exit_on_missing_tools 检查依赖项
- 脚本末尾包含守卫子句
- 使用显式错误处理(不使用
set -e) - 函数顺序正确
- 函数中使用局部变量
- 错误重定向到 stderr
- 使用有意义的退出码
- 支持 NO_COLOR 的彩色输出(如适用)
常见模式
交互式提示
# Yes/no confirmation
read -p "Continue? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Operation cancelled"
exit 0
fi
# Menu selection
echo "Select an option:"
echo "1) Full backup"
echo "2) Incremental backup"
read -p "Enter choice (1-2): " choice
# Password (hidden)
read -s -p "Enter password: " password
echo # Add newline
临时文件
# Create temp file safely
temp_file=$(mktemp)
trap "rm -f '$temp_file'" EXIT
# Use temp directory
temp_dir=$(mktemp -d)
trap "rm -rf '$temp_dir'" EXIT
管道错误处理
# Without set -e, check PIPESTATUS
command1 | command2 | command3
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
echo "Error: command1 failed" >&2
exit 1
fi
可参考的示例
用户要求编写常见任务脚本时,请采用以下模式:
备份脚本
- 在备份名称中包含时间戳
- 备份前确认源存在
- 检查可用磁盘空间
- 提供进度反馈
- 根据保留策略清理旧备份
部署脚本
- 先验证环境
- 清晰展示各步骤的进展
- 失败时回滚
- 记录所有操作
- 最后提供摘要
数据处理脚本
- 验证输入格式
- 高效处理大文件
- 提供进度指示
- 支持失败后恢复执行
- 生成详细报告
如何选择简单脚本与普通脚本
决策树
回答以下问题,确定应使用哪种模式:
-
脚本是否接受命令行参数?
- 是 → 使用普通脚本模式(需要 usage() 和参数解析)
- 否 → 继续回答问题 2
-
逻辑是否超过约 30 行(不包括注释)?
- 是 → 使用普通脚本模式(需要结构)
- 否 → 继续回答问题 3
-
是否包含多个函数或复杂的分支逻辑?
- 是 → 使用普通脚本模式(需要组织结构)
- 否 → 继续回答问题 4
-
是否会由需要帮助的人员交互式运行?
- 是 → 使用普通脚本模式(需要用户友好功能)
- 否 → 继续回答问题 5
-
是否只执行一项简单且不言自明的任务?
- 是 → 使用简单脚本模式
- 否 → 使用普通脚本模式
不确定时,使用普通模式。 有用不上的结构,总比需要结构时却没有好。
简单脚本示例
determine-version.sh- 根据 git 状态输出版本字符串check-env.sh- 验证环境变量是否已设置format-json.sh- 将来自 stdin 的 JSON 重新格式化并输出到 stdoutcount-lines.sh- 统计指定文件类型中的行数is-production.sh- 如果当前位于生产分支,则返回 0
普通脚本示例
deploy.sh- 包含回滚操作的多步骤部署backup.sh- 带保留策略管理的数据库备份setup-dev.sh- 开发环境初始化test-runner.sh- 可配置的测试执行migrate-data.sh- 带验证的数据迁移
测试脚本
始终测试生成的脚本:
- 语法:
bash -n script.sh - 未定义变量:临时使用
set -u运行 - 依赖项:在未安装所需工具的情况下运行
- 错误路径:使用无效输入进行测试
- 加载脚本:验证保护子句是否有效(适用于普通脚本)
请记住
生成或修改的每个脚本都应达到可用于生产环境的标准:
- 便于他人维护
- 通过良好的结构实现自描述
- 能妥善处理错误
- 提供有用的输出
- 遵循一致的模式
这些实践可确保脚本专业、可靠且易于使用。
评论
支持 Markdown