How to check if script executed as a command in POSIX shell script
#posixismadvent この記事はPOSIX原理主義Advent Calendarの17日目だ。
POSIX原理主義のシェルスクリプトがコマンドとして実行されたか,dot(.
)コマンドで読み込まれたかどうかを判定する方法を記す。
結論としては,以下のようなコードでコマンドとして実行されたか判定できる。
#!/bin/sh
## \file script_name.sh
EXE_NAME='script_name.sh'
NOW_EXE=$(ps -p $$ -o args=)
case "$NOW_EXE" in *$EXE_NAME*)
echo "Executed as a file."
esac
Introduction
一般的に,シェルスクリプトでコマンドを自作する場合,コマンドをファイルとして実行することを前提として作られることが多い。例えば以下のようなコードだ。
cat <<- EOT >init.sh
#!/bin/sh
## \file init.sh
init(){
set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"
}
main()(
init
echo 'init: $@'
)
main "$@"
EOT
./init.sh a b c
init: a b c
しかし,上記のinit.sh
のinit
関数に見られるように,どのスクリプトでも共通で使うような処理が出てくる。それぞれのスクリプトで同じ内容を記述するのは冗長なので記述を共通化したくなる。つまり,シェルスクリプトをライブラリとして活用できてもいいのではないかと考えた。
シェルスクリプトでは,dot(.
)コマンドにより外部ファイルの内容を現在のシェルでそのまま実行することができる。これにより関数を現在のシェルやスクリプトで読み込むことができる。
cat <<- EOT >import.sh
#!/bin/sh
## \file import.sh
. /init.sh
main(){
echo "import: $@"
}
EOT
./import.sh 1 2 3
init: 1 2 3
import: 1 2 3
しかし,上記のimport.sh
の実行結果でimport.sh
では記述していないinit: 1 2 3
が出力されていることからわかるように,単にdotコマンドで読み込んだだけだと,当然ながら読み込んだファイルの通常のコードが実行されてしまう。関数定義を読み込みたいだけなので,通常のコマンドを実行したくない。
これについて,例えばPythonやRubyではファイルとして実行されたかどうかを判定する記述方法が存在する。
cat <<- EOT >if_main.py
#!/usr/bin/env python3
## \file if_main.py
def foo():
printf(__name__)
if __name__ == '__main__':
foo()
EOT
./if_main.py
__main__
cat <<- EOT >if_main.rb
#!/usr/bin/env ruby
## \file if_main.rb
def foo
puts $0
end
if __FILE__ == $0
foo()
end
EOT
./if_main.rb
./if_main.rb
PythonとRubyではファイルとして実行されたかどうかの情報を変数で保持している。したがって,この変数を使いファイルとして実行されている場合にだけ,関数を実行することができる。このように記述することで,ライブラリしても,コマンドとしても使える汎用性が高いスクリプトを作成できる。
このようなことをPOSIX準拠のシェルスクリプトでもできないか検討した。
Method
まず,ネット上で提案されている事例を調査した。複数の案があったので,そのうちダメな方法を以下で一覧する。
方法 | 参照元 | ダメな理由 |
---|---|---|
| URL | $_変数がPOSIX非準拠。過去にはPOSIX規格に存在したが,多重定義時にkshで混乱するため2001年に廃止となった。 |
| URL | BASH_SOURCE変数がbash専用。 |
| URL | 関数以外でのreturn コマンドの挙動がPOSIX未定義。bash,zshとdashでは挙動が違う。 |
| URL | caller コマンドがPOSIX未定義。 |
bashの独自拡張を使ってよいのであれば,上記のようにいくつもやり方があるのだが,POSIX原理主義を通すには他の方法を探るしかない。
調べてわかった重要な前提として,dot(.)コマンドで読み込んだファイル名を保存する変数やアクセスするコマンドなどはPOSIXの範囲では存在しない。そのため,自分でファイルにファイル名を格納する変数をハードコーディング(直打ち)して,このファイル名と一致するかで判定する。
具体的には,以下のコードの$hogeに相当する変数を用意する。
#!/bin/sh
## \file file.sh
EXE_NAME='file.sh'
case "$hoge" in *"$EXE_NAME"
echo "MAIN"
esac
この視点に立って,改めて利用可能な方法を検討する。考えられる方法は以下の2通りだ。
- $0に格納される値と比較
- psコマンドの実行で得られる現在実行中のプロセス名と比較
使用する変数を以下の表で説明した。
変数 | 説明 |
---|---|
$_ | シェルかシェルスクリプトの起動で使われたフルパス。POSIX非準拠。 |
$0 | 現在のシェルかシェルスクリプト名。 |
$$ | 現在のプロセスID |
$PPID | 親プロセスのID |
1点目の方法では,スクリプト実行時に$0変数に格納される値と比較する。この方法でうまくいくならこれが一番素直で簡単だ。
2点目の方法では,psコマンドを使ってプロセス名を取得する。
psコマンドを実行すると以下のようにプロセスのIDと実行コマンドが表示される。
ps
PID TTY STAT TIME COMMAND 2275 pts/2 Ss 0:00 bash 22064 pts/9 R+ 0:00 ps w
この出力から[COMMAND]で表示される実行コマンドを取得することでファイル名と比較を行う。
psコマンドのオプションで,-p
で表示させるプロセスIDを指定し,-o
で出力項目(列)を指定できる。-o
オプションで指定できる項目はいくつかあるが,今回は実行コマンド名がほしいので,comm
とargs
を試す。comm
では実行中のコマンドだけ(C言語のargv[0]
相当)を出力する。args
はコマンドだけではなく呼び出しコマンドも表示できる可能性がある。また,-o
オプションでは指定対象の末尾に=を付けることで,ヘッダーを省略できる。
psコマンドで対象とするプロセスには現在のプロセス$$と親プロセス$PPIDが考えられる。現在のプロセスはもちろんであるが,親プロセスが何であるかで特定できるかもしれないので検討対象とした。
これらから,以下のpsコマンドで現在実行中のファイル名を取得できる可能性がある。
ps -p $PPID -o comm=
ps -p $$ -o comm=
ps -p $$ -o args=
ここまでで,$0とpsコマンドによるPOSIXに準拠した方法で現在実行中のファイル名の取得方法について検討した。実際にこれらの方法で値を取得できるかを検証していく。
検証にあたって,ファイル名での実行とdot(.
)コマンドによる読み込みで考えられる全てのパターンを試す。具体的には以下のrun_if_main.sh
のコードで示すように,以下の2パターンを考慮した。
- 現在のシェルでの読み込みと実行
- 新しいシェルでの読み込みと実行
なお,参考までに$_の値も一緒に確認する。
run_if_main.sh
とif_main.sh
を同じディレクトリ(/home/senooken/tmp
)に配置して,run_if_main.sh
を実行した。実行はUbuntu 16.04のbashから以下のコマンドで行った。
cd ~tmp
./run_if_main.sh
実行結果を以下の表にまとめた。
表示項目 | run_if_main.sh 内での実行コマンド | ||||
---|---|---|---|---|---|
./if_main.sh | . ./if_main.sh | sh ./if_main.sh | sh -c './if_main.sh' | sh -c '. ./if_main.sh' | |
期待される値 | if_main.sh | if_main.sh以外 | sh | if_main.sh | sh |
$_ |
./run_if_main.sh | ./run_if_main.sh | ./run_if_main.sh | ./run_if_main.sh | ./run_if_main.sh |
$0 |
./if_main.sh | ./run_if_main.sh | ./if_main.sh | ./if_main.sh | sh |
ps -p $PPID -o comm= |
run_if_main.sh | bash | run_if_main.sh | sh | run_if_main.sh |
ps -p $$ -o comm= |
if_main.sh | run_if_main.sh | sh | if_main.sh | sh |
ps -p $$ -o args= | /bin/sh ./if_main.sh | /bin/sh ./run_if_main.sh | sh ./if_main.sh | /bin/sh ./if_main.sh | sh -c . ./if_main.sh |
この表の結果からいえることを以下にまとめた。
この4番目のsh ./if_main.sh
実行時の結果の違いは重要な論点となる。具体的には,sh ./if_main.sh
はファイルを読み込んでいるのか,それともコマンドとしてファイルを実行しているのか?だ。
この議論を検証するために,実行コマンドであるPOSIXのshのマニュアルを確認する。
上記で書かれているように,sh ファイル
で実行された場合はファイルを実行ではなく読み込んでいる。したがって,$0ではなくps -p $$ -o comm=
がベストな判定方法だろう。
なお,仮に$0がps -p $$ -o comm=
が同じ正しい結果を返していた場合でも,psコマンドを採用することになる。理由は$0のzshの標準の挙動がPOSIX shellと異なるからだ。
zshではfunction_argzero
というオプションがデフォルトで有効になっている。このオプションを有効にすると,dotコマンドでファイルを読み込んだ場合,自動的にそのファイル名を$0に設定してしまう。
この機能が便利になる面もあるのだろうが,この機能のためにzshだけが他のシェルとデフォルトの挙動が異なる。つまり,$0を採用した場合zshだけ特別設定が必要となってしまう。そのため,共通で有効なpsコマンドを使うべきと判断した。
Coding
ここまでの調査で分かったことから,POSIX原理主義のシェルスクリプトでファイルとして実行中かどうかの判定は以下のようにして行える。
#!/bin/sh
## \file script_name.sh
EXE_NAME='script_name.sh'
NOW_EXE=$(ps -p $$ -o comm=)
if [ "$EXE_NAME" = "$NOW_EXE" ]; then
echo "Executed as a file."
fi
実際に,上記ファイルを実行したら,コマンドファイルとして実行したことを判定できている。
. ./script_name.sh
./script_name.sh # Executed as a file
この調査結果を利用して,現在実行中かどうかの判定関数を以下のように作れる。
## コマンドファイルの名前をグローバル変数EXE_NAMEに代入しておき参照
is_main()(
# EXE_NAME='script_name.sh' # またはis_main関数で定義
NOW_EXE=$(ps -p $$ -o comm=)
[ "$EXE_NAME" = "$NOW_EXE" ]
)
この関数を活用すれば,以下のようなライブラリとしても,コマンドファイルとしても利用可能なシェルスクリプトを作成できる。
#!/bin/sh
## \file is_main.sh
EXE_NAME='is_main.sh'
init(){
set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"
}
is_main()(
NOW_EXE=$(ps -p $$ -o comm=)
[ "$EXE_NAME" = "$NOW_EXE" ]
)
main()(
init
echo "MAIN. $@."
)
if is_main; then
main "$@"
fi
## 現在実行中かの判定を関数にしない場合は,上記を以下のように記述する。
# NOW_EXE=$(ps -p $$ -o comm=)
# if [ "$EXE_NAME" = "$CUURENT_EXE" ]; then
# main "$@"
# fi
## もちろん1行で書いてもいい
# is_main && main "$@"
# [ "$EXE_NAME" = "$NOW_EXE" ] && main "$@"
2017-01-15追記:
ps -o comm=
では,コマンド名が15文字までしか表示されないことがわかった。
linux – What is the maximum allowed limit on the length of a process name? – Stack Overflow
そこで,15文字以上のコマンド名にも対応するために,ps -p $$ -o args=
を採用する。この場合,表示される内容は以下の通りにインタープリターのフルパスとコマンド引数まで表示される。
/bin/sh ./script_name.sh 0
コード中に埋め込んだコマンド名とプロセス上でのコマンド名のマッチさせるため,case文により判定を行う。
## Function
is_main()(
NOW_EXE=$(ps -p $$ -o args=)
case "$NOW_EXE" in *$EXE_NAME*);;
*) return 1;;
esac
)
## Without function
NOW_EXE=$(ps -p $$ -o args=)
case "$NOW_EXE" in *$EXE_NAME*)
main "$@"
esac
Conclusion
POSIX原理主義によるシェルスクリプトをライブラリとしても活用するための第一歩として,スクリプトが現在実行中かどうかの判定方法を検討し,確立できた。この方法を使えば,自作のシェルスクリプトのメイン部分を関数にまとめて,4-5行追加するだけで,コマンドだけでなくライブラリとしても活用できる。
実のところ,今のままだとシェルスクリプトをライブラリ化しようが,コマンドのままとたいした違いはない。ただ,変数の命名規則(メソッドやプロパティの区切りを__
に見立てたりなど)や関数の構造を工夫することで,シェルスクリプトでもオブジェクト指向的なことができるのではないかと考えている。もしこれがうまくいけば,より高度で効率的,汎用性の高いPOSIX原理主義による開発ができるかもしれない。
今回の調査はこのための第一歩だった。将来の応用に向けた基礎研究的なものだろう。POSIX原理主義なら時空を超えることができる。蓄積が重要になる。だから,今回のような細かい話で,すぐには役に立たなさそうな内容であっても,無駄にはならないだろう。
参考: