名称:shell-portability 用户可调用:false 描述:适用于编写需要在不同系统、Shell或环境中运行的Shell脚本。涵盖POSIX兼容性和平台差异。 允许的工具:
- 读取
- 写入
- 编辑
- Bash
- Grep
- Glob
Shell脚本可移植性
编写在不同平台和环境间工作的Shell脚本的技术。
Shebang选择
Bash脚本
#!/usr/bin/env bash
# 对于Bash脚本最可移植
# 在Linux、macOS、BSD上工作
POSIX Shell脚本
#!/bin/sh
# 为了最大可移植性
# 仅使用POSIX特性
Bash与POSIX差异
数组(仅Bash)
# Bash - 数组可用
declare -a items=("一" "二" "三")
for item in "${items[@]}"; do
echo "$item"
done
# POSIX - 使用位置参数或空格分隔的字符串
set -- 一 二 三
for item in "$@"; do
echo "$item"
done
测试语法
# Bash - 扩展测试
if [[ "$var" == "值" ]]; then
echo "匹配"
fi
# POSIX - 基本测试
if [ "$var" = "值" ]; then
echo "匹配"
fi
字符串操作
# Bash - 正则表达式匹配
if [[ "$input" =~ ^[0-9]+$ ]]; then
echo "数字"
fi
# POSIX - 使用case或外部工具
case "$input" in
*[!0-9]*|'') echo "非数字" ;;
*) echo "数字" ;;
esac
算术
# Bash - 算术扩展
(( count++ ))
if (( count > 10 )); then
echo "大于"
fi
# POSIX - expr或算术扩展
count=$((count + 1))
if [ "$count" -gt 10 ]; then
echo "大于"
fi
平台差异
macOS与Linux
# 日期命令差异
# GNU(Linux)
date -d "昨天" +%Y-%m-%d
# BSD(macOS)
date -v-1d +%Y-%m-%d
# 可移植方法
if date --version >/dev/null 2>&1; then
# GNU日期
yesterday=$(date -d "昨天" +%Y-%m-%d)
else
# BSD日期
yesterday=$(date -v-1d +%Y-%m-%d)
fi
sed差异
# GNU sed - 原地编辑
sed -i 's/旧/新/g' file.txt
# BSD sed - 需要备份扩展名
sed -i '' 's/旧/新/g' file.txt
# 可移植方法
sed 's/旧/新/g' file.txt > file.txt.tmp && mv file.txt.tmp file.txt
# 或使用函数
sed_inplace() {
if sed --version >/dev/null 2>&1; then
sed -i "$@"
else
sed -i '' "$@"
fi
}
readlink差异
# GNU readlink
readlink -f /path/to/link
# BSD/macOS - 默认无-f选项
# 使用coreutils的greadlink或:
resolve_path() {
local path="$1"
if command -v greadlink >/dev/null 2>&1; then
greadlink -f "$path"
elif command -v realpath >/dev/null 2>&1; then
realpath "$path"
else
# 回退
cd "$(dirname "$path")" && pwd -P
fi
}
检测环境
操作系统
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
MINGW*|CYGWIN*|MSYS*) echo "windows" ;;
FreeBSD*) echo "freebsd" ;;
*) echo "未知" ;;
esac
}
OS=$(detect_os)
case "$OS" in
linux) INSTALL_CMD="apt-get install" ;;
macos) INSTALL_CMD="brew install" ;;
esac
架构
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l) echo "arm" ;;
*) echo "未知" ;;
esac
}
Shell检测
detect_shell() {
if [ -n "$BASH_VERSION" ]; then
echo "bash"
elif [ -n "$ZSH_VERSION" ]; then
echo "zsh"
else
echo "sh"
fi
}
可移植模式
读取文件
# 可移植行读取
while IFS= read -r line || [ -n "$line" ]; do
echo "$line"
done < "$file"
# || [ -n "$line" ] 处理无尾随换行符的文件
临时文件
# POSIX兼容的临时文件
make_temp() {
if command -v mktemp >/dev/null 2>&1; then
mktemp
else
# 回退
local tmp="/tmp/tmp.$$.$RANDOM"
touch "$tmp" && echo "$tmp"
fi
}
命令存在性检查
# POSIX兼容的命令检查
has_command() {
command -v "$1" >/dev/null 2>&1
}
# 使用
if has_command curl; then
curl "$url"
elif has_command wget; then
wget -O- "$url"
else
echo "无HTTP客户端可用" >&2
exit 1
fi
字符串包含
# POSIX兼容的字符串包含
contains() {
case "$1" in
*"$2"*) return 0 ;;
*) return 1 ;;
esac
}
# 使用
if contains "$PATH" "/usr/local/bin"; then
echo "在PATH中找到"
fi
ShellCheck兼容性
禁用警告以实现可移植性
# 当有意使用非可移植特性时
# shellcheck disable=SC2039 # Bash特定特性
if [[ "$var" =~ regex ]]; then
:
fi
# 文档说明原因
# shellcheck disable=SC2016 # 有意不扩展
echo '使用$HOME表示主目录'
测试多个Shell
#!/usr/bin/env bash
# shellcheck shell=bash
# 或对于POSIX:
#!/bin/sh
# shellcheck shell=sh
最佳实践
- 根据需要选择合适的shebang
- 在README中记录Shell要求
- 对于Bash脚本,使用
#!/usr/bin/env bash - 尽可能在多个平台上测试
- 当可移植性重要时,优先使用POSIX特性
- 将平台差异抽象为函数
- 使用ShellCheck并指定适当的shell指令
- 为平台特定命令提供回退