高品質なシェルスクリプトを書くためのベストプラクティス

これはなに Link to this heading

シェルスクリプトを書く際に個人的に意識していることのまとめ。

チェックリスト Link to this heading

  • シバンを書いた。
  • setコマンド(e.g. set -euo pipefail)を記述した。
  • エラー発生時に、ユーザーに意味のあるエラーメッセージを表示して処理を終了するようにした。
  • 必要な引数が指定されているか、コマンドライン引数をチェックした。
  • 変数名や関数名を、その目的を明確に示す名前にした。
  • 繰り返し使用するコードを関数にまとめた。
  • 関数ではローカル変数を用いた。
  • 定数にreadonlyを付けた。
  • 定数と変数が一目でわかる命名規則を採用した。
  • 変数を展開する際にダブルクオートを用いた。
  • スクリプトの冒頭にコードの目的・使用方法・依存関係を記述した。
  • 複雑なコードに説明をコメントで追加した。
  • インデントをそろえた。

ベストプラクティス Link to this heading

必ずシバンから始める Link to this heading

必ずシェルスクリプトの先頭にシバン(Shebang)(e.g. #!/bin/sh#!/bin/bashを記述し、スクリプトを実行するシェルを明示的に指定しよう。これにより、異なるシステムや環境でも正しいシェルの使用が保証され、一貫してスクリプトが実行されるようになる。言い換えれば、スクリプトの互換性と実行の一貫性が向上し、意図しない動作を防げる。シバンを使わないと、スクリプトがデフォルトのシェルで実行されてしまい、意図しない動作が発生する可能性を生じさせてしまう。

set -euo pipefailを使う Link to this heading

setコマンドを利用して、エラー時の挙動を適切に設定しよう。適切に設定しないと、スクリプト実行中にエラーが発生してもスクリプトが停止せず、意図しない動作が発生する可能性を生じさせる。

基本的にはset -euo pipefailを使用すればよい。それぞれのオプションは以下の意味を持つ。

  • -e (errexit): このオプションが設定されている場合、スクリプト内のコマンドがエラー(非ゼロの終了ステータス)で終了した場合に、スクリプト全体が直ちに終了する。これにより、エラーが無視されることなく、問題が発生した時点でスクリプトが停止する。
  • -u (nounset): このオプションが設定されている場合、未定義の変数を参照しようとすると、スクリプトが終了する。これにより、誤って未定義の変数を使用してしまう問題を防げる。
  • -o pipefail: このオプションが設定されている場合、パイプでつながれたコマンドのいずれかがエラー(非ゼロの終了ステータス)で終了した場合に、パイプ全体がエラーとして扱われる。これにより、パイプの途中で問題が発生しても、適切にエラーハンドリングできる。

つまり、スクリプトでエラーが発生した際にすぐ停止するようset -eを用いる。未定義の変数が参照された場合にエラーを出すためset -uを用いる。そして、パイプの途中でエラーが発生した場合にも終了ステータスを正しく返すためにset -o pipefailを使う。これらにより、バグの発見と修正が容易になり、スクリプトの安全性と信頼性が向上する。

エラー処理を実装する Link to this heading

エラーが発生した場合に、適切なエラーメッセージを表示して処理を終了するようにしよう。さもなければ、エラーが発生してもスクリプトが正常終了するかのように振る舞い、問題の特定が困難になってしまう。スクリプトにエラー処理を実装して、予期せぬ問題を捕捉し、ユーザーに意味のあるエラーメッセージを提供しよう。

コマンドライン引数をチェックする Link to this heading

必要な引数が指定されているかコマンドライン引数をチェックし、不足している場合はエラーメッセージを表示して終了するようにしよう。そうしないと、必要な引数が指定されていない場合でも、スクリプトが実行され、意図しない動作が発生してしまう。

getoptsコマンドを使用して、スクリプトのオプションと引数を適切に処理しよう。getoptsを使用することで、オプションや引数の処理が簡潔かつ安全に行える。エラーチェックも自動で行われる。

ユーザー入力は予測できないことが多いため、処理前のバリデーションが不可欠である。これは、スクリプトのエラーやセキュリティの脆弱性を防ぐのに役立つ。条件文や正規表現を使って、入力が予想されるパターンや値であるかどうかをチェックしよう。

なお、コマンドライン引数を使用すると、ユーザーはスクリプトに直接入力を渡せるため、より柔軟で使いやすいスクリプトになる。よって、コマンドライン引数は活用しよう。

変数名と関数名を説明的な名前にする Link to this heading

変数名や関数名は、その目的を明確に示す名前を選ぼう。これにより、他の開発者がコードを理解しやすくなり、保守性が向上する。また、誤解やバグの発生を減らせる。とくに、1文字の変数や、混乱を招くような略語の使用は避けよう。

また、一貫した命名規則を使用しよう。シェルスクリプトの命名規則を統一して、識別しやすく、保守しやすいようにしよう。

なるべく関数を使う Link to this heading

関数は、スクリプト内で複数回呼び出せる、再利用可能なコードブロックである。関数を使用することで、コードの再利用性を高め、重複を減らし、スクリプトをより効率的でメンテナンスしやすくできる。とくに、繰り返し使用するコードは関数にまとめよう

ローカル変数を活用する Link to this heading

とくに関数では、ローカル変数を使おう。さもなくば、変数のスコープが不明確になり、グローバル変数への誤ったアクセスや予期しない変更が発生してしまう。

たとえば、次のスクリプトでは、変数messageがローカル変数でないため、print_message関数によって変数messageが意図せず定義され、変更されてしまう。

sample.sh
#!/bin/bash
set -euo pipefail

function print_message {
    message="Hello, $1"
    echo "$message"
}
name="Alice"
print_message "$name"
echo "$message"  # 本来は定義されていない変数として扱われるはず

よって、このスクリプトは問題なく実行できてしまう。

$ sh sample.sh
Hello, Alice
Hello, Alice

このような事態を防ぐため、関数内の定数はローカル変数にしよう。

#!/bin/bash
set -euo pipefail

function print_message {
-   message="Hello, $1"
+   local message="Hello, $1"
    echo "$message"
}
name="Alice"
print_message "$name"
echo "$message"  # 本来は定義されていない変数として扱われるはず

これにより、関数とメインコードの変数の共有化を防げる。

$ sample.sh
Hello, Alice
sample.sh: line 10: message: unbound variable

定数はreadonlyにする Link to this heading

readonlyキーワードを使用して定数を定義し、値が変更されないようにしよう。 変更されるべきでない値が変更されてしまうと、意図しない動作が生じてしまう。

また、定数と変数がわかるように命名しよう。たとえば、定数をUPPER_SNAKE_CASE、変数をlower_snake_caseで命名すると、定数と変数の違いがわかりやすくなる。

ダブルクオートで変数を展開する Link to this heading

変数を展開する際には、ダブルクォート(")を使用しよう。ダブルクォートを使用することで、変数内の空白文字や特殊文字が適切に扱われ、意図しない動作を防げる。たとえば、以下のように変数を展開している場合を考える。

filename="file with spaces.txt"
rm $filename

上記のスクリプトは、空白文字を含むファイル名を正しく扱えず、エラーが発生する。これを避けるためには、ダブルクォートを使用して変数を展開する。

filename="file with spaces.txt"
rm "$filename"

コメントでコードを説明する Link to this heading

コードの目的や動作を説明するコメントを適切に記述しよう。コメントを使用して、スクリプトの目的と、コードの複雑な部分を説明しよう。

スクリプトの冒頭には、目的、使用方法、依存関係の概要を説明するヘッダーコメントを付けよう。また、複雑なコードやわかりにくいコードを説明するために、明確で簡潔なコメントを付けよう。スクリプトに適切に配置されたコメントは、貴重な文脈を提供し、コードの目的や機能を他の人が理解するのに役立つ。

ただし、過剰なコメントや、明らかなコードを説明するためにコメントを使用することは避けなければならない。良いコードは、自分で説明できるものであるべきである。

インデントをそろえる Link to this heading

関数や制御構造(if、while、forなど)の内部はインデントし、ネストされた構造を明確にしよう。スクリプトの構造を把握しやすくするために、インデントの幅はスクリプト内で統一しよう

また、異なる処理やセクションの間には改行を入れ、コードの見た目を整え、可読性を向上させるとより良い。

Licensed under CC BY-NC-SA 4.0
最終更新 5月 21, 2023
Hugo で構築されています。
テーマ StackJimmy によって設計されています。