Sourceしても副作用のないシェルスクリプト
ファイル置き場
(2019 Aug.追記) 遅ればせながらGitHub, GitLabに慣れるため、それぞれにプロジェクトを作って置いてみましたよ。(2019 Sep. 追記 レポジトリを作り直しました。)
- https://github.com/nanigashi-uji/bash_script_skeleton.git
- https://gitlab.com/nanigashi_uji/bash_script_skeleton.git
修正 / 機能追加 v0.0.3 ~ v0.0.5
(2023 Jul. 追記)
-
source
した場合の自分自身の関数定義を元に戻すコードを追加しました。ただし、local
でないシェル変数を使っているので、複数回source
された場合は完全に元に戻せない可能性があります。(「副作用のない」を謳っているのですが......orz.) - ファイルの最後に自由記述のコメントが追加できるように、明示的に実行を止めるコードを追加しました。直接実行される場合には
exit
、source
される場合にはreturn
と使い分ける必要があるのを忘れないようにするためにテンプレートに残しておくことにしました。たとえば、スクリプトの後半に読み込むデータを埋め込んでおくような場合に有用です。 - 変数評価でQuoteされていなかったところをQuoteしました。
ちょっと修正
(2021 Oct. 追記) バグフィックス。詳細は最後に。
動機/経緯
ひょんなことからとある作業をシェルスクリプトで実装することにした。同じような作業をいくつかのシェルスクリプトで使うので、別のファイルに関数定義を書いてsource
することにしたのだが、関数内で関数外で定義済みのものを上書きしてしまう副作用は避けたい。変数に関しては、
-
local
をつけて定義 - 環境変数は
export
は使わず、コマンド実行時にenv
で設定して実行
で回避できる。ただし、関数定義やシグナルハンドラに関してはあまり便利な方法は見つけられなかった。名前の重複を避けるように気をつけて毎回名前を変えるという手はあるが、その注意力は別に使いたい。とはいえ、毎回同じような手順を実装するのも手間なので、なるべく汎用的なコードのスケルトンを用意することにした。source
して関数として実行するのと、単独でスクリプト実行するのを両立できるようにしたかったので、やむなくbash
拡張を使用している。
スケルトン
下記スケルトンでは、
-
func_skeleton
という関数名(2箇所)を、所望のものに変える。 - ヘルプメッセージをアップデートする。
- 引数処理をアップデートする。
- 関数本体を実装する。
- (必要に応じて)終了処理を修正
- (必要に応じて)シグナルハンドラを修正
という作業手順で使用することを想定している。機能としては、
- 単独実行しても
source
して関数実行することも可能に - 変数定義は
local
を使う/環境変数定義が必要であればenv
を使うこととする。 - 関数を定義する際は、あらかじめ定義をローカル変数にバックアップしておき、終了時にリストアする。もっとも外側の関数(
func_skeleton
(をリネームしたもの))はそうしていないが、これは「他と被らないファイル名/コマンド名をつけるべし」という範疇のものであろう。 - 割り込み処理の設定も、設定済みのものをローカル変数にバックアップしておき、終了時にリストアする。実際に割り込みがあった場合には、リストア後に同じシグナルを自分自身に送る。
-
エイリアスも、設定済みのものをローカル変数にバックアップしておき、終了時にリストアするサンプルをつけた。(あっても害はない。)(エイリアスはスクリプト内では使えないので意味なし。コメントアウトした:2019 Sep.追記) - 割り込み処理が必要になるもっとも一般的なケースである、一時ファイルの消去をサンプルとして書いておく。
という内容である。あまりBシェルは使ったことがなかったのでいろいろ調べた点の備忘録としては、「関数内で関数が定義できる」「複数のlocal変数定義を一行に書ける」「シグナルハンドラ自身でシグナル番号を知る機能はない(よってシグナル毎に処理を変えるにはtrap
をシグナルごとに定義する必要がある。)」「シグナル処理はサブシェルには引き継がれない」といったことがありました。
修正 (2021/10)
関数の定義をバックアップしておき終了時にリストアする方法に関して問題になるのは、declear -f
コマンドで出力される関数定義には最後の実行文の末尾にセミコロン(;
)が付けられないが、一方でeval
コマンドで読み込む場合には最後の実行文の前にセミコロン(;
)をつけないとエラーとなる。最初のバージョンでは単純に関数定義の最後にセミコロンを追加していたが、これだけでは関数の中で関数を多層的に定義してる場合には、関数内の関数の末尾にセミコロンがないのでエラーとなる。これに対処するため、場当たり的対処ではあるが、(1)関数定義の最後には必ずnop
(:
)をつける、(2)declear -f
で出力されたnop
(:
)を探してその後にセミコロン(;
)を追記する、ということにした。(多少強引な方法ではあるが、)
- 関数定義の最後に
nop
を追記 \
function echo_usage () {
... 途中省略 ...
return
}
function echo_usage () {
... 途中省略 ...
return
:
}
- 関数定義を復元する前に、関数定義の最後っぽい
nop
を探してセミコロンを追記して評価。(バックスラッシュの多重エスケープが必要)
unset echo_usage
test -n "${echo_usage_bk}" && eval ${echo_usage_bk%\}}" ; }"
unset echo_usage
test -n "${echo_usage_bk}" && { local echo_usage_bk="${echo_usage_bk%\}}"' \\; }'; eval "${echo_usage_bk//\; : \}/\; : \; \}}" ; }
コード全体は下記に。
#!/bin/bash
function func_skeleton () {
# Prepare Help Messages
local funcstatus=0
local echo_usage_bk=$(declare -f echo_usage)
local cleanup_bk=$(declare -f cleanup)
local tmpfiles=()
local tmpdirs=()
# local ls_sec_inifile_bk=$(alias ls_sec_inifile 2>/dev/null)
# alias ls_sec_inifile='sed -E -n -e "s/^[[:blank:]]*\[([^]]+)\][[:blank:]]*$/\1/p" ${1}'
function echo_usage () {
if [ "$0" == "${BASH_SOURCE:-$0}" ]; then
local this=$0
else
local this="${FUNCNAME[1]}"
fi
echo "[Usage] % $(basename ${this}) options" 1>&2
echo "[Options]" 1>&2
echo " -d path : Set destenation " 1>&2
echo " -h : Show Help (this message)" 1>&2
return
:
}
local hndlrhup_bk=$(trap -p SIGHUP)
local hndlrint_bk=$(trap -p SIGINT)
local hndlrquit_bk=$(trap -p SIGQUIT)
local hndlrterm_bk=$(trap -p SIGTERM)
trap -- 'cleanup ; kill -1 $$' SIGHUP
trap -- 'cleanup ; kill -2 $$' SIGINT
trap -- 'cleanup ; kill -3 $$' SIGQUIT
trap -- 'cleanup ; kill -15 $$' SIGTERM
function cleanup () {
# removr temporary files and directories
if [ ${#tmpfiles} -gt 0 ]; then
rm -f "${tmpfiles[@]}"
fi
if [ ${#tmpdirs} -gt 0 ]; then
rm -rf "${tmpdirs[@]}"
fi
# Restore signal handler
if [ -n "${hndlrhup_bk}" ] ; then eval "${hndlrhup_bk}" ; else trap -- 1 ; fi
if [ -n "${hndlrint_bk}" ] ; then eval "${hndlrint_bk}" ; else trap -- 2 ; fi
if [ -n "${hndlrquit_bk}" ] ; then eval "${hndlrquit_bk}" ; else trap -- 3 ; fi
if [ -n "${hndlrterm_bk}" ] ; then eval "${hndlrterm_bk}" ; else trap -- 15 ; fi
# Restore alias and functions
# unalias ls_sec_inifile
# test -n "${ls_sec_inifile_bk}" && eval alias ${ls_sec_inifile_bk}
unset echo_usage
test -n "${echo_usage_bk}" && { local echo_usage_bk="${echo_usage_bk%\}}"' \\; }'; eval "${echo_usage_bk//\; : \}/\; : \; \}}" ; }
unset cleanup
test -n "${cleanup_bk}" && { local cleanup_bk="${cleanup_bk%\}}"' \\; }'; eval "${cleanup_bk//\; : \}/\; : \; \}}" ; }
:
}
# Analyze command line options
local OPT=""
local OPTARG=""
local OPTIND=""
local dest=""
while getopts "d:h" OPT
do
case ${OPT} in
d) local dest=${OPTARG}
;;
h) echo_usage
cleanup
return 0
;;
\?) echo_usage
cleanup
return 1
;;
esac
done
shift $((OPTIND - 1))
local scriptpath=${BASH_SOURCE:-$0}
local scriptdir=$(dirname ${scriptpath})
if [ "$0" == "${BASH_SOURCE:-$0}" ]; then
local this=$(basename ${scriptpath})
else
local this="${FUNCNAME[0]}"
fi
local tmpdir0=$(mktemp -d "${this}.tmp.XXXXXX" )
local tmpdirs=( "${tmpdirs[@]}" "${tmpdir0}" )
local tmpfile0=$(mktemp "${this}.tmp.XXXXXX" )
local tmpfiles=( "${tmpfiles[@]}" "${tmpfile0}" )
echo "------------------------------"
echo "called as ${this}"
echo "ARGS:" $*
echo "------------------------------"
echo_usage 0
# clean up
cleanup
return ${funcstatus}
:
}
if [ "$0" == ${BASH_SOURCE:-$0} ]; then
func_skeleton "$@"
fi