💾

シェルのオプション(-x等)を子プロセスでも維持させる方法を考える

に公開

TL;DR

  • 環境変数にオプション設定値を保存
  • オプション設定値を読み込んでカレントシェルでsetコマンドを実行するシェルスクリプトを用意し、呼び出し元シェルスクリプト内のカレントで当該スクリプトを実行
  • スクリプト

背景

例えば以下のようなシェルスクリプトがあったとする。

script1.sh
#!/bin/sh
echo "script 1 options: ${-}"
"./script2.sh"
script2.sh
#!/bin/sh
echo "script 2 options: ${-}"
実行結果
sh -c "set -x && ./script1.sh"
----------
+ ./script1.sh
script 1 options: 
script 2 options: 

script1.shを呼び出すまではset -xの効果は適用されているが、子プロセスとして呼ばれるscript1.sh以降はset -xの設定が継承されない。
. (a single period)sourceコマンドであればカレントシェルで実行されるためオプションは維持される。

所詮はただの子プロセスなので動作としては当然ではあるが、子プロセスかどうかにかかわらずシェルスクリプトで実行されるコマンドは解析のために-x-vを用いて全部確認したい、ということはよくあるケースと思われるが、ハードコーディングで子プロセスの呼び出し毎に引数を追加したり、呼び出し先でsetコマンドを都度修正するのはメンテナンス性がかなり悪い。

可能な限りスクリプトを修正することなく動的にシェルのオプションを1回で設定できるようにして維持させる方法を考えてみる。

.sourceを使えば良いという考えもあるかもしれないが、事実上引数を渡せないのは使い勝手が悪いので今回は考えない。

アプローチ

  • シェル間で使える設定値共有の仕組みとして環境変数が適切と考え、オプション値を環境変数に格納してスクリプトごとに値を読み込んでsetコマンドを実行する。
  • より汎用的に多くのシェルで使えるようにするため、POSIX準拠の記述とする。
  • 可能な限り厳密に検証するためオプション値を正規化し、使えないオプションは弾く。
  • (Bashなど向けのみ) setコマンドのヘルプから取得できる使用可能なコマンドを用いて、正規化処理の高速化を図る。

スクリプト

restore_opts.sh
#!/bin/sh
ENABLE_OPT_ARGS="${ENABLE_OPT_ARGS:-}"
DISABLE_OPT_ARGS="${DISABLE_OPT_ARGS:-}"

HELP_OPTION_PATTERN="[^]]*\[-?([^ ]+)\]"
VALID_OPT_ARGS="$(help set 2>&1 |
    grep -oE "${HELP_OPTION_PATTERN}" 2>/dev/null |
    head -n 1 |
    sed -nE "s/${HELP_OPTION_PATTERN}/\1/p")"

is_set_e=false
if echo "${-}" | grep -q e; then
    is_set_e=true
fi

filter_valid_opt_args() {
    filtered_opt_args=
    if [ -n "${1}" ] &&
        [ -n "${VALID_OPT_ARGS}" ]; then
        filtered_opt_args="$(echo "${1}" |
            sed "s/[^${VALID_OPT_ARGS}]//g")"
    else
        filtered_opt_args="${1}"
    fi
    echo "${filtered_opt_args}"
}

unique_opt_args() {
    printf "%s" "${1}" |
        fold -w1 |
        sort -u |
        tr -d "\n"
}

check_opt_args() {
    no_invalid_opt=false
    opts="${2}"
    escaped_prefix="$(echo "${1}" |
        sed -E "s/([\.\^\$\*\+\?\(\)\{\}\|\/\\])/\\\\\1/g")"
    if ${is_set_e}; then
        set +e
    fi
    while ! ${no_invalid_opt}; do
        set_result="$(set "${1}${opts}" 2>&1)"
        invalid_opt="$(echo "${set_result}" |
            grep -Ei "(invalid|illegal)" 2>/dev/null |
            head -n 1 |
            sed -nE "s/.*[^${escaped_prefix}]*\s+${escaped_prefix}(.+)([\s:].*|$)/\1/p")"
        if [ -z "${invalid_opt}" ]; then
            no_invalid_opt=true
        else
            opts="$(echo "${opts}" |
                sed "s/${invalid_opt}//g")"
            if [ -z "${opts}" ]; then
                no_invalid_opt=true
            fi
        fi
    done
    if ${is_set_e}; then
        set -e
    fi
    echo "${opts}"
}

