四 bash选项的获取和设置

四 bash选项的获取和设置

【动机】

众所周知,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

不难看出, 以上方法至少存在以下几个不方便之处:

  1. 任意指定选项, 到底是归 shopt -o 还是 shopt 管, 要记住还真不是件容易的事;
  2. 不论是获取或者设置,均少不了判断, 至少是返回字符串的解析.
  3. 考虑到选项名的输入错误, 即查询不存在的选项时, 判断可能还会升级.

【意图】

将上述选项的获取设置方法, 统一封装到一个函数.

【有问题的实现】

新建文件 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
                 ----------------------

可以看到,

  1. 对于存在的 cdspell, history 选项, tstat 的获取和设置表现均正常;
  2. 不存在的 xyz 选项, 获取得到 NaN 状态, 设置被忽略, 也正是我们需要的;
  3. 唯独 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 与 栈结合起来, 以进一步简化保存修改还原的代码.