Bash 可移植性
编写可移植的 POSIX 兼容脚本指南,并了解何时利用 Bash 特有功能。
Shebang 选择
使用 #!/usr/bin/env bash 编写 Bash 脚本
#!/usr/bin/env bash
原因: 在 PATH 中搜索 bash,适用于 bash 可能位于不同位置的系统。
使用 #!/bin/sh 编写 POSIX 脚本
#!/bin/sh
原因: 当不需要 bash 功能时,提供最大可移植性。在许多系统上,/bin/sh 是 dash 或其他 POSIX shell。
需要时使用直接路径
#!/bin/bash
仅在以下情况使用: 系统要求保证 bash 位置,或安全策略要求绝对路径。
POSIX 与 Bash 特性矩阵
| 特性 | POSIX | Bash | 建议 |
|---|---|---|---|
[[ ]] | 否 | 是 | 在 POSIX 下使用 [ ] |
(( )) | 否 | 是 | 在 POSIX 下使用 [ ] 配合 -eq 等 |
| 数组 | 否 | 是 | 使用位置参数或文件 |
local | 部分 | 是 | 通常安全 |
${var,,} | 否 | 4+ | 在 POSIX 下使用 tr |
<<< | 否 | 是 | 使用 echo | cmd |
=~ 正则 | 否 | 是 | 使用 grep 或 expr |
source | 否 | 是 | 使用 .(点)命令 |
function f() | 否 | 是 | 仅使用 f() |
$'...' | 否 | 是 | 使用 printf |
{1..10} | 否 | 是 | 使用 seq 或 while 循环 |
POSIX 兼容模式
条件判断
# POSIX - 使用 [ ] 并正确引用
if [ -f "$file" ]; then
echo "文件存在"
fi
# 字符串比较
if [ "$var" = "value" ]; then
echo "匹配"
fi
# 数字比较
if [ "$num" -gt 10 ]; then
echo "更大"
fi
# 复合条件
if [ -f "$file" ] && [ -r "$file" ]; then
echo "可读文件"
fi
大小写转换 (POSIX)
# 转换为小写
lower=$(echo "$string" | tr '[:upper:]' '[:lower:]')
# 转换为大写
upper=$(echo "$string" | tr '[:lower:]' '[:upper:]')
子串操作 (POSIX)
# 获取子串 - 使用 expr 或 cut
substr=$(expr "$string" : '.\(.{5}\)') # 字符 4-8
substr=$(echo "$string" | cut -c4-8)
# 字符串长度
length=$(expr length "$string")
length=${#string} # 这实际上是 POSIX 的
读取文件 (POSIX)
# 逐行读取
while IFS= read -r line; do
echo "$line"
done < "$file"
# 读取整个文件(不使用 cat)
content=$(cat "$file") # cat 是 POSIX 的
命令替换
# 现代语法(即使在 POSIX 中也推荐)
result=$(command)
# 旧语法(避免使用)
result=`command`
# 嵌套(现代语法更清晰)
result=$(echo $(date)) # 清晰
result=`echo \`date\`` # 转义噩梦
值得使用的 Bash 特有功能
当不需要可移植性时,这些 bash 功能可提高代码质量:
扩展测试 [[ ]]
# 模式匹配
[[ "$file" == *.txt ]]
# 正则匹配
[[ "$input" =~ ^[0-9]+$ ]]
# 无需担心单词拆分
[[ -f $file ]] # 引号可选(但仍建议加上)
# 逻辑运算符内部使用
[[ -f "$file" && -r "$file" ]]
数组
# 索引数组
declare -a files=()
files+=("one.txt")
files+=("two.txt")
for f in "${files[@]}"; do
process "$f"
done
# 关联数组(Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
参数扩展
# 默认值
"${var:-default}"
# 大小写转换(Bash 4+)
"${var,,}" # 小写
"${var^^}" # 大写
# 子串
"${var:0:10}" # 前 10 个字符
"${var: -5}" # 后 5 个字符
# 搜索替换
"${var//old/new}"
Here Strings
# Bash
read -r var <<< "input string"
# POSIX 等效
var=$(echo "input string")
进程替换
# Bash - 比较两个命令输出
diff <(sort file1) <(sort file2)
# POSIX 等效(使用临时文件)
sort file1 > /tmp/sorted1
sort file2 > /tmp/sorted2
diff /tmp/sorted1 /tmp/sorted2
检测 Shell 类型
# 检查是否在 bash 中运行
if [ -n "${BASH_VERSION:-}" ]; then
echo "正在 Bash 中运行"
fi
# 检查 bash 版本以支持特性
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
echo "Bash 4+ 可用"
fi
# 通用 shell 检测
case "${SHELL##*/}" in
bash) echo "bash" ;;
zsh) echo "zsh" ;;
*) echo "other" ;;
esac
可移植工具函数
可移植性决策指南
使用 POSIX 的情况:
- 脚本在最小系统(容器、嵌入式)上运行
- 目标包括 dash、ash 或 busybox sh
- 需要最大兼容性
- 脚本是系统初始化的一部分
使用 Bash 的情况:
- 目标系统保证有 bash
- 需要使用数组、关联数组或正则表达式
- 需要复杂的字符串操作
- 代码清晰度显著提高
- 需要交互式功能
常见可移植性陷阱
echo vs printf
# 有问题 - 行为各异
echo -n "不换行"
echo -e "含有\t制表符"
# 可移植的
printf '%s' "不换行"
printf '含有\t制表符\n'
变量赋值
# 到处可用
var="value"
# 某些 shell 可能失败
var = "value" # = 两侧有空格是错误的
导出与赋值结合
# POSIX - 分步命令
var="value"
export var
# Bash/现代写法 - 合并(大多数地方可用)
export var="value"
无需数组的类似数组操作
# 使用位置参数
set -- "item1" "item2" "item3"
for item in "$@"; do
echo "$item"
done
# 或者基于 IFS 的分割
items="item1:item2:item3"
IFS=':' read -r item1 item2 item3 <<EOF
$items
EOF