set_opt_args() {
    opt_args="$(check_opt_args \
        "${1}" "$(unique_opt_args \
            "$(filter_valid_opt_args "${2}")")")"
    if [ -n "${opt_args}" ]; then
        set "${1}${opt_args}"
    fi
}

set_opt_args "-" "${ENABLE_OPT_ARGS}"
set_opt_args "+" "${DISABLE_OPT_ARGS}"

動作確認

事前準備

  • 有効にしたいオプションを環境変数ENABLE_OPT_ARGSにセットしておく(-不要)。
  • 同様に無効にしたいオプションをDISABLE_OPT_ARGSにセットしておく(+不要)。
  • 以下のようなシェルスクリプトを作成し、実行権限を付与する。
    ※本例では処理パスを通すためBashとする。
    script1.sh
    #!/bin/bash
    . "./restore_opts.sh"
    echo "script 1 options: ${-}"
    "./script2.sh"
    
    script2.sh
    #!/bin/bash
    set -x
    . "./restore_opts.sh"
    echo "script 2 options: ${-}"
    "./script3.sh"
    
    効果がわかりやすいようにscript2.shの冒頭でset -xを実行する。
    script3.sh
    #!/bin/bash
    . "./restore_opts.sh"
    echo "script 3 options: ${-}"
    

実行結果

