name: shell-scripting description: Shell脚本编写的最佳实践和模式。适用于编写bash/zsh脚本、自动化任务、创建CLI工具或调试shell命令。 author: Joseph OBrien status: unpublished updated: ‘2025-12-23’ version: 1.0.1 tag: skill type: skill
Shell脚本编写
全面的shell脚本编写技能,涵盖bash/zsh模式、自动化、错误处理和CLI工具开发。
何时使用此技能
- 编写自动化脚本
- 创建CLI工具
- 系统管理任务
- 构建和部署脚本
- 日志处理和分析
- 文件操作和批量操作
- 定时任务和计划任务
脚本结构
模板
#!/usr/bin/env bash
# 脚本: name.sh
# 描述: 此脚本的功能
# 用法: ./name.sh [选项] <参数>
set -euo pipefail # 错误时退出,未定义变量,管道失败
IFS=$'
\t' # 更安全的单词分割
# 常量
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# 默认值
VERBOSE=false
DRY_RUN=false
# 函数
usage() {
cat <<EOF
用法: $SCRIPT_NAME [选项] <参数>
选项:
-h, --help 显示此帮助信息
-v, --verbose 启用详细输出
-n, --dry-run 显示将要执行的操作
EOF
}
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
}
error() {
log "错误: $*"
exit 1
}
# 主逻辑
main() {
# 解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
*)
break
;;
esac
done
# 你的逻辑在这里
}
main "$@"
错误处理
设置选项
set -e # 任何错误时退出
set -u # 未定义变量时报错
set -o pipefail # 管道失败即脚本失败
set -x # 调试:打印每个命令(谨慎使用)
清理陷阱
cleanup() {
rm -f "$TEMP_FILE"
log "清理完成"
}
trap cleanup EXIT
# 同时处理特定信号
trap 'error "脚本被中断"' INT TERM
错误检查模式
# 检查命令是否存在
command -v jq >/dev/null 2>&1 || error "需要jq但未安装"
# 检查文件是否存在
[[ -f "$FILE" ]] || error "文件未找到: $FILE"
# 检查目录是否存在
[[ -d "$DIR" ]] || mkdir -p "$DIR"
# 检查变量是否设置
[[ -n "${VAR:-}" ]] || error "VAR未设置"
# 显式检查退出状态
if ! some_command; then
error "some_command失败"
fi
变量与替换
变量扩展
# 默认值
${VAR:-default} # 如果VAR未设置或为空则使用默认值
${VAR:=default} # 如果VAR未设置或为空则设置为默认值
${VAR:+value} # 如果VAR已设置则使用value
${VAR:?error msg} # 如果VAR未设置或为空则报错
# 字符串操作
${VAR#pattern} # 移除最短前缀匹配
${VAR##pattern} # 移除最长前缀匹配
${VAR%pattern} # 移除最短后缀匹配
${VAR%%pattern} # 移除最长后缀匹配
${VAR/old/new} # 替换第一个匹配项
${VAR//old/new} # 替换所有匹配项
${#VAR} # VAR的长度
数组
# 声明数组
declare -a ARRAY=("一" "二" "三")
# 访问元素
echo "${ARRAY[0]}" # 第一个元素
echo "${ARRAY[@]}" # 所有元素
echo "${#ARRAY[@]}" # 元素数量
echo "${!ARRAY[@]}" # 所有索引
# 遍历
for item in "${ARRAY[@]}"; do
echo "$item"
done
# 追加
ARRAY+=("四")
关联数组
declare -A MAP
MAP["键1"]="值1"
MAP["键2"]="值2"
# 访问
echo "${MAP[键1]}"
# 检查键是否存在
[[ -v MAP[键1] ]] && echo "键1存在"
# 遍历
for key in "${!MAP[@]}"; do
echo "$key: ${MAP[$key]}"
done
控制流
条件语句
# 字符串比较
[[ "$str" == "value" ]]
[[ "$str" != "value" ]]
[[ -z "$str" ]] # 为空
[[ -n "$str" ]] # 不为空
# 数字比较
[[ "$num" -eq 5 ]] # 等于
[[ "$num" -ne 5 ]] # 不等于
[[ "$num" -lt 5 ]] # 小于
[[ "$num" -gt 5 ]] # 大于
# 文件测试
[[ -f "$file" ]] # 文件存在
[[ -d "$dir" ]] # 目录存在
[[ -r "$file" ]] # 可读
[[ -w "$file" ]] # 可写
[[ -x "$file" ]] # 可执行
# 逻辑运算符
[[ "$a" && "$b" ]] # 与
[[ "$a" || "$b" ]] # 或
[[ ! "$a" ]] # 非
循环
# For循环
for i in {1..10}; do
echo "$i"
done
# While循环
while read -r line; do
echo "$line"
done < "$file"
# 进程替换
while read -r line; do
echo "$line"
done < <(command)
# C风格for循环
for ((i=0; i<10; i++)); do
echo "$i"
done
输入/输出
读取输入
# 从用户读取
read -r -p "输入名称: " name
# 读取密码(隐藏)
read -r -s -p "密码: " password
# 带超时读取
read -r -t 5 -p "快! " answer
# 逐行读取文件
while IFS= read -r line; do
echo "$line"
done < "$file"
输出与重定向
# 重定向标准输出
command > file # 覆盖
command >> file # 追加
# 重定向标准错误
command 2> file
# 重定向两者
command &> file
command > file 2>&1
# 丢弃输出
command > /dev/null 2>&1
# Tee(输出并保存)
command | tee file
文本处理
常见模式
# 查找并处理文件
find . -name "*.log" -exec grep "ERROR" {} +
# 处理CSV
while IFS=, read -r col1 col2 col3; do
echo "$col1: $col2"
done < file.csv
# JSON处理(使用jq)
jq '.key' file.json
jq -r '.items[]' file.json
# AWK单行命令
awk '{print $1}' file # 第一列
awk -F: '{print $1}' /etc/passwd # 自定义分隔符
awk 'NR > 1' file # 跳过表头
# SED单行命令
sed 's/old/new/g' file # 全部替换
sed -i 's/old/new/g' file # 原地编辑
sed -n '10,20p' file # 打印第10-20行
最佳实践
应该做
- 引用所有变量扩展:
"$VAR" - 使用
[[ ]]而非[ ]进行测试 - 使用
$(command)而非反引号 - 检查返回值
- 对常量使用
readonly - 在函数中使用
local - 提供
--help选项 - 使用有意义的退出码
不应该做
- 解析
ls输出 - 对不受信任的输入使用
eval - 假设路径中没有空格
- 忽略shellcheck警告
- 编写一个巨型脚本(模块化)
参考文件
references/one_liners.md- 有用的单行命令
与其他技能的集成
- developer-experience - 用于工具自动化
- debugging - 用于脚本调试
- testing - 用于脚本测试模式