現在の shell を shellscript の中から確認する(Linux, BSD 両対応)
現在の shell(以下、current shell) を確認する方法として真っ先に思い浮かぶのは以下だと思います。
$ echo $SHELL /bin/bash
ただ、これは不正解で、$SHELL はあくまでも login shell ですので、
login shell と current shell が違う場合は正しい値を返してくれません。
私の場合ですと、ローカル環境で常時利用しているのは zsh ですが、
login shell は bash です(zsh を login shell にすると重いと教えていただいたことがあるので)。
このようなケースでも正しい値を返す方法が以下です。
$ echo $0 -zsh
shell 上では $0 に current shell が格納されています。
これで解決すれば良いのですが、shellscript 内で確認しようと思うと話しは変わってきます。
shellscript 内で $0 には、script 名が格納されています。
shell 上での利用よりも、shellscript 内での利用頻度の方が高いでしょうからこれでは困ります。
色々調べていたら、現在使っているシェルの名前を知る方法 - 揮発性のメモ を発見しました。
いろいろ参考になりました、ありがとうございます。
readlink /proc/$$/exe
の説明をすると、
Linux の /proc/PID 以下には、プロセスに関する情報が色々入っていて、
- $$ で shellscript 自身の PID を取得して、
- シンボリックリンクの実体のパスを返す readlink に、
- コマンドのパスのシンボリックリンクが含まれている /proc/$$/exe を渡して、
- shellscript を実行している shell を取得する
となります。
これで解決かと思いましたが、以下の欠点があります。
- /proc が Linux にしかない(たぶん)
- 実行した時の shell になるので、current shell とは違う
2点目について説明すると、
shebang に #!/bin/bash と指定してスクリプトを実行したり、
bash /path/to/script のように実行すると、zsh を使っていても /bin/bash が返ってくる、ということです。
今回は、あくまでも current shell を取得したいのでこれでは要件を満たせません。
解決策
実行した shellscript の親プロセスは current shell になっているのではないか、
と当たりをつけて実験したらできました。
1つずつ説明すると、
_PID=$$
で Shellscript 自身の PID を取得して、
_PPID=$(ps -o ppid -p $_PID | tail -n 1)
で、Shellscript 自身の親PID(PPID)を出力しています。
これを更に分解すると、
$ ps -o ppid -p $_PID PPID 12345
のような出力になるので、
tail -n 1
で 2 行目だけ出力しているようにしています。
--no-headers を使っていない理由は BSD の ps になかったからです。
あとは、親PIDが取れたので、
ps -p $_PPID
で PID 指定で ps しています。
これで 実行した shellscript の親プロセスの情報を取得出来ました。
zsh を使っている状態でこれを実行すると、
$ ./get_current_shell.sh PID TTY TIME CMD 12345 pts/1 00:00:00 zsh
となります。
header を消したければ、同じ要領で tail をかませればよいでしょう。
あとは、grep -qs zsh にマッチすれば zsh、grep -qs bash にマッチすれば bash
といった判定が可能になります。
まとめ
login shell と current shell が違う場合や、
実行時の shell と current shell が違う場合でも、current shell を返す方法を示しました。
それほど使い道はないかもしれませんが、
.bashrc や .zshrc を読み直させたり追記したいときに、
login shell と current shell が違う場合でも、適切なファイルに対して処理が行えるでしょう。
Linux だけでなく BSD でも動くようにしましたので、汎用的に使えるのではないかと思います。
追記1
@hnakamur2 さんに、awk で 2行目をだけを出力するよりも、
tail -n 1 のほうがシンプルだとご指摘いただきましたので修正しました。
ご指摘ありがとうございます!
Linux, BSD で検証して動作することを確認済みです。
# 訂正前 _PPID=$(ps -o ppid -p $_PID | awk 'NR==2 { print }') # 訂正後 _PPID=$(ps -o ppid -p $_PID | tail -n 1)
追記2
@songmu さんからご指摘頂きました。
ありがとうございます!
shellscript(親shellscript) の中で shellscript(子shellscript) を呼び出す場合は、
子shellscript から見た親プロセスは、親shellscript となってしまいます。
したがって、正しく動作しません。
現状では、まだ解決できていませんが、
current shell 判定処理を関数化して別ファイルにし、
. get_current_shell.sh
get_current_shell
のように読み込んで、子shellscript から呼ばないようにするしかありません。
解決できたら追記致します。
Shellscript で指定回数ループするコードのベンチマーク
Shellscript で 指定回数ループする場合を調べていたら、
while でインクリメントしていくか、for in seq の2 パターンが見つかった。
どちらが高速なのか疑問に思ったのでベンチマークを取った。
while
code
#!/bin/sh i=0 while [ $i -ne 10000 ]; do i=`expr $i + 1` echo $i done
result
$ time ./while.sh real 0m20.557s user 0m4.466s sys 0m10.371s
for
code
#!/bin/sh for i in `seq 1 10000`; do echo $i; done
result
$ time ./for.sh real 0m0.188s user 0m0.132s sys 0m0.041s
考察
while が圧倒的に遅いことがわかった。
この結果から、while で行っている変数代入、expr、while 自体、のいずれかが遅いと考えられる。
そこで、この3つのどこに原因があるのか調査した。
while 自体が遅い?
これはほとんどないと思われるが一応ベンチをとる。
code
seq の結果をパイプで渡して、read すれば for in seq と大体同等の処理になると思われる(多分)。
seq 1 10000 | while read i; do echo $i done
result
$ time ./seq_while.sh real 0m0.239s user 0m0.184s sys 0m0.061s
read の分で for より少し遅くなっていると思われるが、誤差の範囲内である。
変数代入
code
seq_while.sh に 代入を加えただけ。
seq 1 10000 | while read i; do hoge=$i echo $i done
result
$ time ./assign.sh real 0m0.326s user 0m0.250s sys 0m0.092s
seq_while.sh より少し遅くなったくらいで、これも原因ではなさそうだ。
expr
消去法で、これが原因だとわかる。
結論
expr は遅いので、指定回数ループを回すときには使わないほうが良い。
また、expr 自体コストが高いようなので四則演算を頻繁に行う場合は、perl などに行わせたほうが良いのだと思う。
dstat で expr のループを見ていたがそこそこ CPU を喰っていたので、CPU リソースの観点からも使わないほうが良いと思われる。
おまけ
以下のような perl script もベンチをとってみた。
code
for my $i (1..10000) { print $i + 1, "\n"; }
result
$ time perl ./loop.pl real 0m0.044s user 0m0.017s sys 0m0.026s
四則演算しても、shellscript より perl のほうが速い。
2014/04/08 追記
id:hirata_yasuyuki さんにコメントいただいた、$(()) (以下、算術式展開)のベンチマークもとってみました。
id:hirata_yasuyuki さん、ありがとうございます。
以前の環境が思い出せないので、expr と for seq を再度実行して、結果の比較を行った。
expr
code
while.sh と同じ
result
real 0m17.375s user 0m4.588s sys 0m8.534s
for seq
code
for.sh と同じ
result
real 0m0.207s user 0m0.154s sys 0m0.028s
算術式展開
code
#!/bin/sh i=0 while [ $i -ne 10000 ]; do i=$(($i+1)) echo $i done
result
real 0m0.291s user 0m0.254s sys 0m0.035s
結論
ご指摘いただいた通り、算術式展開を使ったほうが圧倒的に速い。
また、for seq とも大差ない結果だった。
ただ、私の手元にある以下の書籍には、
BSDの一部の sh で一部使用できない演算子があったり、Solaris の sh だとそもそも使えない?らしく、移植性を重視したい場合は、expr を使ったほうが良いと記されていました(共に未検証)。
![[改訂新版] シェルスクリプト基本リファレンス ??#!/bin/shで、ここまでできる (WEB+DB PRESS plus) [改訂新版] シェルスクリプト基本リファレンス ??#!/bin/shで、ここまでできる (WEB+DB PRESS plus)](http://ecx.images-amazon.com/images/I/41GMyWgQ3cL._SL160_.jpg)
[改訂新版] シェルスクリプト基本リファレンス ??#!/bin/shで、ここまでできる (WEB+DB PRESS plus)
- 作者: 山森丈範
- 出版社/メーカー: 技術評論社
- 発売日: 2011/04/27
- メディア: 単行本(ソフトカバー)
- 購入: 9人 クリック: 119回
- この商品を含むブログ (11件) を見る
しかし、Linux 上で使うだけであれば、算術式展開で十分だと思われる。
while read でファイルを行読み込みして ssh をするシェルスクリプトの注意点
$ cat /tmp/hosts 192.168.0.10 192.168.0.11 192.168.0.12
のようなファイルがあったときに、
cat /tmp/hosts | while read _HOST; do ssh $_HOST "touch /tmp/testfile" done
などして、複数のホストに ssh でつないでコマンドを実行しようとする。
期待している処理としては、ループが回って 3 回処理を行ってほしいところだが、
実際は一度しか処理が実行されない。
実験と調査の結果、解決策は 2 つある(他にもあるかもしれない)。
解決策1. while の代わりに for を使う
私がこの問題に直面したときに解決した方法はこちらである。
原因はよくわからないが、read が標準入力を受けて処理するということだったので、
おそらく ssh でつなぐと 標準入力がなくなって、それ以降 read で読めなくなるのではないかと考えた(実際それは当たっていたようで、詳しくは解決策2 に記す)。
それなら cat をパイプして while read しなければうまくいくだろうと思い、
以下のように修正した。
for in _HOST $(cat /tmp/hosts); do ssh $_HOST "rm -rf /tmp/*" done
これで標準入力から読まなくなったので、無事に実行された。
for の場合、空白を含む行を処理しようとすると、スペースで区切って変数に代入されるので、
行ごとに変数に入れたい場合は、
IFS=$'\n'
として、区切り文字を変更すると良い。
解決策2. ssh の n オプションを使う
while だとだめな理由はなんとなくしかわからなかったので、調べてみた。
shのwhileループでファイルを読み、中でsshを実行すると1回しかループしない | b.l0g.jp に説明が書いてあった。
大変勉強になりました。ありがとうございます。
引用させていただくと、
SSH を実行すると、標準入力がそちらに振り向けられるため、read で読んだ1行のみならず、ファイル全体が SSH に渡されてしまう。従って、SSH を実行した後はもう読める行がないので while ループは1回で終了してしまう。
これを防ぐには、ssh に -n オプションを付け、/dev/null をリダイレクトし、標準入力をリダイレクトしないようにする
とのこと。
man にも書いてあった。
-n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the background. A common
trick is to use this to run X11 programs on a remote machine. For example, ssh -n shadows.cs.hut.fi emacs & will start an emacs on shad-
ows.cs.hut.fi, and the X11 connection will be automatically forwarded over an encrypted channel. The ssh program will be put in the back-
ground. (This does not work if ssh needs to ask for a password or passphrase; see also the -f option.)