こんにちは。サーバーサイドエンジニアの伊藤です。
この記事は Enigmo Advent Calendar 2021 の 14 日目の記事です。
みなさんはシェルスクリプトを実装する機会はどのくらいの頻度でありますでしょうか?
私は社内ツールや個人で利用するちょっとしたツールを作成する際に、シェルスクリプトを実装することがあります。 とはいえ、普段の業務では Ruby on Rails を用いて実装をすることが多いので、 シェルスクリプトを実装する機会自体はそれほど多いものではありません。
その為、シェルスクリプト実装時に割と基本的な罠に陥ってしまうことがありました。
そこでこの記事ではシェルスクリプトの実装に潜む 4 つの罠を共有したいと思います。
- 1. shebang の記載漏れ
- 2. 適切な set options を指定しない
- 3. 1行で宣言と代入を行う
- 4. Double quotes で変数を囲わない
- shellcheck とは?
- 参考
1. shebang
の記載漏れ
# bad echo "FOO" # good #!/usr/bin/env bash echo "FOO"
ファイルの 1 行目に shebang
を記載することで、プログラムを実行するシェルを明示的に指定することが推奨されます。
というのも、シェルによってはサポートされている機能が異なることがあるため、
スクリプトを実行するシェルを明示的に指定することで意図しない挙動を避けることができます。
ここで 1 つの疑問が発生しました。
シェルスクリプトにおいてshebang
の記述は下記の通り複数ありますが、好ましい記述方はどれなのでしょうか?
#!/bin/bash #!/usr/bin/env bash
一般的には#!/usr/bin/env bash
の形式を利用することが推奨されるようです。
絶対パス等で指定をした場合、実行環境によっては指定の場所に実行可能なコマンドが存在しない可能性が考えられます。
上記の方法で記載すると、 $PATH
から実行可能なコマンド(この場合 bash
)を捜査してくれるのでポータビリティという点でメリットがあります。
さて、少し話が逸れてしまったので閑話休題、 2 つ目の罠に話を移します。
2. 適切な set options
を指定しない
# bad #!/usr/bin/env bash echo "FOO" # good #!/usr/bin/env bash set -eu echo "FOO"
-e (errexit)
こちらのオプションを指定することで、シェルスクリプトの途中でエラーが発生した場合その場で直ちにスクリプトを終了します。
大抵のケースではエラーが発生した場合、その場でスクリプトを終了したいことが多いと思いますので有効にしておくことが推奨されます。
ちなみに、エラーを許容したい場合には下記のように記述することで該当のコマンドのみエラーを許容することが可能です。
command1 || true echo "This line is executed anyway"
-u (nounset)
こちらは変数宣言に関するオプションです。
デフォルトでは宣言していない変数を利用した場合、空文字として扱われてしまいます。 その為、typo 等で未宣言の変数を利用したとしても特にエラーにはならないので、意図しない挙動を招くことがあります。 そういったケースを避けるためこちらのオプションを有効にしておくことをお勧めします。
3. 1行で宣言と代入を行う
# bad export BAR="$(command2)" # good BAR="$(command2)" export BAR
上記のスクリプトでは command2
の結果が無視されてしまいます。
set -e
を指定している場合、本来エラーが発生した箇所でスクリプトは終了します。
ただ、上記のケースでは command2
の結果に関わらず後続の処理が実行されてしまいます。
その為、代入と宣言の箇所を分けることが推奨されます。
4. Double quotes で変数を囲わない
# bad file="sample file.txt" ls $file DIR="$(dirname $0)/.." # good file="sample file.txt" ls "$file" DIR="$(dirname "$0")/.."
double quotes を利用しないと該当の文字列中にスペースやタブ等を含むケースで、意図しない結果を招くことがあります。
例えば、上記のようにファイル名にスペースを含むケースだと、double quotes で該当の変数を囲わない場合スペースによって sample
と file.txt
という 2 つの単語に分割されてしまいます。
結果として、 sample file.txt
というファイルではなく sample
と file.txt
を引数に ls
コマンドが実行されてしまいます。
このような、意図しない挙動を避ける為、シェルスクリプトでは変数を double quotes で囲むことが推奨されます。
さて、ここまでシェルスクリプトの実装に潜む 4 つの罠について記載してきました。 シェルスクリプトを実装したことがある開発者にとっては、これらのどれもが一度は経験してきた問題なのではないでしょうか?
これらの問題が一般的ではある一方で、全てをマニュアルで人間が気をつけることは難しく労力を要する作業かと思います。 ということで、 shellcheck というツールを利用することをお勧めします。
shellcheck
とは?
シェルスクリプト用の静的 Lint ツールです。 こちらを利用することで、上記で説明してきたエラー(罠)を簡単に検知することが可能です。
コマンドを実行すると、下記の通り問題のある箇所を明瞭に指摘してくれます。
>>> shellcheck ./tmp.sh In ./tmp.sh line 1: set -eu ^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive. In ./tmp.sh line 5: echo ${baz} ^----^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: echo "${baz}" For more information: https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y... https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
さて、これだけでも記憶を頼りにマニュアルで全てを確認することに比べ、 簡便になったとは思います。
しかし、こちらのコマンドを修正の度に毎度手動実行することは非常に骨の折れる作業であり、非人間的です。 ということで、CI として設定して自動で実行しましょう。
今回はサンプルとして昨年の Advent Calendar 2020 の記事に記載した自作の OATHTOOL ラッパーコマンド に shellcheck
を CI として追加してみます。
こちらのレポジトリは GitHub 上に存在するので、今回は GitHub Actions を用いて設定していきます。
設定ファイルは下記。
# .github/workflows/main.yml name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: shellcheck: name: Shellcheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master with: scandir: './bin'
shellcheck は既に GitHub Action としてマーケットプレースに存在するので、設定自体は簡単です。 上記のように設定ファイルを 1 つ追加するだけで設定は完了です。
無事に CI として shellcheck
が走るようになりました。
最後まで読んでいただきありがとうございました。
明日の記事の担当は検索エンジニアの伊藤 明大さんです。お楽しみに。
参考
- What is the preferred Bash shebang?(最終アクセス: 2021/12/05)
- set man page(最終アクセス: 2021/12/05)
- koalaman/shellcheck(最終アクセス: 2021/12/05)
株式会社エニグモ すべての求人一覧