こんにちは。 マネーフォワードでアグリゲーション開発を担当しています中川です。
今回のブログは、私が bash スクリプトを書く際に心がけている事のおさらいをします。 知ってて当たり前のことかも知れませんが、意外と理解されていないアレです。
では、私が bash スクリプトを書く際によく使う記述を一つずつ紹介します。
2種類の shebang
シェルスクリプトの一行目に必ず記述する #!
で始まる行を shebang と言います。
bash スクリプトの shebang は、bash
を絶対パスで指定する方法と、env
を使って指定する方法の二種類あります。
bash
を絶対パスを指定する方法
#!/bin/bash
env
を使ってを指定する方法
#!/usr/bin/env bash
前者は /bin/bash
が使われます。
(/bin/bash
が存在しなければスクリプトの起動時にエラーとなります)
後者は $PATH
上の bash
が使われます。
(通常、bash
は一か所にしか無いので、後者でも /bin/bash
となる可能性が高いです。)
後者のメリットは、例えば $HOME/.opt
配下に最新の bash
をインストールするなどした場合、$PATH
にさえ入っていればそっちが使われるというのがあります。
変数の設定漏れを防止するオプション: set -u
- my_script.sh:
#!/usr/bin/env bash SOME_VARIABLE=foo echo $SOME_VARAIBLE echo bar
5 行目の echo
するところで SOME_VARIABLE
をタイポしています。
- 実行:
$ ./my_script.sh; echo ?=$? bar ?=0
実行すると、タイポの $SOME_VARAIBLE
は空に置換され、エラーになりません。
スクリプトの終了コードは正常(?=0
)となります。
set -u
を使った場合このようになります
- my_script.sh:
#!/usr/bin/env bash set -u SOME_VARIABLE=foo echo $SOME_VARAIBLE echo bar
- 実行:
$ ./my_script.sh; echo ?=$? ./my_script.sh: line 7: SOME_VARAIBLE: unbound variable ?=1
SOME_VARAIBLE
を使おうとしたところで中断し、スクリプトは異常終了(?=1
)となってくれます。
シェルスクリプトはコンパイルしないため変数名のタイポに気づきにくいところがあります。
習慣的に set -u
を指定するようにしておくと、タイポした変数を使ってスクリプトが続行してしまう事を防止できます。
補足
変数が未定義でも許したい場合があります。
例えば、シェルの引数を存在チェックし、存在すれば処理を続行し無ければ使用法を表示する場合、普通に $1
を見ようとするとコマンドの引数の指定が無かった時に $1
が未定義のエラーとなってしまうので使用法を表示する処理に進めません。
こうのような場合は bash の デフォルト値記述 (${parameter:-word}
) が活用できます。
if [ -z "${1:-}" ]; then echo "Usage: ${0##*/} MYARG1" >&2 exit 2 fi
このように ${1:-}
と記述すると $1
が未定義の場合は空に置換されエラーとなりません。
処理がエラーとなった時に中断するオプション: set -e
set -e
を使っていない例:
- my_script.sh:
#!/usr/bin/env bash FOO=$(wc --liens "$0") echo "lines: $FOO"
スクリプト自身の行数をカウントし "lines: N" と印字するはずのスクリプトですが、wc
の --lines
オプションをタイポしています。
- 実行:
$ ./my_script.sh; echo ?=$? wc: unrecognized option '--liens' Try `wc --help' for more information. lines: ?=0
引数が間違っているため wc
がエラーとなるが、スクリプトは続行するため未設定な FOO
が印字に使われ lines:
と表示されます。終了コードも正常となっています。
set -e
で直した場合:
- my_script.sh:
#!/usr/bin/env bash set -e FOO=$(wc --liens "$0") echo "lines: $FOO"
$ ./my_script.sh; echo ?=$? wc: unrecognized option '--liens' Try `wc --help' for more information. ?=1
エラーが起きた行で中断してくれます。習慣的に set -e
を設定してると予想外のエラーを無視してスクリプトが処理を続行するのを防げます。
補足
コマンドがエラーになっても無視して続行したい場合があります。
例えば、grep
は指定した文字列がヒットしたら正常(0
)、ヒットしなかったらエラー(1
)を返す仕様となっていますが、ヒットしない場合も何かしら処理したい事が大抵です。
もう一つ例として rm somefile
で somefile
がコマンドを実行する前から無くなっていても気にせずエラーにしたくない時があります。
set -e
の指定したスクリプトで、コマンドがエラーになっても続行される手段を紹介します。
手段1: if 条件コマンド; then コマンド; fi
を使う
if grep foo somefile >/dev/null; then : else : fi
if 条件コマンド; then
の条件コマンドはエラーを返してもスクリプトは中断しません。コマンドの結果で分岐する場合はこの方法が妥当でしょう。
手段2: コマンドが設けている特有のオプションを使用する
rm
の場合は -f
オプションを指定すると指定したファイルが存在しなくてもエラーとなりません。
他にも、
mkdir -p
は親フォルダも作成するのと同時に、フォルダが既に存在してもエラーになりませんrmdir --ignore-fail-on-non-empty
はフォルダが空でない場合はエラーにならずフォルダを残します
このように、コマンド側でよく使うケースを考慮してオプションを設けている事があります。
手段3: 対象コマンドの末に || true
と付ける。
COUNT=$(grep --count foo somefile || true) asdf || true
grep
が foo
を見つけられなても、asdf
というコマンドが存在しなくてもスクリプトはエラーを無視して続行します。
パイプライン内のコマンドがエラーとなった時に中断するオプション: set -o pipefail
上記で紹介した set -e
はパイプの右端のコマンドがエラーとなった時のみ有効です。
(例えば find | grep | wc
のパイプラインの場合、wc
がエラーとなった場合は中断するが、find
と grep
のエラーには無効です)
set -o pipefail
を使っていない例:
- my_script.sh:
#!/usr/bin/env bash set -e FOO=$(wc --liens "$0" | cut -d' ' -f1) echo "lines: $FOO"
上記で使った例に対して wc
の出力となる "N my_script.sh" から N の部分のみ取るために cut
を加えました。wc
の引数は再びタイポしています。
- 実行:
$ ./my_script.sh; echo ?=$? wc: unrecognized option '--liens' Try `wc --help' for more information. lines: ?=0
set -e
を指定しているのに関わらずエラー時点でスクリプトが中断しない。
set -o pipefail
で直した結果:
- my_script.sh:
#!/usr/bin/env bash set -e -o pipefail FOO=$(wc --liens "$0") echo "lines: $FOO"
wc: unrecognized option '--liens' Try `wc --help' for more information. ?=1
パイプ内のコマンドのエラーでも中断するようになります。
コマンドの結果が locale によって微妙に変わる件
システムの locale 設定によって sort
の並び順が違ったり sed
の結果が変わってきたりします。
せっかくテストしても実行時の LANG
設定が違っていたらスクリプトが想定通りに動かないと言う事があり得ます。
export LC_ALL=C
このようにスクリプト内で LC_ALL
を設定しておくと、スクリプトを実行する環境の LANG
が違っていても誤動作を避けられます。
まとめ
私は bash スクリプトを作成するときに習慣的に最小限以下のコマンドを先頭に書くようにしています。 参考までに紹介しておきます。
#!/usr/bin/env bash # Fail on unset variables and command errors set -ue -o pipefail # Prevent commands misbehaving due to locale differences export LC_ALL=C
最後に
マネーフォワードでは基本を大事にしつつ、爆速で開発できるエンジニアを募集しています。 下記サイトにてご応募お待ちしております!
【採用サイト】 ■『マネーフォワード採用サイト』 https://recruit.moneyforward.com/ ■『Wantedly』 https://www.wantedly.com/companies/moneyforward
【プロダクト一覧】 ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 https://moneyforward.com/ ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 iPhone,iPad ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 Android ■クラウド型会計ソフト『MFクラウド会計』 https://biz.moneyforward.com/ ■クラウド型請求書管理ソフト『MFクラウド請求書』 https://invoice.moneyforward.com/ ■クラウド型給与計算ソフト『MFクラウド給与』 https://payroll.moneyforward.com/