パターン①:有効オプション: x
bash -c "export ENABLE_OPT_ARGS=x && ./script1.sh"
----------
++ set_opt_args + ''
+++++ filter_valid_opt_args ''
+++++ filtered_opt_args=
+++++ '[' -n '' ']'
+++++ filtered_opt_args=
+++++ echo ''
++++ unique_opt_args ''
++++ printf %s ''
++++ fold -w1
++++ sort -u
++++ tr -d '\n'
+++ check_opt_args + ''
+++ no_invalid_opt=false
+++ opts=
++++ echo +
++++ sed -E 's/([\.\^$\*\+\?\(\)\{\}\|\/\])/\\\1/g'
+++ escaped_prefix='\+'
+++ false
+++ false
++++ set +
+++ set_result=
++++ echo ''
++++ grep -Ei '(invalid|illegal)'
++++ head -n 1
++++ sed -nE 's/.*[^\+]*\s+\+(.+)([\s:].*|$)/\1/p'
+++ invalid_opt=
+++ '[' -z '' ']'
+++ no_invalid_opt=true
+++ true
+++ false
+++ echo ''
++ opt_args=
++ '[' -n '' ']'
+ echo 'script 1 options: hxB'
script 1 options: hxB
+ ./script2.sh
+ . ./restore_opts.sh
++ ENABLE_OPT_ARGS=x
++ DISABLE_OPT_ARGS=
++ HELP_OPTION_PATTERN='[^]]*\[-?([^ ]+)\]'
+++ help set
+++ grep -oE '[^]]*\[-?([^ ]+)\]'
+++ head -n 1
+++ sed -nE 's/[^]]*\[-?([^ ]+)\]/\1/p'
++ VALID_OPT_ARGS=abefhkmnptuvxBCHP
++ is_set_e=false
++ echo hxB
++ grep -q e
++ set_opt_args - x
+++++ filter_valid_opt_args x
+++++ filtered_opt_args=
+++++ '[' -n x ']'
+++++ '[' -n abefhkmnptuvxBCHP ']'
++++++ echo x
++++++ sed 's/[^abefhkmnptuvxBCHP]//g'
+++++ filtered_opt_args=x
+++++ echo x
++++ unique_opt_args x
++++ printf %s x
++++ fold -w1
++++ sort -u
++++ tr -d '\n'
+++ check_opt_args - x
+++ no_invalid_opt=false
+++ opts=x
++++ echo -
++++ sed -E 's/([\.\^$\*\+\?\(\)\{\}\|\/\])/\\\1/g'
+++ escaped_prefix=-
+++ false
+++ false
++++ set -x
+++ set_result=
++++ echo ''
++++ grep -Ei '(invalid|illegal)'
++++ head -n 1
++++ sed -nE 's/.*[^-]*\s+-(.+)([\s:].*|$)/\1/p'
+++ invalid_opt=
+++ '[' -z '' ']'
+++ no_invalid_opt=true
+++ true
+++ false
+++ echo x
++ opt_args=x
++ '[' -n x ']'
++ set -x
++ set_opt_args + ''
+++++ filter_valid_opt_args ''
+++++ filtered_opt_args=
+++++ '[' -n '' ']'
+++++ filtered_opt_args=
+++++ echo ''
++++ unique_opt_args ''
++++ printf %s ''
++++ fold -w1
++++ sort -u
++++ tr -d '\n'
+++ check_opt_args + ''
+++ no_invalid_opt=false
+++ opts=
++++ echo +
++++ sed -E 's/([\.\^$\*\+\?\(\)\{\}\|\/\])/\\\1/g'
+++ escaped_prefix='\+'
+++ false
+++ false
++++ set +
+++ set_result=
++++ echo ''
++++ grep -Ei '(invalid|illegal)'
++++ head -n 1
++++ sed -nE 's/.*[^\+]*\s+\+(.+)([\s:].*|$)/\1/p'
+++ invalid_opt=
+++ '[' -z '' ']'
+++ no_invalid_opt=true
+++ true
+++ false
+++ echo ''
++ opt_args=
++ '[' -n '' ']'
+ echo 'script 2 options: hxB'
script 2 options: hxB
+ ./script3.sh
++ set_opt_args + ''
+++++ filter_valid_opt_args ''
+++++ filtered_opt_args=
+++++ '[' -n '' ']'
+++++ filtered_opt_args=
+++++ echo ''
++++ unique_opt_args ''
++++ printf %s ''
++++ fold -w1
++++ sort -u
++++ tr -d '\n'
+++ check_opt_args + ''
+++ no_invalid_opt=false
+++ opts=
++++ echo +
++++ sed -E 's/([\.\^$\*\+\?\(\)\{\}\|\/\])/\\\1/g'
+++ escaped_prefix='\+'
+++ false
+++ false
++++ set +
+++ set_result=
++++ echo ''
++++ grep -Ei '(invalid|illegal)'
++++ head -n 1
++++ sed -nE 's/.*[^\+]*\s+\+(.+)([\s:].*|$)/\1/p'
+++ invalid_opt=
+++ '[' -z '' ']'
+++ no_invalid_opt=true
+++ true
+++ false
+++ echo ''
++ opt_args=
++ '[' -n '' ']'
+ echo 'script 3 options: hxB'
script 3 options: hxB```
パターン②:無効オプション: x
bash -c "export DISABLE_OPT_ARGS=x && ./script1.sh"
----------
script 1 options: hB
script 2 options: hB
script 3 options: hB

補足

shellcheck-xフラグを付与しないとSC1091で警告が出る。

説明

VALID_OPT_ARGSは、helpコマンドがあるシェルでのみhelp setを呼び出してヘルプから使用できないオプションのみを弾いた結果である。

  • Bashでの例
    bash
    help set
    ----------
    set: set [-abefhkmnptuvxBCEHPT] [-o option-name] [--] [-] [arg ...]
        Set or unset values of shell options and positional parameters.
        
        Change the value of shell attributes and positional parameters, or
       ・・・
    
    上記からabefhkmnptuvxBCEHPTの部分を取り出している。
    使用可能なオプションが取得できた場合のみ、filter_valid_opt_args()で環境変数をフィルタする。

unique_opt_args()で重複文字列を排除する(意味がないため)。

check_opt_args()ではサブシェルでsetコマンドを検証する。
VALID_OPT_ARGSが取得できていればここで引っかかることはほぼないはずだが、取得できていない場合等実際にsetコマンドでエラーとなった場合、その文字列を抽出して要求されたオプションから削除する。
エラーがなくなるか、要求されたオプションが空になるまで検証し続ける。

Discussion