Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

bash スクリプトの先頭によく書く記述のおさらい

こんにちは。 マネーフォワードでアグリゲーション開発を担当しています中川です。

今回のブログは、私が 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 somefilesomefile がコマンドを実行する前から無くなっていても気にせずエラーにしたくない時があります。

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

grepfoo を見つけられなても、asdf というコマンドが存在しなくてもスクリプトはエラーを無視して続行します。  

パイプライン内のコマンドがエラーとなった時に中断するオプション: set -o pipefail

上記で紹介した set -e はパイプの右端のコマンドがエラーとなった時のみ有効です。 (例えば find | grep | wc のパイプラインの場合、wc がエラーとなった場合は中断するが、findgrep のエラーには無効です)

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/