Shell脚本学习指南-第十四章-Shell可移植性议题与扩展
第14章 Shell 可移植性议题与扩展
背景 POSIX Shell是标准基准;bash、ksh93 在POSIX之上增加大量扩展语法。二者功能大量重叠,但不完全兼容。 写可移植脚本要遵循POSIX;写便捷脚本可以使用bash/ksh93扩展,但要注意跨shell坑点。
14.1 迷思(常见坑点 gotchas)
这些是实际写脚本高频踩坑,很多是bash/ksh行为不一致。
1.存储shell状态
POSIX没有标准方法保存函数定义;bash/ksh93可以用下面代码导出完整shell状态(选项、变量、函数、trap)到文件。
⚠注意:bash、ksh函数语法不一样,导出的状态文件不能直接跨shell恢复。
2. echo不可移植
不同系统内置echo对\n \t、-n行为差异巨大。
可移植方案:优先用printf,不要依赖echo转义与选项。
3. getopts的OPTIND
ksh93支持函数内部把OPTIND做本地局部变量,函数内部调用getopts不会破坏外部OPTIND;bash不支持该特性。
4. ${var:?message} 变量检查
语法:变量未设置则打印提示信息并退出shell。
行为差异:
| 执行方式 | bash | ksh93 |
|---|---|---|
bash x.sh |
打印信息,脚本退出,后面不执行 | 打印信息,脚本退出 |
. x.sh(source) |
打印信息,继续执行后续代码 | 打印信息,直接退出shell |
风险:source执行脚本时,ksh会直接把交互式终端退出。如果使用该语法,出错后最好手动写
exit。
5. for循环空列表
当$a $b $c全部为空,展开得到空列表。
- 新版bash/ksh:合法,循环一次都不执行
- 古老原始Bourne Shell:报语法错误。
6. DEBUG trap行为不一致
- ksh88:DEBUG trap 在每条命令执行之后触发
- ksh93 /现代bash:DEBUG trap 在每条命令执行之前触发
⚠可移植脚本禁止使用DEBUG trap。
7.set选项总表
set -xxx,set -o 长名字两种写法等价。
| 短选项 | 长选项 | POSIX | 说明 |
|---|---|---|---|
| -a | allexport | ✅ | 后续定义变量自动export导出 |
| -b | notify | ✅ | 后台任务完成立刻打印提示(交互用) |
| -C | noclobber | ✅ | 禁止>覆盖已存在文件;`> |
| -e | errexit | ✅ | 命令非0退出则shell直接退出 |
| -f | noglob | ✅ | 关闭通配符* ? []展开 |
| -h | hashall | ✅ | 函数内命令提前缓存路径 |
| -m | monitor | ✅ | 开启作业控制,交互shell默认打开 |
| -n | noexec | ✅ | 只检查语法,不执行脚本 |
| -u | nounset | ✅ | 使用未定义变量直接报错退出 |
| -v | verbose | ✅ | 原样打印输入行 |
| -x | xtrace | ✅ | 打印展开之后的命令,调试用 |
bash独有:
-B braceexpand、-H histexpand;ksh独有:-A数组赋值、bgnice后台自动降nice优先级。
14.2 bash 的shopt命令
shopt仅bash可用,ksh不支持,专门控制bash独有行为。 语法:
高频实用选项:
extglob:开启扩展模式匹配(重要!14.3.3会讲)nullglob:通配符匹配不到文件时,展开为空字符串,保留原样dotglob:文件名通配匹配包含.开头隐藏文件cdspell:交互式,cd自动修正目录拼写错误histappend:历史命令追加到文件而不是覆盖checkwinsize:命令执行后自动更新窗口行列变量LINES、COLUMNS
示例:
|
|
14.3 bash与ksh93共同扩展(两者都支持,非POSIX标准)
14.3.1 select 交互式选择菜单
作用:快速生成数字选择菜单,bash/ksh特有。
语法
内置特殊变量:
PS3:select的提示符,默认#?REPLY:保存用户输入的数字
示例:
运行输出:
进阶:显示友好中文菜单,用REPLY拿到数字做数组下标
数组定义:
| 语法 | 作用 |
|---|---|
arr=(a b c) |
定义 / 赋值数组,圆括号,一次性装入多个元素 |
arr[2]="hello" |
给下标 2 位置赋值,方括号指定下标 |
${arr[2]} |
读取下标 2 的元素,方括号访问元素 |
${arr[@]} |
取出数组全部元素(推荐双引号 "${arr[@]}") |
补充变量
TMOUT:设置秒数,select超时无输入自动退出循环。
14.3.2 扩展测试 [[ 条件 ]]
[[ ]]是shell关键字,不是外部命令test,bash/ksh支持,POSIX sh不支持。
- 内部不会发生单词分割、通配符展开,字符串一般不需要加双引号
- 支持
&& ||逻辑运算符直接写括号内;支持><字符串比较 ==支持shell通配符模式匹配
常用运算符(和[ test ]大部分一致)
-e file文件存在;-d目录;-f普通文件;-r可读-w可写-x可执行-z str字符串空;-n str字符串非空fileA -nt fileBA比B新;fileA -ot fileBA比B旧;-ef指向同一个硬链接
⚠可移植脚本用
[ ];写bash/ksh脚本优先[[ ]]。
14.3.3 扩展模式匹配 extglob
ksh默认开启;bash必须执行
shopt -s extglob才生效。 类似正则表达式,shell文件名匹配,对比egrep正则:
| shell模式 | 等价egrep | 含义 |
|---|---|---|
*(exp) |
exp* |
0次或多次匹配exp |
+(exp) |
exp+ |
至少1次exp |
?(exp) |
exp? |
0或1次exp |
@(a| b |c) |
a | b | c |
匹配a或者b或者c |
!(exp) |
无 | 匹配除exp以外所有内容 |
示例
14.3.4 大括号展开 brace expansion
生成批量字符串,csh继承过来。
14.3.5 进程替换 Process substitution
语法:
<(命令)、>(命令)原理:生成临时命名管道,把命令输出伪装成文件,传给别的程序。Linux/ksh/bash支持。
输入替换 <(cmd):把cmd输出当成一个“文件”给程序读
输出替换 >(cmd):把数据重定向输入到cmd
14.3.6 索引数组(一维数组)
bash/ksh支持一维数组;下标从0开始;稀疏数组(中间下标可以跳过不赋值)
三种赋值方式
读取数组
示例,select菜单搭配数组:
⚠坑:
$arr等价${arr[0]},只会拿第0个元素。
14.3.7 其他零散扩展(bash、ksh共同)
- 波浪号扩展增强
(( 算术 ))算术命令(非POSIX)
$(( ))是算术展开(输出值);(( ))是shell语句,有退出状态,不需要$。1 <= 3这个比较,算术逻辑结果是 1(代表真),(( ))返回退出状态 04 <= 3这个比较,算术逻辑结果是 0(代表假),,(( ))返回退出状态 1while、if完全只看命令的「退出状态 (返回码)」,不看什么布尔逻辑值。
- C风格for算术循环
- here-string
<<<把字符串作为命令标准输入,省去管道产生子进程
$'字符串'C风格转义字符串
trap -p:打印当前trap捕获设置,可以重定向保存。
14.4 bash与ksh93编译构建
实际工作极少需要,简单总结
- bash:官网下载源码包,下载补丁,
./configure && make && make install - ksh93:AT&T ast项目源码包,专用package工具编译。
14.5 其他Bourne家族Shell
- pdksh:公开实现ksh88,功能有限。
- zsh:交互能力极强,大量扩展语法,兼容ksh/bash部分语法。
查看各个shell版本命令
14.7 Shell初始化与终止文件(高频面试&实操)
关键区分4种shell类型:
- 登录shell:ssh登录、本机登录;会读取profile系列
- 交互式非登录shell:终端直接敲bash/zsh,新终端窗口
- 非交互式shell:执行脚本
./xxx.sh,几乎不读取rc配置- shell以
-开头的$0代表登录shell。
sh(原始Bourne shell)
- 登录shell:读
/etc/profile→~/.profile - 非登录shell:不读取任何启动文件
- 退出:没有自动logout脚本,只能自己写
trap '. ~/.logout' EXIT
ksh93
- 登录shell:
/etc/profile→~/.profile - 交互式shell(登录/非登录):读取环境变量
$ENV指向文件 - ⚠非交互式ksh执行脚本:不读取$ENV
bash
| shell类型 | 读取配置文件 |
|---|---|
| 登录shell | /etc/profile → 优先.bash_profile,没有就读.bash_login,再没有读.profile;退出执行.bash_logout |
| 交互式非登录shell(新开终端敲bash) | 只读取 ~/.bashrc |
| 非交互式执行脚本 | 不读.bashrc;如果设置BASH_ENV,就读取该变量指向文件 |
重点坑:脚本执行时,
.bashrc / .bash_profile不会自动加载。脚本不能依赖用户的别名、自定义变量,脚本内部要自己设置PATH。
zsh
启动文件最复杂:zshenv(所有shell都会加载)→ zprofile → zshrc → zlogin;登录退出读取.zlogout。
14.8 本章小结
- POSIX是可移植脚本基准;bash/ksh提供大量扩展,但存在行为差异,写通用脚本尽量少用扩展。
- bash专属工具
shopt,extglob/nullglob写脚本很常用。 - bash/ksh共有重要扩展:
select交互式菜单;[[ ]]高级条件测试;- extglob扩展模式匹配;
- 进程替换
<() >(); - 一维数组;
(( ))算术运算,C风格for循环;- here-string
<<<;
- shell启动文件规则一定要分清登录/交互式/非交互式:脚本执行属于非交互式,不会加载用户rc配置,脚本必须自给自足,不能依赖用户环境。
echo移植性差,可移植脚本优先printf。
文章作者 会写代码的小郎中
上次更新 2011-07-27
许可协议 CC BY-NC-ND 4.0