シェルのオプション(-x等)を子プロセスでも維持させる方法を考える
TL;DR
- 環境変数にオプション設定値を保存
- オプション設定値を読み込んでカレントシェルで
set
コマンドを実行するシェルスクリプトを用意し、呼び出し元シェルスクリプト内のカレントで当該スクリプトを実行 - スクリプト
背景
例えば以下のようなシェルスクリプトがあったとする。
#!/bin/sh
echo "script 1 options: ${-}"
"./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