bash-master by josiahsiegel/claude-plugin-marketplace
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill bash-master强制要求:在 Windows 上始终对文件路径使用反斜杠
在 Windows 上使用编辑或写入工具时,您必须在文件路径中使用反斜杠(\),而不是正斜杠(/)。
示例:
D:/repos/project/file.tsxD:\repos\project\file.tsx这适用于:
除非用户明确要求,否则切勿创建新的文档文件。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
适用于所有平台编写专业、可移植且可维护的 bash 脚本的综合指南。
每个 Bash 脚本的基本清单:
#!/usr/bin/env bash
set -euo pipefail # 出错时退出,未定义变量时退出,管道失败时退出
IFS=$'\n\t' # 安全的单词分割
# 使用:部署前运行 shellcheck your_script.sh
# 生产前在目标平台测试
平台兼容性快速检查:
# Linux/macOS: ✓ 完整的 bash 功能
# Git Bash (Windows): ✓ 大多数功能,✗ 某些系统调用
# 容器: ✓ 取决于基础镜像
# POSIX 模式: 使用 /bin/sh 并避免 bash 特有语法
此技能为任何脚本任务提供专业的 bash/shell 脚本知识,确保在所有平台上达到专业级质量。
必须使用此技能的情况:
此技能提供的内容:
set -euo pipefail、trap 处理程序、退出代码此技能在以下情况自动激活:
始终以安全设置开始脚本:
#!/usr/bin/env bash
# 快速失败并大声报告
set -e # 任何错误时退出
set -u # 未定义变量时退出
set -o pipefail # 管道失败时退出
set -E # ERR 陷阱由函数继承
# 可选:
# set -x # 调试模式(执行前打印命令)
# set -C # 防止重定向覆盖文件
# 安全的单词分割
IFS=$'\n\t'
# 脚本元数据
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
为什么这很重要:
set -e:防止级联故障set -u:捕获变量名中的拼写错误set -o pipefail:捕获管道中间部分的故障IFS=$'\n\t':防止在空格上进行单词分割(安全问题)知道何时使用哪个:
# POSIX 兼容(跨 shell 可移植)
#!/bin/sh
# 使用:[ ] 测试,无数组,无 [[ ]],无 <(进程替换)
# Bash 特有(现代特性,语法更清晰)
#!/usr/bin/env bash
# 使用:[[ ]],数组,关联数组,<(),进程替换
决策矩阵:
#!/bin/sh 且仅使用 POSIX#!/usr/bin/env bash#!/usr/bin/env bash# 始终引用变量以防止单词分割和通配符展开
bad_cmd=$file_path # ✗ 错误 - 单词分割
good_cmd="$file_path" # ✓ 正确
# 数组:引用展开
files=("file 1.txt" "file 2.txt")
process "${files[@]}" # ✓ 正确 - 每个元素都被引用
process "${files[*]}" # ✗ 错误 - 所有元素作为一个字符串
# 命令替换:引用结果
result="$(command)" # ✓ 正确
result=$(command) # ✗ 错误(除非您想要单词分割)
# 例外:当您想要单词分割时
# shellcheck disable=SC2086
flags="-v -x -z"
command $flags # 故意的单词分割
部署前始终运行 ShellCheck:
# 安装
# Ubuntu/Debian: apt-get install shellcheck
# macOS: brew install shellcheck
# Windows: scoop install shellcheck
# 用法
shellcheck your_script.sh
shellcheck -x your_script.sh # 跟踪源语句
# 在 CI/CD 中
find . -name "*.sh" -exec shellcheck {} +
ShellCheck 捕获:
必备知识: Git Bash/MINGW 自动将 Unix 风格的路径转换为 Windows 路径。这是 Windows 上跨平台脚本错误的最常见来源。
完整指南: 请参阅 references/windows-git-bash-paths.md 获取完整文档。
快速参考:
# 自动转换发生在以下情况:
/foo → C:/Program Files/Git/usr/foo
--dir=/tmp → --dir=C:/msys64/tmp
# 需要时禁用转换
MSYS_NO_PATHCONV=1 command /path/that/should/not/convert
# 使用 cygpath 手动转换
unix_path=$(cygpath -u "C:\Windows\System32") # Windows 转 Unix
win_path=$(cygpath -w "/c/Users/username") # Unix 转 Windows
# Shell 检测(最快方法)
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "mingw"* ]]; then
echo "检测到 Git Bash"
# 使用路径转换
fi
# 或者检查 $MSYSTEM 变量(Git Bash/MSYS2 特有)
case "${MSYSTEM:-}" in
MINGW64|MINGW32|MSYS)
echo "MSYS2/Git Bash 环境: $MSYSTEM"
;;
esac
常见问题:
# 问题:标志被转换为路径
command /e /s # /e 变为 C:/Program Files/Git/e
# 解决方案:使用双斜杠或破折号
command //e //s # 或者:command -e -s
# 问题:路径中的空格
cd C:\Program Files\Git # 失败
# 解决方案:引用路径
cd "C:\Program Files\Git" # 或者:cd /c/Program\ Files/Git
大多数 bash 脚本的主要目标:
# 可用的 Linux 特有功能
/proc 文件系统
systemd 集成
Linux 特有命令(apt、yum、systemctl)
# 检查是否为 Linux
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux 特有代码
fi
基于 BSD 的实用程序(与 GNU 不同):
# macOS 差异
sed -i '' # macOS 需要空字符串
sed -i # Linux 不需要
# 使用 ggrep、gsed 等获取 GNU 版本
if command -v gsed &> /dev/null; then
SED=gsed
else
SED=sed
fi
# 检查是否为 macOS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS 特有代码
fi
Git Bash 限制:
# Git Bash 中可用:
- 大多数核心实用程序
- 文件操作
- 进程管理(有限)
# 不可用:
- systemd
- 某些信号(SIGHUP 行为不同)
- /proc 文件系统
- 原生 Windows 路径处理问题
# 路径处理
# Git Bash 使用 Unix 路径:/c/Users/...
# 需要时转换:
winpath=$(cygpath -w "$unixpath") # Unix → Windows
unixpath=$(cygpath -u "$winpath") # Windows → Unix
# 检查是否为 Git Bash
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
# Git Bash / Cygwin 代码
fi
WSL (Windows 的 Linux 子系统):
# WSL 本质上是 Linux,但:
# - 可以在 /mnt/c/ 访问 Windows 文件系统
# - 某些系统调用行为不同
# - 网络配置不同
# 检查是否为 WSL
if grep -qi microsoft /proc/version 2>/dev/null; then
# WSL 特有代码
fi
容器感知脚本:
# 最小基础镜像可能没有 bash
# 使用 #!/bin/sh 或显式安装 bash
# 容器检测
if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then
# 在 Docker 中运行
fi
# Kubernetes 检测
if [ -n "$KUBERNETES_SERVICE_HOST" ]; then
# 在 Kubernetes 中运行
fi
# 最佳实践:
# - 最小化依赖
# - 使用绝对路径或 PATH
# - 不要假设用户/组存在
# - 正确处理信号(PID 1 问题)
#!/usr/bin/env bash
set -euo pipefail
# 检测平台
detect_platform() {
case "$OSTYPE" in
linux-gnu*) echo "linux" ;;
darwin*) echo "macos" ;;
msys*|cygwin*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
PLATFORM=$(detect_platform)
# 平台特定路径
case "$PLATFORM" in
linux)
SED=sed
;;
macos)
SED=$(command -v gsed || echo sed)
;;
windows)
# Git Bash 特定
;;
esac
# 良好的函数结构
function_name() {
# 1. 首先定义局部变量
local arg1="$1"
local arg2="${2:-default_value}"
local result=""
# 2. 输入验证
if [[ -z "$arg1" ]]; then
echo "错误:arg1 是必需的" >&2
return 1
fi
# 3. 主要逻辑
result=$(some_operation "$arg1" "$arg2")
# 4. 输出/返回
echo "$result"
return 0
}
# 使用函数,而不是脚本中的脚本
# 好处:可测试性、可重用性、命名空间
# 常量:UPPER_CASE
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/app/config.conf"
# 全局变量:UPPER_CASE 或 lower_case(保持一致)
GLOBAL_STATE="initialized"
# 局部变量:lower_case
local user_name="john"
local file_count=0
# 环境变量:UPPER_CASE(按约定)
export DATABASE_URL="postgres://..."
# 尽可能使用只读
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 方法 1:显式检查退出代码
if ! command_that_might_fail; then
echo "错误:命令失败" >&2
return 1
fi
# 方法 2:使用 || 进行替代操作
command_that_might_fail || {
echo "错误:命令失败" >&2
return 1
}
# 方法 3:使用 trap 进行清理
cleanup() {
local exit_code=$?
# 清理操作
rm -f "$TEMP_FILE"
exit "$exit_code"
}
trap cleanup EXIT
# 方法 4:自定义错误处理程序
error_exit() {
local message="$1"
local code="${2:-1}"
echo "错误:$message" >&2
exit "$code"
}
# 用法
[[ -f "$config_file" ]] || error_exit "未找到配置文件:$config_file"
validate_input() {
local input="$1"
# 检查是否为空
if [[ -z "$input" ]]; then
echo "错误:输入不能为空" >&2
return 1
fi
# 检查格式(示例:仅字母数字)
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "错误:输入包含无效字符" >&2
return 1
fi
# 检查长度
if [[ ${#input} -gt 255 ]]; then
echo "错误:输入过长(最多 255 个字符)" >&2
return 1
fi
return 0
}
# 使用前验证
read -r user_input
if validate_input "$user_input"; then
process "$user_input"
fi
# 简单的参数解析
usage() {
cat <<EOF
用法:$SCRIPT_NAME [选项] <命令>
选项:
-h, --help 显示此帮助信息
-v, --verbose 详细输出
-f, --file 文件 输入文件
-o, --output 目录 输出目录
命令:
build 构建项目
test 运行测试
EOF
}
main() {
local verbose=false
local input_file=""
local output_dir="."
local command=""
# 解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
verbose=true
shift
;;
-f|--file)
input_file="$2"
shift 2
;;
-o|--output)
output_dir="$2"
shift 2
;;
-*)
echo "错误:未知选项:$1" >&2
usage >&2
exit 1
;;
*)
command="$1"
shift
break
;;
esac
done
# 验证必需参数
if [[ -z "$command" ]]; then
echo "错误:命令是必需的" >&2
usage >&2
exit 1
fi
# 执行命令
case "$command" in
build) do_build ;;
test) do_test ;;
*)
echo "错误:未知命令:$command" >&2
usage >&2
exit 1
;;
esac
}
main "$@"
# 日志级别
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
# 当前日志级别
LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO}
log_debug() { [[ $LOG_LEVEL -le $LOG_LEVEL_DEBUG ]] && echo "[DEBUG] $*" >&2; }
log_info() { [[ $LOG_LEVEL -le $LOG_LEVEL_INFO ]] && echo "[INFO] $*" >&2; }
log_warn() { [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]] && echo "[WARN] $*" >&2; }
log_error() { [[ $LOG_LEVEL -le $LOG_LEVEL_ERROR ]] && echo "[ERROR] $*" >&2; }
# 带时间戳
log_with_timestamp() {
local level="$1"
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}
# 用法
log_info "开始进程"
log_error "无法连接到数据库"
# 切勿将 eval 与用户输入一起使用
# ✗ 错误 - 危险
eval "$user_input"
# 切勿使用来自用户输入的动态变量名
# ✗ 错误 - 危险
eval "var_$user_input=value"
# 切勿将用户输入连接到命令中
# ✗ 错误 - 危险
grep "$user_pattern" file.txt # 如果模式包含 -e 标志,可能注入
# ✓ 正确 - 使用数组
grep_args=("$user_pattern" "file.txt")
grep "${grep_args[@]}"
# ✓ 正确 - 使用 -- 将选项与参数分开
grep -- "$user_pattern" file.txt
# 清理文件路径
sanitize_path() {
local path="$1"
# 移除 .. 组件
path="${path//..\/}"
path="${path//\/..\//}"
# 移除前导 /
path="${path#/}"
echo "$path"
}
# 验证路径在允许的目录内
is_safe_path() {
local file_path="$1"
local base_dir="$2"
# 解析为绝对路径
local real_path
real_path=$(readlink -f "$file_path" 2>/dev/null) || return 1
local real_base
real_base=$(readlink -f "$base_dir" 2>/dev/null) || return 1
# 检查路径是否以基础目录开头
[[ "$real_path" == "$real_base"/* ]]
}
# 用法
if is_safe_path "$user_file" "/var/app/data"; then
process_file "$user_file"
else
echo "错误:无效文件路径" >&2
exit 1
fi
# 检查是否以 root 身份运行
if [[ $EUID -eq 0 ]]; then
echo "错误:不要以 root 身份运行此脚本" >&2
exit 1
fi
# 如果需要,降低权限
drop_privileges() {
local user="$1"
if [[ $EUID -eq 0 ]]; then
exec sudo -u "$user" "$0" "$@"
fi
}
# 以提升的权限运行特定命令
run_as_root() {
if [[ $EUID -ne 0 ]]; then
sudo "$@"
else
"$@"
fi
}
# 创建安全的临时文件
readonly TEMP_DIR=$(mktemp -d)
readonly TEMP_FILE=$(mktemp)
# 退出时清理
cleanup() {
rm -rf "$TEMP_DIR"
rm -f "$TEMP_FILE"
}
trap cleanup EXIT
# 安全的临时文件(仅所有者可读)
secure_temp=$(mktemp)
chmod 600 "$secure_temp"
# ✗ 慢 - 每次迭代创建子 shell
while IFS= read -r line; do
count=$(echo "$count + 1" | bc)
done < file.txt
# ✓ 快 - 在 bash 中进行算术运算
count=0
while IFS= read -r line; do
((count++))
done < file.txt
# ✗ 慢 - 外部命令
dirname=$(dirname "$path")
basename=$(basename "$path")
# ✓ 快 - 参数展开
dirname="${path%/*}"
basename="${path##*/}"
# ✗ 慢 - 用于简单检查的 grep
if echo "$string" | grep -q "pattern"; then
# ✓ 快 - Bash 正则表达式
if [[ "$string" =~ pattern ]]; then
# ✗ 慢 - 用于简单提取的 awk
field=$(echo "$line" | awk '{print $3}')
# ✓ 快 - 读入数组
read -ra fields <<< "$line"
field="${fields[2]}"
# 当您需要读取多个命令的输出时
# ✓ 好 - 进程替换
while IFS= read -r line1 <&3 && IFS= read -r line2 <&4; do
echo "$line1 - $line2"
done 3< <(command1) 4< <(command2)
# 并行处理
command1 &
command2 &
wait # 等待所有后台作业
# ✓ 快 - 原生数组操作
files=(*.txt)
echo "找到 ${#files[@]} 个文件"
# ✗ 慢 - 解析 ls 输出
count=$(ls -1 *.txt | wc -l)
# ✓ 快 - 数组过滤
filtered=()
for item in "${array[@]}"; do
[[ "$item" =~ ^[0-9]+$ ]] && filtered+=("$item")
done
# ✓ 快 - 数组连接
IFS=,
joined="${array[*]}"
IFS=$'\n\t'
# 安装 BATS
# git clone https://github.com/bats-core/bats-core.git
# cd bats-core && ./install.sh /usr/local
# test/script.bats
#!/usr/bin/env bats
# 加载要测试的脚本
load '../script.sh'
@test "函数返回正确的值" {
result=$(my_function "input")
[ "$result" = "expected" ]
}
@test "函数处理空输入" {
run my_function ""
[ "$status" -eq 1 ]
[ "${lines[0]}" = "错误:输入不能为空" ]
}
@test "函数验证输入格式" {
run my_function "invalid@input"
[ "$status" -eq 1 ]
}
# 运行测试
# bats test/script.bats
# integration_test.sh
#!/usr/bin/env bash
set -euo pipefail
# 设置
setup() {
export TEST_DIR=$(mktemp -d)
export TEST_FILE="$TEST_DIR/test.txt"
}
# 清理
teardown() {
rm -rf "$TEST_DIR"
}
# 测试用例
test_file_creation() {
./script.sh create "$TEST_FILE"
if [[ ! -f "$TEST_FILE" ]]; then
echo "失败:文件未创建"
return 1
fi
echo "通过:文件创建有效"
return 0
}
# 运行测试
main() {
setup
trap teardown EXIT
test_file_creation || exit 1
echo "所有测试通过"
}
main
# .github/workflows/test.yml
name: 测试
on: [push, pull_request]
jobs:
测试:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 安装 shellcheck
run: sudo apt-get install -y shellcheck
- name: 运行 shellcheck
run: find . -name "*.sh" -exec shellcheck {} +
- name: 安装 bats
run: |
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local
- name: 运行测试
run: bats test/
# 方法 1:set -x(打印命令)
set -x
command1
command2
set +x # 关闭
# 方法 2:PS4 以获得更好的输出
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
# 方法 3:条件调试
DEBUG=${DEBUG:-false}
debug() {
if [[ "$DEBUG" == "true" ]]; then
echo "[DEBUG] $*" >&2
fi
}
# 用法:DEBUG=true ./script.sh
# 跟踪函数调用
trace() {
echo "[TRACE] 函数:${FUNCNAME[1]},参数:$*" >&2
}
my_function() {
trace "$@"
# 函数逻辑
}
# 执行时间分析
profile() {
local start=$(date +%s%N)
"$@"
local end=$(date +%s%N)
local duration=$(( (end - start) / 1000000 ))
echo "[PROFILE] 命令 '$*' 耗时 ${duration}ms" >&2
}
# 用法
profile slow_command arg1 arg2
# 问题:脚本在 bash 中有效但在 sh 中无效
# 解决方案:检查 bash 特有语法
checkbashisms script.sh
# 问题:本地有效但在服务器上无效
# 解决方案:检查 PATH 和环境
env
echo "$PATH"
# 问题:文件名中的空格破坏脚本
# 解决方案:始终引用变量
for file in *.txt; do
process "$file" # 不是:process $file
done
# 问题:脚本在 cron 中行为不同
# 解决方案:显式设置 PATH
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
# 简单的 key=value 配置
load_config() {
local config_file="$1"
if [[ ! -f "$config_file" ]]; then
echo "错误:未找到配置文件:$config_file" >&2
return 1
fi
# 源配置(如果不信任则危险)
# shellcheck source=/dev/null
source "$config_file"
}
# 安全的配置解析(无代码执行)
read_config() {
local config_file="$1"
while IFS='=' read -r key value; do
# 跳过注释和空行
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
# 修剪空白
key=$(echo "$key" | tr -d ' ')
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# 导出变量
declare -g "$key=$value"
done < "$config_file"
}
# 简单的后台作业
process_files_parallel() {
local max_jobs=4
local job_count=0
for file in *.txt; do
# 启动后台作业
process_file "$file" &
# 限制并发作业
((job_count++))
if [[ $job_count -ge $max_jobs ]]; then
wait -n # 等待任何作业完成
((job_count--))
fi
done
# 等待剩余作业
wait
}
# GNU Parallel(如果可用)
parallel_with_gnu() {
parallel -j 4 process_file ::: *.txt
}
# 优雅关闭
shutdown_requested=false
handle_sigterm() {
echo "收到 SIGTERM,正在优雅关闭..." >&2
shutdown_requested=true
}
trap handle_sigterm SIGTERM SIGINT
main_loop() {
while [[ "$shutdown_requested" == "false" ]]; do
# 执行工作
sleep 1
done
echo "关闭完成" >&2
}
main_loop
retry_with_backoff() {
local max_attempts=5
local timeout=1
local attempt=1
local exitCode=0
while [[ $attempt -le $max_attempts ]]; do
if "$@"; then
return 0
else
exitCode=$?
fi
echo "尝试 $attempt 失败!$timeout 秒后重试..." >&2
sleep "$timeout"
attempt=$((attempt + 1))
timeout=$((timeout * 2))
done
echo "命令在 $max_attempts 次尝试后失败!" >&2
return "$exitCode"
}
# 用法
retry_with_backoff curl -f https://api.example.com/health
Bash 参考手册
POSIX Shell 命令语言
Google Shell 风格指南
防御性 Bash 编程
ShellCheck
BATS (Bash 自动化测试系统)
shfmt
Bash 学院
Bash 初学者指南
高级 Bash 脚本编程指南
Bash 陷阱
explainshell.com
GNU Coreutils 手册
FreeBSD 手册页
Git for Windows
WSL 文档
Stack Overflow - Bash 标签
Unix & Linux Stack Exchange
Reddit - r/bash
Bash 速查表
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/).
Examples:
D:/repos/project/file.tsxD:\repos\project\file.tsxThis applies to:
NEVER create new documentation files unless explicitly requested by the user.
Comprehensive guide for writing professional, portable, and maintainable bash scripts across all platforms.
Essential Checklist for Every Bash Script:
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
IFS=$'\n\t' # Safe word splitting
# Use: shellcheck your_script.sh before deployment
# Test on target platform(s) before production
Platform Compatibility Quick Check:
# Linux/macOS: ✓ Full bash features
# Git Bash (Windows): ✓ Most features, ✗ Some system calls
# Containers: ✓ Depends on base image
# POSIX mode: Use /bin/sh and avoid bashisms
This skill provides expert bash/shell scripting knowledge for ANY scripting task, ensuring professional-grade quality across all platforms.
MUST use this skill for:
What this skill provides:
set -euo pipefail, trap handlers, exit codesThis skill activates automatically for:
ALWAYS start scripts with safety settings:
#!/usr/bin/env bash
# Fail fast and loud
set -e # Exit on any error
set -u # Exit on undefined variable
set -o pipefail # Exit on pipe failure
set -E # ERR trap inherited by functions
# Optionally:
# set -x # Debug mode (print commands before execution)
# set -C # Prevent file overwrites with redirection
# Safe word splitting
IFS=$'\n\t'
# Script metadata
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
Why this matters:
set -e: Prevents cascading failuresset -u: Catches typos in variable namesset -o pipefail: Catches failures in the middle of pipesIFS=$'\n\t': Prevents word splitting on spaces (security issue)Know when to use which:
# POSIX-compliant (portable across shells)
#!/bin/sh
# Use: [ ] tests, no arrays, no [[ ]], no <(process substitution)
# Bash-specific (modern features, clearer syntax)
#!/usr/bin/env bash
# Use: [[ ]], arrays, associative arrays, <(), process substitution
Decision matrix:
#!/bin/sh and POSIX only#!/usr/bin/env bash#!/usr/bin/env bash# ALWAYS quote variables to prevent word splitting and globbing
bad_cmd=$file_path # ✗ WRONG - word splitting
good_cmd="$file_path" # ✓ CORRECT
# Arrays: Quote expansion
files=("file 1.txt" "file 2.txt")
process "${files[@]}" # ✓ CORRECT - each element quoted
process "${files[*]}" # ✗ WRONG - all elements as one string
# Command substitution: Quote the result
result="$(command)" # ✓ CORRECT
result=$(command) # ✗ WRONG (unless you want word splitting)
# Exception: When you WANT word splitting
# shellcheck disable=SC2086
flags="-v -x -z"
command $flags # Intentional word splitting
ALWAYS run ShellCheck before deployment:
# Install
# Ubuntu/Debian: apt-get install shellcheck
# macOS: brew install shellcheck
# Windows: scoop install shellcheck
# Usage
shellcheck your_script.sh
shellcheck -x your_script.sh # Follow source statements
# In CI/CD
find . -name "*.sh" -exec shellcheck {} +
ShellCheck catches:
ESSENTIAL KNOWLEDGE: Git Bash/MINGW automatically converts Unix-style paths to Windows paths. This is the most common source of cross-platform scripting errors on Windows.
Complete Guide: See references/windows-git-bash-paths.md for comprehensive documentation.
Quick Reference:
# Automatic conversion happens for:
/foo → C:/Program Files/Git/usr/foo
--dir=/tmp → --dir=C:/msys64/tmp
# Disable conversion when needed
MSYS_NO_PATHCONV=1 command /path/that/should/not/convert
# Manual conversion with cygpath
unix_path=$(cygpath -u "C:\Windows\System32") # Windows to Unix
win_path=$(cygpath -w "/c/Users/username") # Unix to Windows
# Shell detection (fastest method)
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "mingw"* ]]; then
echo "Git Bash detected"
# Use path conversion
fi
# Or check $MSYSTEM variable (Git Bash/MSYS2 specific)
case "${MSYSTEM:-}" in
MINGW64|MINGW32|MSYS)
echo "MSYS2/Git Bash environment: $MSYSTEM"
;;
esac
Common Issues:
# Problem: Flags converted to paths
command /e /s # /e becomes C:/Program Files/Git/e
# Solution: Use double slashes or dashes
command //e //s # OR: command -e -s
# Problem: Spaces in paths
cd C:\Program Files\Git # Fails
# Solution: Quote paths
cd "C:\Program Files\Git" # OR: cd /c/Program\ Files/Git
Primary target for most bash scripts:
# Linux-specific features available
/proc filesystem
systemd integration
Linux-specific commands (apt, yum, systemctl)
# Check for Linux
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux-specific code
fi
BSD-based utilities (different from GNU):
# macOS differences
sed -i '' # macOS requires empty string
sed -i # Linux doesn't need it
# Use ggrep, gsed, etc. for GNU versions
if command -v gsed &> /dev/null; then
SED=gsed
else
SED=sed
fi
# Check for macOS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS-specific code
fi
Git Bash limitations:
# Available in Git Bash:
- Most core utils
- File operations
- Process management (limited)
# NOT available:
- systemd
- Some signals (SIGHUP behavior differs)
- /proc filesystem
- Native Windows path handling issues
# Path handling
# Git Bash uses Unix paths: /c/Users/...
# Convert if needed:
winpath=$(cygpath -w "$unixpath") # Unix → Windows
unixpath=$(cygpath -u "$winpath") # Windows → Unix
# Check for Git Bash
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
# Git Bash / Cygwin code
fi
WSL (Windows Subsystem for Linux):
# WSL is essentially Linux, but:
# - Can access Windows filesystem at /mnt/c/
# - Some syscalls behave differently
# - Network configuration differs
# Check for WSL
if grep -qi microsoft /proc/version 2>/dev/null; then
# WSL-specific code
fi
Container-aware scripting:
# Minimal base images may not have bash
# Use #!/bin/sh or install bash explicitly
# Container detection
if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then
# Running in Docker
fi
# Kubernetes detection
if [ -n "$KUBERNETES_SERVICE_HOST" ]; then
# Running in Kubernetes
fi
# Best practices:
# - Minimize dependencies
# - Use absolute paths or PATH
# - Don't assume user/group existence
# - Handle signals properly (PID 1 issues)
#!/usr/bin/env bash
set -euo pipefail
# Detect platform
detect_platform() {
case "$OSTYPE" in
linux-gnu*) echo "linux" ;;
darwin*) echo "macos" ;;
msys*|cygwin*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
PLATFORM=$(detect_platform)
# Platform-specific paths
case "$PLATFORM" in
linux)
SED=sed
;;
macos)
SED=$(command -v gsed || echo sed)
;;
windows)
# Git Bash specifics
;;
esac
# Good function structure
function_name() {
# 1. Local variables first
local arg1="$1"
local arg2="${2:-default_value}"
local result=""
# 2. Input validation
if [[ -z "$arg1" ]]; then
echo "Error: arg1 is required" >&2
return 1
fi
# 3. Main logic
result=$(some_operation "$arg1" "$arg2")
# 4. Output/return
echo "$result"
return 0
}
# Use functions, not scripts-in-scripts
# Benefits: testability, reusability, namespacing
# Constants: UPPER_CASE
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/app/config.conf"
# Global variables: UPPER_CASE or lower_case (be consistent)
GLOBAL_STATE="initialized"
# Local variables: lower_case
local user_name="john"
local file_count=0
# Environment variables: UPPER_CASE (by convention)
export DATABASE_URL="postgres://..."
# Readonly when possible
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Method 1: Check exit codes explicitly
if ! command_that_might_fail; then
echo "Error: Command failed" >&2
return 1
fi
# Method 2: Use || for alternative actions
command_that_might_fail || {
echo "Error: Command failed" >&2
return 1
}
# Method 3: Trap for cleanup
cleanup() {
local exit_code=$?
# Cleanup operations
rm -f "$TEMP_FILE"
exit "$exit_code"
}
trap cleanup EXIT
# Method 4: Custom error handler
error_exit() {
local message="$1"
local code="${2:-1}"
echo "Error: $message" >&2
exit "$code"
}
# Usage
[[ -f "$config_file" ]] || error_exit "Config file not found: $config_file"
validate_input() {
local input="$1"
# Check if empty
if [[ -z "$input" ]]; then
echo "Error: Input cannot be empty" >&2
return 1
fi
# Check format (example: alphanumeric only)
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: Input contains invalid characters" >&2
return 1
fi
# Check length
if [[ ${#input} -gt 255 ]]; then
echo "Error: Input too long (max 255 characters)" >&2
return 1
fi
return 0
}
# Validate before use
read -r user_input
if validate_input "$user_input"; then
process "$user_input"
fi
# Simple argument parsing
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <command>
Options:
-h, --help Show this help
-v, --verbose Verbose output
-f, --file FILE Input file
-o, --output DIR Output directory
Commands:
build Build the project
test Run tests
EOF
}
main() {
local verbose=false
local input_file=""
local output_dir="."
local command=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
verbose=true
shift
;;
-f|--file)
input_file="$2"
shift 2
;;
-o|--output)
output_dir="$2"
shift 2
;;
-*)
echo "Error: Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
command="$1"
shift
break
;;
esac
done
# Validate required arguments
if [[ -z "$command" ]]; then
echo "Error: Command is required" >&2
usage >&2
exit 1
fi
# Execute command
case "$command" in
build) do_build ;;
test) do_test ;;
*)
echo "Error: Unknown command: $command" >&2
usage >&2
exit 1
;;
esac
}
main "$@"
# Logging levels
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
# Current log level
LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO}
log_debug() { [[ $LOG_LEVEL -le $LOG_LEVEL_DEBUG ]] && echo "[DEBUG] $*" >&2; }
log_info() { [[ $LOG_LEVEL -le $LOG_LEVEL_INFO ]] && echo "[INFO] $*" >&2; }
log_warn() { [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]] && echo "[WARN] $*" >&2; }
log_error() { [[ $LOG_LEVEL -le $LOG_LEVEL_ERROR ]] && echo "[ERROR] $*" >&2; }
# With timestamps
log_with_timestamp() {
local level="$1"
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}
# Usage
log_info "Starting process"
log_error "Failed to connect to database"
# NEVER use eval with user input
# ✗ WRONG - DANGEROUS
eval "$user_input"
# NEVER use dynamic variable names from user input
# ✗ WRONG - DANGEROUS
eval "var_$user_input=value"
# NEVER concatenate user input into commands
# ✗ WRONG - DANGEROUS
grep "$user_pattern" file.txt # If pattern contains -e flag, injection possible
# ✓ CORRECT - Use arrays
grep_args=("$user_pattern" "file.txt")
grep "${grep_args[@]}"
# ✓ CORRECT - Use -- to separate options from arguments
grep -- "$user_pattern" file.txt
# Sanitize file paths
sanitize_path() {
local path="$1"
# Remove .. components
path="${path//..\/}"
path="${path//\/..\//}"
# Remove leading /
path="${path#/}"
echo "$path"
}
# Validate path is within allowed directory
is_safe_path() {
local file_path="$1"
local base_dir="$2"
# Resolve to absolute path
local real_path
real_path=$(readlink -f "$file_path" 2>/dev/null) || return 1
local real_base
real_base=$(readlink -f "$base_dir" 2>/dev/null) || return 1
# Check if path starts with base directory
[[ "$real_path" == "$real_base"/* ]]
}
# Usage
if is_safe_path "$user_file" "/var/app/data"; then
process_file "$user_file"
else
echo "Error: Invalid file path" >&2
exit 1
fi
# Check if running as root
if [[ $EUID -eq 0 ]]; then
echo "Error: Do not run this script as root" >&2
exit 1
fi
# Drop privileges if needed
drop_privileges() {
local user="$1"
if [[ $EUID -eq 0 ]]; then
exec sudo -u "$user" "$0" "$@"
fi
}
# Run specific command with elevated privileges
run_as_root() {
if [[ $EUID -ne 0 ]]; then
sudo "$@"
else
"$@"
fi
}
# Create secure temporary files
readonly TEMP_DIR=$(mktemp -d)
readonly TEMP_FILE=$(mktemp)
# Cleanup on exit
cleanup() {
rm -rf "$TEMP_DIR"
rm -f "$TEMP_FILE"
}
trap cleanup EXIT
# Secure temporary file (only readable by owner)
secure_temp=$(mktemp)
chmod 600 "$secure_temp"
# ✗ SLOW - Creates subshell for each iteration
while IFS= read -r line; do
count=$(echo "$count + 1" | bc)
done < file.txt
# ✓ FAST - Arithmetic in bash
count=0
while IFS= read -r line; do
((count++))
done < file.txt
# ✗ SLOW - External commands
dirname=$(dirname "$path")
basename=$(basename "$path")
# ✓ FAST - Parameter expansion
dirname="${path%/*}"
basename="${path##*/}"
# ✗ SLOW - grep for simple checks
if echo "$string" | grep -q "pattern"; then
# ✓ FAST - Bash regex
if [[ "$string" =~ pattern ]]; then
# ✗ SLOW - awk for simple extraction
field=$(echo "$line" | awk '{print $3}')
# ✓ FAST - Read into array
read -ra fields <<< "$line"
field="${fields[2]}"
# When you need to read multiple commands' output
# ✓ GOOD - Process substitution
while IFS= read -r line1 <&3 && IFS= read -r line2 <&4; do
echo "$line1 - $line2"
done 3< <(command1) 4< <(command2)
# Parallel processing
command1 &
command2 &
wait # Wait for all background jobs
# ✓ FAST - Native array operations
files=(*.txt)
echo "Found ${#files[@]} files"
# ✗ SLOW - Parsing ls output
count=$(ls -1 *.txt | wc -l)
# ✓ FAST - Array filtering
filtered=()
for item in "${array[@]}"; do
[[ "$item" =~ ^[0-9]+$ ]] && filtered+=("$item")
done
# ✓ FAST - Array joining
IFS=,
joined="${array[*]}"
IFS=$'\n\t'
# Install BATS
# git clone https://github.com/bats-core/bats-core.git
# cd bats-core && ./install.sh /usr/local
# test/script.bats
#!/usr/bin/env bats
# Load script to test
load '../script.sh'
@test "function returns correct value" {
result=$(my_function "input")
[ "$result" = "expected" ]
}
@test "function handles empty input" {
run my_function ""
[ "$status" -eq 1 ]
[ "${lines[0]}" = "Error: Input cannot be empty" ]
}
@test "function validates input format" {
run my_function "invalid@input"
[ "$status" -eq 1 ]
}
# Run tests
# bats test/script.bats
# integration_test.sh
#!/usr/bin/env bash
set -euo pipefail
# Setup
setup() {
export TEST_DIR=$(mktemp -d)
export TEST_FILE="$TEST_DIR/test.txt"
}
# Teardown
teardown() {
rm -rf "$TEST_DIR"
}
# Test case
test_file_creation() {
./script.sh create "$TEST_FILE"
if [[ ! -f "$TEST_FILE" ]]; then
echo "FAIL: File was not created"
return 1
fi
echo "PASS: File creation works"
return 0
}
# Run tests
main() {
setup
trap teardown EXIT
test_file_creation || exit 1
echo "All tests passed"
}
main
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install shellcheck
run: sudo apt-get install -y shellcheck
- name: Run shellcheck
run: find . -name "*.sh" -exec shellcheck {} +
- name: Install bats
run: |
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local
- name: Run tests
run: bats test/
# Method 1: set -x (print commands)
set -x
command1
command2
set +x # Turn off
# Method 2: PS4 for better output
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
# Method 3: Conditional debugging
DEBUG=${DEBUG:-false}
debug() {
if [[ "$DEBUG" == "true" ]]; then
echo "[DEBUG] $*" >&2
fi
}
# Usage: DEBUG=true ./script.sh
# Trace function calls
trace() {
echo "[TRACE] Function: ${FUNCNAME[1]}, Args: $*" >&2
}
my_function() {
trace "$@"
# Function logic
}
# Execution time profiling
profile() {
local start=$(date +%s%N)
"$@"
local end=$(date +%s%N)
local duration=$(( (end - start) / 1000000 ))
echo "[PROFILE] Command '$*' took ${duration}ms" >&2
}
# Usage
profile slow_command arg1 arg2
# Issue: Script works in bash but not in sh
# Solution: Check for bashisms
checkbashisms script.sh
# Issue: Works locally but not on server
# Solution: Check PATH and environment
env
echo "$PATH"
# Issue: Whitespace in filenames breaking script
# Solution: Always quote variables
for file in *.txt; do
process "$file" # Not: process $file
done
# Issue: Script behaves differently in cron
# Solution: Set PATH explicitly
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
# Simple key=value config
load_config() {
local config_file="$1"
if [[ ! -f "$config_file" ]]; then
echo "Error: Config file not found: $config_file" >&2
return 1
fi
# Source config (dangerous if not trusted)
# shellcheck source=/dev/null
source "$config_file"
}
# Safe config parsing (no code execution)
read_config() {
local config_file="$1"
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
# Trim whitespace
key=$(echo "$key" | tr -d ' ')
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Export variable
declare -g "$key=$value"
done < "$config_file"
}
# Simple background jobs
process_files_parallel() {
local max_jobs=4
local job_count=0
for file in *.txt; do
# Start background job
process_file "$file" &
# Limit concurrent jobs
((job_count++))
if [[ $job_count -ge $max_jobs ]]; then
wait -n # Wait for any job to finish
((job_count--))
fi
done
# Wait for remaining jobs
wait
}
# GNU Parallel (if available)
parallel_with_gnu() {
parallel -j 4 process_file ::: *.txt
}
# Graceful shutdown
shutdown_requested=false
handle_sigterm() {
echo "Received SIGTERM, shutting down gracefully..." >&2
shutdown_requested=true
}
trap handle_sigterm SIGTERM SIGINT
main_loop() {
while [[ "$shutdown_requested" == "false" ]]; do
# Do work
sleep 1
done
echo "Shutdown complete" >&2
}
main_loop
retry_with_backoff() {
local max_attempts=5
local timeout=1
local attempt=1
local exitCode=0
while [[ $attempt -le $max_attempts ]]; do
if "$@"; then
return 0
else
exitCode=$?
fi
echo "Attempt $attempt failed! Retrying in $timeout seconds..." >&2
sleep "$timeout"
attempt=$((attempt + 1))
timeout=$((timeout * 2))
done
echo "Command failed after $max_attempts attempts!" >&2
return "$exitCode"
}
# Usage
retry_with_backoff curl -f https://api.example.com/health
Bash Reference Manual
POSIX Shell Command Language
Google Shell Style Guide
Defensive Bash Programming
ShellCheck
BATS (Bash Automated Testing System)
shfmt
Bash Academy
Bash Guide for Beginners
Advanced Bash-Scripting Guide
Bash Pitfalls
explainshell.com
GNU Coreutils Manual
FreeBSD Manual Pages
Git for Windows
WSL Documentation
Stack Overflow - Bash Tag
Unix & Linux Stack Exchange
Reddit - r/bash
Bash Cheat Sheet
ShellCheck Wiki
For deeper coverage of specific topics, see the reference files:
Always activate for:
Key indicators:
A bash script using this skill should:
Quality checklist:
# Run before deployment
shellcheck script.sh # No errors or warnings
bash -n script.sh # Syntax check
bats test/script.bats # Unit tests pass
./script.sh --help # Usage text displays
DEBUG=true ./script.sh # Debug mode works
checkbashisms script.shcommand -v tool_namesed --version (GNU) vs sed (BSD)shellcheck -W SC2086# shellcheck disable=SC2086 reason: intentional word splitting./script.sh >> /tmp/cron.log 2>&1time commandset -xThis skill provides comprehensive bash scripting knowledge. Combined with the reference files, you have access to industry-standard practices and platform-specific guidance for any bash scripting task.
Weekly Installs
83
Repository
GitHub Stars
21
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code65
gemini-cli64
opencode64
codex62
cursor59
github-copilot56
Azure 升级评估与自动化工具 - 轻松迁移 Functions 计划、托管层级和 SKU
104,900 周安装