【动机】
众所周知,bash 选项分两部分, 分别归 shopt 和 shopt -o 管理, 而后者等同于 set -o.
以 errexit 和 autocd 为例, 使用命令行返回的字符串获取:
shopt -o errexit | grep -E 'on|off' -o
shopt autocd | grep -E 'on|off' -o
或者通过退出码获取:
shopt -o errexit &>/dev/null && echo on || echo off
shopt autocd &>/dev/null && echo on || echo off
设置, 不考虑类似 set -e , set +e , set -x, set +x 的快捷方式, 通用型设置, 需要 on 到 -s 以及 off 到 -u 的变换或者映射:
# set to on
shopt -o -s errexit
shopt -s autocd
# set to off
shopt -o -u errexit
shopt -u autocd
不难看出, 以上方法至少存在以下几个不方便之处:
- 任意指定选项, 到底是归 shopt -o 还是 shopt 管, 要记住还真不是件容易的事;
- 不论是获取或者设置,均少不了判断, 至少是返回字符串的解析.
- 考虑到选项名的输入错误, 即查询不存在的选项时, 判断可能还会升级.
【意图】
将上述选项的获取设置方法, 统一封装到一个函数.
【有问题的实现】
新建文件 options.sh, 新建函数 tstat, 通过参数界定当前是获取还是设置. 采用的是 echo 返回具体的 on / off, 会有一点问题:
:<<COMMENT
【已废弃】: 设置自身 shell, 但获取的其实是子 shell 选项
获取或设置 option 的状态 (on or off)
参数, $2 为空时, 表示获取; 非空则表示设置
$1: option name
$2: on | off, other ignored
usage: tstat <option-name> <status? = on | off>
返回值:
1. 设置选项值时,无返回值
2. 获取选项值时,分以下情况:
1) 不存在的选项,返回 NaN
2) 存在的选项,根据当前的实际状态,返回 on 或者 off
注意:设置选项值为 on off 意外的值被忽略
COMMENT
tstat(){
if [ "$1" ]; then
if [ "$2" == on -o "$2" == off ];then
# 设置:
# shopt -s <opt_name>, shopt -u <opt_name>, 找不到 option, 返回 1; 其余返回 0
# shopt -o -s <opt_name>m shopt -o -u <opt_name>, 不论何种情况,均返回 0(这与文档有出入)
# 所以末尾的 || : 可选
local act; [ "$2" == off ] && act=-u || act=-s
shopt $act "$1" 2>/dev/null || shopt -o $act "$1" 2>/dev/null || :
elif [ -z "$2" ];then
# 获取:shopt <opt_name> 和 shopt -o <opt_name> 顺序可选。
# 因为两者均在成功获取到 on 状态, 返回 0; option 不存在或获取到 off 状态, 返回 1
# 其一返回 0, 则一定是 on 状态
# 否则, 如两者的返回均包含 invalid, 则一定是非法选项, 则标记 NaN
# 其他, 则一定是 off
local res
if shopt "$1" &>/dev/null || shopt -o "$1" &>/dev/null; then
res=on
elif [[ "$(shopt "$1" 2>&1 )" =~ invalid && "$(shopt -o "$1" 2>&1)" =~ invalid ]];then
res=NaN
else
res=off
fi
echo $res
fi
fi
}
各个调用命令调用有相应的注释说明, 这里不多做解释. 先测试一下. 新建文件 test.sh:
# 根据实际文件位置修改路径
source ./options
way1(){
local opt res
local opts="cdspell history xyz errexit"
for opt in $opts; do
echo -e "\t init $opt to on:"
shopt -s $opt 2>/dev/null || shopt -o -s $opt 2>/dev/null || :
echo "current $opt status:<$(tstat $opt)>, next change to off"
tstat $opt off
# 非法 shell option 名称, 或者状态为 off, 退出码均为非 0, 所以需要区分
shopt -o $opt 2>/dev/null || shopt $opt 2>/dev/null || :
echo "continue to change to on"
tstat $opt on
shopt -o $opt 2>/dev/null || shopt $opt 2>/dev/null || :
echo -e "\t\t ----------------------"
done
}
clear
way1
运行结果如下:
init cdspell to on:
current cdspell status:<on>, next change to off
cdspell off
continue to change to on
cdspell on
----------------------
init history to on:
current history status:<on>, next change to off
history off
continue to change to on
history on
----------------------
init xyz to on:
current xyz status:<NaN>, next change to off
continue to change to on
----------------------
init errexit to on:
current errexit status:<off>, next change to off
errexit off
continue to change to on
errexit on
----------------------
可以看到,
- 对于存在的 cdspell, history 选项, tstat 的获取和设置表现均正常;
- 不存在的 xyz 选项, 获取得到 NaN 状态, 设置被忽略, 也正是我们需要的;
- 唯独 errexit 选项, 开始明明已经设置为 on, 为什么获取到的是 off 呢?
原因在于 tstat 函数采用的是 echo 返回值, 决定了调用方式 $(tstat $opt), 后者实际上创建了子 shell, 也就是说, 此时获取到的是子 shell 的 errexit, 而 errexit 默认值是 off!
当然, 如果先将 inherit_errexit 设置为 on, 可以得到貌似正常的结果:
# 根据实际文件位置修改路径
# source ./options
way2(){
local opt res
local opts="cdspell history xyz errexit"
shopt -s inherit_errexit
for opt in $opts; do
echo -e "\t init $opt to on:"
shopt -s $opt 2>/dev/null || shopt -o -s $opt 2>/dev/null || :
echo "current $opt status:<$(tstat $opt)>, next change to off"
tstat $opt off
# 非法 shell option 名称, 或者状态为 off, 退出码均为非 0, 所以需要区分
shopt -o $opt 2>/dev/null || shopt $opt 2>/dev/null || :
echo "continue to change to on"
tstat $opt on
shopt -o $opt 2>/dev/null || shopt $opt 2>/dev/null || :
echo -e "\t\t ----------------------"
done
shopt -u inherit_errexit
}
clear
way2
运行结果如下:
...... 以上相同, 略
init errexit to on:
current errexit status:<on>, next change to off
errexit off
continue to change to on
errexit on
----------------------
看起来似乎正常了, 但从调用上讲, 是没有意义的. 因为 $(tstat $opt) 语意上即是获取子 shell 的选项, 像 cdspell history 等, 之所以能得到正确结果, 在于这些选项本来就是跨 shell 的. 况且如果每次获取 errexit 前都需要修改 inherit_errexit, 用完后又将其修改回去, 似乎不够优雅, 也不科学.
那么, 有没有调用后, 能真实获取到当前 shell 的选项状态的方法呢? 有, 这就是上篇讲的输出参数.
【正确的实现】
options.sh 文件中, 添加函数 tstatx:
:<<COMMENT
获取或设置 shell option 的状态 (on or off)
参数:
-t: opt 选项名称,必填
-v: value 要设置选项的新值,可选
-n: v_name 获取选项值,将值写入该变量,可选
usage: tstatx -t <opt> -v <value?> -n <v_name?>
返回值:无
注意:
1. -t 参数必填;
2. -v 与 -n 虽然可选, 但最少必须提供一个;两个都提供,则 -v 被忽略,即获取优先。
3. 获取选项值时,分以下情况:
1) 不存在的选项, v_name 变量赋值为 NaN
2) 存在的选项,根据当前的实际状态, v_name 变量赋值为 on 或者 off
4. 设置选项值为 on off 以外的值被忽略
5. 参数违反 1 2, exitcode=1; 其余 exitcode=0(即使操作被忽略)
COMMENT
tstatx(){
eval "$PAS"
local opt=$(para t)
local value=$(para v)
local v_name=$(para n)
local ecd=0
if [[ -z "$opt" || -z "$value" && -z "$v_name" ]]; then
error "选项名称(-t)必须提供; 选项新值(-v)或用来获取当前值的变量名(-n)最少提供其一!"
ecd=1
else
#echo "start...opt=<$opt>,value=<$value>,v_name=<$v_name>"
if [ "$v_name" ];then
# 获取:shopt <opt_name> 和 shopt -o <opt_name> 顺序可选。
# 因为两者均在成功获取到 on 状态, 返回 0; option 不存在或获取到 off 状态, 返回 1
# 其一返回 0, 则一定是 on 状态
# 否则, 如两者的返回均包含 invalid, 则一定是非法选项, 则标记 Nan
# 其他, 则一定是 off
local _res
if shopt "$opt" &>/dev/null || shopt -o "$opt" &>/dev/null; then
_res=on
# 仅仅查验 option 是否 invalid(是否存在该 option), 所以创建子 shell 无不良影响.
elif [[ "$(shopt "$opt" 2>&1 )" =~ invalid && "$(shopt -o "$opt" 2>&1)" =~ invalid ]];then
_res=NaN
else
_res=off
fi
eval $v_name=$_res
elif [ "$value" == on -o "$value" == off ];then
# 设置:
# shopt -s <opt_name>, shopt -u <opt_name>, 找不到 option, 返回 1; 其余返回 0
# shopt -o -s <opt_name>m shopt -o -u <opt_name>, 不论何种情况,均返回 0(这与文档有出入)
# 所以末尾的 || : 可选
local act; [ "$value" == off ] && act=-u || act=-s
shopt $act "$opt" 2>/dev/null || shopt -o $act "$opt" 2>/dev/null || :
fi
fi
return $ecd
}
测试文件 test.sh
# 根据实际文件位置修改路径
source ./options
way3(){
shopt -o -s emacs
shopt -s autocd
set +e
local v_emacs v_ee
local v_acd v_csp v_xyz
# 获取
tstatx -t emacs -n v_emacs # shopt -o
tstatx -t errexit -n v_ee
tstatx -t autocd -n v_acd # shopt
tstatx -t cdspell -n v_csp
tstatx -t xyz -n v_xyz
echo "原始状态: emacs=<$v_emacs>,errexit=<$v_ee>,autocd=<$v_acd>,cdspell=<$v_csp>,xyz=<$v_xyz>"
# 反向
tstatx -t emacs -v off
tstatx -t errexit -v on
tstatx -t autocd -v off
tstatx -t cdspell -v on
tstatx -t xyz -v on_or_off
# 再获取
tstatx -t emacs -n v_emacs # shopt -o
tstatx -t errexit -n v_ee
tstatx -t autocd -n v_acd # shopt
tstatx -t cdspell -n v_csp
tstatx -t xyz -n v_xyz
echo "反向后: emacs=<$v_emacs>,errexit=<$v_ee>,autocd=<$v_acd>,cdspell=<$v_csp>,xyz=<$v_xyz>"
}
clear
way3
结果如下:
原始状态: emacs=<on>,errexit=<off>,autocd=<on>,cdspell=<off>,xyz=<NaN>
反向后: emacs=<off>,errexit=<on>,autocd=<off>,cdspell=<on>,xyz=<NaN>
可以看到, 现在获取和设置的均是当前 shell 的选项.
下篇继续讨论如何将 tstatx 与 栈结合起来, 以进一步简化保存修改还原的代码.