写在前面
Shell 不是某一种固定语法,而是命令解释器这一类程序的统称。sh 可以指历史上的 Bourne Shell、POSIX 定义的可移植语法,或系统上名为 sh 的兼容实现。Bash 是 GNU 项目的 Shell,在 Linux 服务器和 CI 脚本中非常常见。
本文明确使用 Bash;如果脚本必须由 /bin/sh 运行,就不应使用 Bash 数组、[[ ... ]] 等扩展。
系列导航:第一篇:Windows Batch · 第二篇:PowerShell
一、Shebang 与执行方式
1
2
3
| #!/usr/bin/env bash
echo "Hello, Bash!"
|
1
2
| chmod +x script.sh
./script.sh
|
./script.sh 按 Shebang 选择解释器,bash script.sh 则明确交给 Bash。source script.sh 在当前 Shell 中执行,可以改变当前会话的变量和工作目录,不能与启动子进程混为一谈。
二、变量、环境变量与参数
1
2
3
4
5
| name="demo"
port=8080
readonly app_env="production"
export LOG_LEVEL="info"
|
赋值等号两边不能有空格。普通变量只存在于当前 Shell,export 后才会传给子进程。
1
2
3
4
| echo "script: $0"
echo "first: ${1:-default}"
echo "count: $#"
printf 'arg: <%s>\n' "$@"
|
"$@" 会保留每个参数的边界,是转发参数的标准写法。不加引号的 $@ 会再经历分词和文件名展开。
三、引号与 Shell 展开
1
2
3
4
5
| name="Bash"
echo '$name' # 原样字符
echo "Hello, $name" # 变量展开
echo "kernel: $(uname -s)" # 命令替换
echo "$((2 + 3))" # 算术展开
|
Bash 的展开包括大括号展开、波浪号展开、参数展开、命令替换、算术展开、单词分割和文件名展开等。这也是 Shell 很多陷阱的根源。
1
2
3
| file="report final.md"
rm -- "$file" # 正确:一个参数
# rm $file # 错误:可能被拆成两个参数
|
一条实用规则是:变量展开默认加双引号,只在明确需要分词或 glob 时才取消。
四、参数展开
1
2
3
4
5
6
7
| name="${1:-guest}" # 空或未定义时用默认值
: "${API_KEY:?API_KEY is required}"
path="/var/log/app.log"
echo "${path##*/}" # app.log
echo "${path%/*}" # /var/log
echo "${#path}" # 长度
|
${var:-default} 不会修改变量,${var:=default} 会赋值。${var:?message} 适合在脚本开始处校验必需配置。
五、条件与 case
1
2
3
4
5
6
7
| if [[ -f "$file" ]]; then
echo "regular file"
elif [[ -d "$file" ]]; then
echo "directory"
else
echo "missing"
fi
|
[[ ... ]] 是 Bash 条件语法,比传统 [ ... ] 更适合 Bash 脚本;需要 POSIX sh 兼容时使用 [ ... ]。
1
2
3
4
5
| case "${1:-}" in
start|restart) echo "starting" ;;
stop) echo "stopping" ;;
*) echo "usage: $0 {start|stop|restart}" >&2; exit 2 ;;
esac
|
六、循环、数组与读取文件
1
2
3
4
5
6
7
8
| for file in ./*.md; do
[[ -e "$file" ]] || continue
printf '%s\n' "$file"
done
for ((i = 0; i < 5; i++)); do
echo "$i"
done
|
1
2
3
4
| services=(api worker scheduler)
for service in "${services[@]}"; do
echo "$service"
done
|
逐行读取时避免 for line in $(cat file):
1
2
3
| while IFS= read -r line; do
printf 'line: %s\n' "$line"
done < names.txt
|
IFS= 避免剪掉首尾空白,-r 避免把反斜杠解释为转义。
七、函数与返回状态
1
2
3
4
5
6
7
8
| log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
}
ensure_directory() {
local dir=$1
[[ -d "$dir" ]] || mkdir -p -- "$dir"
}
|
Shell 函数的 return 返回 0–255 状态码,不是任意数据。数据通常通过标准输出产生,调用者再用命令替换接收:
1
2
3
4
5
| get_version() {
printf '%s\n' '1.0.0'
}
version=$(get_version)
|
日志如果不属于函数的数据输出,应写入标准错误:printf 'message\n' >&2。
八、管道、重定向与状态码
1
2
3
| command >stdout.log 2>stderr.log
command >all.log 2>&1
printf '%s\n' a b c | sort | uniq
|
Unix 命令的约定是 0 表示成功,非 0 表示失败。
1
2
3
4
5
| if curl -fsS https://example.com/health; then
echo "healthy"
else
echo "unhealthy" >&2
fi
|
1
2
| build && deploy
cleanup || echo "cleanup failed" >&2
|
九、set -euo pipefail 不是魔法
-u:读取未定义变量时报错。-o pipefail:管道中任一命令失败时,管道可返回非零状态。-e:在诸多非零状态场景下退出,但存在重要例外。-E:让 ERR trap 在函数、命令替换和子 Shell 中更容易继承。
set -e 在 if 条件、while 条件、&& / || 列表、被 ! 取反的命令等上下文中不按“遇到非零立即退出”的简化规则工作。因此关键命令仍应显式地放在 if 中或检查其状态。
十、完整实战:构建并打包 Hugo 站点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #!/usr/bin/env bash
set -Eeuo pipefail
readonly script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly output_dir="${script_dir}/public"
log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
}
on_error() {
local exit_code=$?
local line_no=$1
printf '[ERROR] line %s, exit code %s\n' "$line_no" "$exit_code" >&2
exit "$exit_code"
}
trap 'on_error "$LINENO"' ERR
command -v hugo >/dev/null 2>&1 || {
echo '[ERROR] hugo was not found in PATH' >&2
exit 10
}
cd -- "$script_dir"
log 'Building site'
hugo --minify --buildFuture --cleanDestinationDir -d "$output_dir"
log 'Creating archive'
tar -C "$output_dir" -czf "${script_dir}/site.tar.gz" .
log 'Done'
|
${BASH_SOURCE[0]} 指向当前 Bash 源文件,比 $0 更适合函数和被 source 的场景。cd ... && pwd 将脚本目录规范化为绝对路径。
十一、常见陷阱
- 不加引号的变量可能触发分词和 glob。
- 不要用
for x in $(cat file) 逐行读取文件。 - 不要用
ls 的输出驱动文件处理,直接使用 glob 或 find 的结构化能力。 - 不要把
set -e 当成完整错误处理。 - 临时文件使用
mktemp,并通过 trap 清理。 - 对任意文件名处理时,优先使用 NUL 分隔,例如
find ... -print0。
十二、三种脚本怎么选
| 维度 | Windows Batch | PowerShell | Bash |
|---|
| 主要环境 | Windows | Windows、Linux、macOS | Linux / Unix-like |
| 管道数据 | 文本 | .NET 对象 | 字节流 / 文本 |
| 优势 | Windows 零依赖、兼容历史环境 | 结构化数据、系统管理、跨平台 | Linux 工具链、CI/CD、远程运维 |
| 短板 | 语法易错、不擅长结构化数据 | 启动和语法相对更重 | 引号与展开规则复杂 |
| 典型场景 | 启动器、简单安装脚本 | Windows 管理、API / JSON 自动化 | 服务器部署、命令编排 |
Batch 解决 Windows 零依赖启动,PowerShell 擅长对象和系统管理,Bash 则是 Linux 命令编排的粘合层。选择工具的关键不是“谁的语法更现代”,而是目标环境、数据形态和脚本复杂度。
系列回顾:第一篇:Windows Batch · 第二篇:PowerShell
参考资料