『UNIXシェルスクリプト マスターピース132』読書ノート

2017/01/15

UNIXシェルスクリプト マスターピース132』を読んだので、学んだ点をまとめる。

パラメータ展開

  • ${param%word}で変数paramからwordに後方最短一致でマッチする部分を削除した値
  • ${param%%word}で変数paramからwordに後方最長一致でマッチする部分を削除した値
  • ${param#word}で変数paramからwordに前方最短一致でマッチする部分を削除した値
  • ${param##word}で変数paramからwordに前方最長一致でマッチする部分を削除した値
  • ${#param}で変数paramの文字数
  • ${param:offset:len}で変数paramのn+1文字目(offset)からm文字(len)
  • ${param:offset}で変数paramのn+1文字目(offset)から最後まで

文字数を取得するパラメータ展開と文字列の一部を取得するパラメータ展開を合わせると、変数の末尾1文字のみを取り出すといったこともできる。

paramの文字がABCDEの5文字であれば、上記は${param:4}となり、offsetは0からカウントするので、paramの5文字目から最後まで(つまりE)を取得できる。

ファイルの読み込み、変数への格納、一時的な環境変数

以下のCSVファイルを読み込んで各列を変数に格納するベストな方法について。

普段はforを使っていた。

しかし、while + readに一時的な環境変数を用いると、もっと簡潔に書ける。

readのうしろに変数を並べると、カラム列を変数に順に格納してくれる。
もしファイルがCSVでなくスペース区切りであれば、区切り文字解釈用のシェル変数IFS(Internal Field Separator)を変更する必要はないのだが、今回はCSVつまりカンマ区切りとしたいので、IFS=,を実行する必要がある。
ここでもしIFSの変更をreadと一緒に書かないと、環境変数が変わってしまうためにこの次に実行するコマンド全てが影響を受ける。

readとIFSの変更を同時に行っており、後続の処理に影響を与えていないパターン

readとIFSの変更を別々に行っており、後続の処理に影響を与えているパターン

シェルには環境変数を一時的に設定してコマンドを実行する機能があり、環境変数=値 コマンドとすれば、このコマンドやスクリプトを実行する時だけ環境変数を一時的に設定できる。
この機能を使用しないのであれば、IFS_ORG="$IFS"のように元のIFSの値をバックアップしてからIFSを変更し、処理が終わったのちにIFS="$IFS_ORG"としてIFSに元の値を書き戻す必要がある。

1行を区切り文字で分割して各変数に分けるのは、forを使うよりwhile readの方が簡潔なのは明らかだが、項目数に気をつけないと変な値になってしまう点には注意が必要。

上記の実行結果は以下のようになってしまう。

1項目と2項目のみ取得したければ、readのうしろにkey val1 val2のように項目数分の変数を用意しなければいけない。

もう一つ注意すべき点として、サーバ管理でよく行うforループとsshで複数台のサーバに同一の作業を行う方法を、そのままwhile readに変えても実行できないという点がある。
shのwhileループでファイルを読み、中でsshを実行すると1回しかループしないに書かれている通り、sshははじめの1回しか実行されない。

SSH を実行すると、標準入力がそちらに振り向けられるため、read で読んだ1行のみならず、ファイル全体が SSH に渡されてしまう。従って、SSH を実行した後はもう読める行がないので while ループは1回で終了してしまう。
これを防ぐには、ssh に -n オプションを付け、/dev/null をリダイレクトし、標準入力をリダイレクトしないようにする。

sshのmanには-nオプションについて以下のように書かれている。

-n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the background.

例えば、redis.confのslaveofの設定をしたいとする。対象のサーバは複数台あり、各slaveが参照するmasterがバラバラだとして、slaveとmasterの一覧が記載されたファイル(redis_slave_list.txt)を元にwhile readとsshでコマンドを組み立てるとき、sshに-nオプションを付けて、以下のように書く。

execコマンドと常駐型サーバプログラムの起動

Tomcatのstartup.shのようなラッパースクリプトでプログラムを起動する場合、execコマンドを使用するのが一般的。

単に外部プログラムを起動するだけなら、以下のように書いても問題ないのに、なぜexecコマンドが使用するかというと、execコマンドではシェルがforkを行わないから。現在のシェルで直接execシステムコールを発行し、結果として余計なプロセスを生成することがなくなる。また、execを使用しないで別プロセスがforkされると、このプログラムが終了するまでシェルが待ち続けてしまう。

コマンド失敗時にスクリプトを止めるset -eの一時無効化法

スクリプト内で途中のコマンドが失敗するとそこでスクリプトを停止したいとき、スクリプト冒頭でset -eを行うが、一時的にset -eを無効にしなければいけない時がある。
例えばdiffのようにコマンドの終了ステータスが非0を返す時にはset -eを無効にしなければならない。ファイルが存在しない等が理由でエラーになるなら問題ないが、ファイルの差分を抽出することが目的でdiffを行ったのに、差分があっただけで終了ステータスが1になってしまうため、スクリプトが停止してしまう。

無効にする有名で簡単な方法は、diffの直前でset +eにしてdiffの後でset -eにする方法だが、あまりスマートじゃない。
||:を用いる方法がスマートだと感じた。

このように書くと、diffの終了ステータスが0でなければ、OR演算子以降の:ヌルコマンドが実行される。ヌルコマンドは常に成功して0を返すため、set -eに引っかからない。
ヌルコマンドの代わりにtrueコマンドを用いてもOK。

空のif文

シェルスクリプトでは空のif文は許されない。
空にする代わりに、実行しても何も出力されず、必ず成功して終了ステータス0を返すヌルコマンドを実行するのがいい。

許可したユーザのみスクリプトを実行可能とする

許可したユーザのみスクリプトを実行可能とする方法は、whoamiid -nu$USER等で実行ユーザ名を取得して、実行していいかどうか判断すればいいという簡単なものだが、実行ユーザを指定する意義について以下の点があることを覚えておく。

  • 想定と異なるユーザで実行するとパーミッションの問題等でログファイルへの追記や一時ファイルへの出力ができずエラーになる。
  • rootならばエラーなく実行できるだろうと安易に考えて実行すると、ファイルがroot権限で作成されてしまい、次に正規のユーザで実行するときにファイルの上書きができなくなる

rpm

普段そこまで使用しないコマンドオプションだが便利そうなものをピックアップ。

  • あるファイルが属しているパッケージ名: rpm -qf ファイル名
  • あるパッケージに含まれるファイル一覧: rpm -ql パッケージ名

-読書ノート