tkuchikiの日記

新ブログ https://blog.tkuchiki.net

現在の 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 以下には、プロセスに関する情報が色々入っていて、

  1. $$ で shellscript 自身の PID を取得して、
  2. シンボリックリンクの実体のパスを返す readlink に、
  3. コマンドのパスのシンボリックリンクが含まれている /proc/$$/exe を渡して、
  4. shellscript を実行している shell を取得する

となります。

これで解決かと思いましたが、以下の欠点があります。

  • /proc が Linux にしかない(たぶん)
  • 実行した時の shell になるので、current shell とは違う

2点目について説明すると、
shebang に #!/bin/bash と指定してスクリプトを実行したり、
bash /path/to/script のように実行すると、zsh を使っていても /bin/bash が返ってくる、ということです。

今回は、あくまでも current shell を取得したいのでこれでは要件を満たせません。

解決策

実行した shellscript の親プロセスは current shell になっているのではないか、
と当たりをつけて実験したらできました。

以下がスクリプトです。
2つ目のスクリプトは、活用例です。

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 にマッチすれば zshgrep -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)

しかし、Linux 上で使うだけであれば、算術式展開で十分だと思われる。

シェルスクリプトの for で seq 以外の方法で指定回数ループを回す

シェルスクリプトで指定回数ループを回す方法をぐぐってみたところ、
上位の結果は以下の方法がヒットした。

for i in `seq 1 3`; do
  echo $i;
done

# 実行結果
1
2
3

これってブレース展開でもできるんじゃ?
と思ってやったら、予想通りできた。

for i in {1..3}; do
  echo $i;
done

# 実行結果
1
2
3

ただ、bash 以外のシェルで動作するかはわからないので、
移植性を考えると seq を使ったほうが良いのかもしれない。

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.)

n オプションは、BSDssh にもあるようなので、mac でも同様に使用することができそうだ。