ShellCheck:シェルスクリプトのバグを見つける解析ツール
(shellcheck.net)- シェルスクリプトでよくあるミスをWeb上ですぐに検査できる ShellCheck ツールで、貼り付けたコードの問題をエディタ下部の出力で確認できる
- ローカル環境では
cabal、apt、dnf、pkg、brew installなどでインストールでき、パッケージマネージャベースで利用できる - 例では、
shshebang の移植性に関する警告、意味的な問題、クォート処理の誤りなど、実際のスクリプトで見落としがちな点を示している - GPLv3 ライセンスの自由ソフトウェアで、GitHub、Wiki、主要エディタの統合リンターとしても提供されている
- CodeClimate、Codacy、CodeFactor で GitHub リポジトリの自動検査に活用でき、ShellCheck 自体は Haskell で書かれている
シェルスクリプト解析機能
- ShellCheck はシェルスクリプトのバグを見つける解析ツールである
- Webサイトにスクリプトを貼り付けると、エディタ下部の出力ウィンドウで検査結果をすぐに確認できる
- 含まれる例は、複数の種類の問題を検出できることを示している
- 一般的なスクリプトに潜むさまざまな問題
- shebang が
shの場合に発生する移植性に関する警告 - より高いレベルの意味的な問題
- さまざまなクォート処理の問題
インストール、ライセンス、統合
- ローカルインストールは
cabal、apt、dnf、pkg、brew installで可能 - GPLv3 ライセンスの自由ソフトウェアである
- ドキュメントは ShellCheck Wiki で確認できる
- GitHub で公開されており、Webサイトのコード もあわせて公開されている
- ディストリビューションまたはパッケージマネージャ向けのパッケージがすでに提供されている
- 主要エディタで統合リンターとして利用できる
- CodeClimate、Codacy、CodeFactor で GitHub リポジトリの自動検査に活用できる
- ShellCheck は Haskell で書かれている
1件のコメント
Hacker News の意見
私が使っているコツはこうです。shebang にはほぼ常に
-u(nounset)を入れて、宣言されていない変数をエラーにするのがよいと思います。例外としてよく遭遇するのは、"${arr[@]}"構文で空の配列を展開すると unbound と見なされる場合です。-n(noexec)はコマンドの実行を防ぐので、貧者の dry-run のように使えます。-e(errexit)も便利ですが、実質的に失敗した「そのものの」コマンドだけが終了を引き起こす点に注意が必要なので、個人的には避けて、コマンドの後ろに|| fail "..."をよく付けるほうを好みます。"${arr[@]}"の問題は bash 3 以前にだけあり、bash 4 以降では、変数が本当に定義されていない場合でも[@]は unbound variable を投げません。それでも macOS がいまだにデフォルトで bash v3 をインストールし、自動更新もしないため問題として残っています。bash 3 の最後のリリースが20年前というのは本当にどうかしています。空配列の展開で unbound になるのは、
${var+alter}展開で回避できます:echo "${arr+${arr[@]}}"./my_script.shではなくbash my_script.shのようにインタプリタを明示して実行すると、オプションが適用されないためです。実行ビットを設定したくなくてこうする人はかなり多く、ときには理解不足が理由の場合もあります。そのため、shebang の次の最初の行に
set -euo pipefailのようなsetを書け、という助言が一般的ですし、#!/usr/bin/env bashのように shebang が追加引数を扱いにくい場合にも役立ちます。-uを shebang に入れる特別な理由があるのか気になります。set -uではなく、なぜ shebang なのでしょうか?Bash では
"${arr[@]}"はうまく動くように見えます。他のコメントでも触れられているように、最近の Bash ではさらに改善されており、問題があるのは<= 4.3だけのようです: https://news.ycombinator.com/item?id=38397241たとえば
bash -uc 'unset x; echo "=> ${x[@]}"'とbash -uc 'x=(); echo "=> ${x[@]}"'は空値として通りますが、bash -uc 'x=(); echo "=> ${x[0]}"'はbash: x[0]: unbound variableになります。Zsh は最初の例を嫌いますが、どちらもbash -uc 'unset x; echo "=> ${x[@]:-null}"'のようなデフォルト値展開をサポートしているはずです。-eは関数と絡むと非常に紛らわしいので、年を経るほど好きではなくなっています。-eの問題は-o pipefailでうまく扱えますし、これは昨年から POSIX に含まれています。trapも一緒に使う必要がありました。trapは優れたスクリプティング機能ですが、体感としては十分に語られていないように思います。最近、算術展開が原因で shell script に権限昇格の脆弱性を見つけました。https://research.nccgroup.com/2020/05/12/shell-arithmetic-ex... にあるものと似たタイプです。
たとえば
$((1 + ENV_VAR))は、$ENV_VARを制御できるとコードインジェクションが可能です。残念ながら ShellCheck は、少なくともデフォルト設定ではこれを検出できませんでした。ただし、少しでもセキュリティ上重要なものを実装するなら、そもそも shell を使うべきではありません。ShellCheck は本当に救世主です。以前、小さなラッパー https://github.com/jamespwilliams/strictbash を作りましたが、スクリプトの shebang として使えます。
スクリプト実行前に ShellCheck を走らせ、失敗があればそもそも実行されないようにし、bash の “strict mode” フラグもすべて設定します。参考: http://redsymbol.net/articles/unofficial-bash-strict-mode/
この話題は何度も出ています: https://news.ycombinator.com/from?site=shellcheck.net
最後の大きな議論は2021年で、301ポイント、コメント54件でした: https://news.ycombinator.com/item?id=27030504
少し前に、ビルドとデプロイのスクリプト、単一の本番サーバー用の bash スクリプトをいくつか Haskell の Turtle に置き換えた
重複を大きく減らせてよかったし、結果のコードもずっと短くなった。 https://hackage.haskell.org/package/turtle
私の理解では Turtle プログラムには現在ディレクトリが 1 つしかないため、特定のディレクトリで実行される必要がある並行タスクを動かすときに難しい。ロック、キュー、ワーカー方式で一部は解決したが、Turtle の現在ディレクトリが削除されて失敗し始めると、手に負えなくなった
その一方で typed-process は別プロセスを起動し、
cdする必要なく作業ディレクトリ内で実行できるので、大規模で複雑なワークフローに向いている。OverloadedStringsのサポートもよく、たいてい bash に入力していた内容をコピー&ペーストすればそのまま動く生文字列をソースコード内でより見やすくするために
interpolateパッケージとQuasiQuotesも使っているが、hlintと互換性がないので、文字列処理用の別パッケージを探してみるつもりあからさまな自己宣伝だが、コミット前、あるいは少なくともマージ前に すべての警告を直す という方針で、ShellCheck と複数のリンターを pre-commit 設定に入れている
ところが私のプロジェクトの shell の大半は
.gitlab-ci.ymlファイルの中に入っていて、検査しづらい。そこで自動で処理するラッパーを作った: https://pypi.org/project/glscpc/ShellCheck プロジェクトと少しの魔法を使って、ほぼ正確な行番号とともに ShellCheck の指摘を表示する
GitLab CI は使っていないが、Dockerfile、GitHub Actions、Justfile のように、本質的に shell script をインラインで入れるファイル形式をかなり多く使っている
ふつうは ShellCheck のためだけでも、数個のコマンドより複雑なものは別の shell script に切り出し、Dockerfile のインラインスクリプトから呼び出すようにしている。このパターンは、CI が GitHub Actions に縛られすぎないようにするうえでも役立つ
使用例は
.gitlab-ci.ymlを対象にする pre-commit フックで、設定例はここにある: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...script部分としてレンダリングする 前処理 を入れたかった利点は、依然としてすべてが gitlab-ci ジョブの中に自己完結している点だ。しかし GitLab CI runner 環境で shell のあらゆる奇妙さに対処するのがあまりに苦痛で中止し、今はすべてのジョブを Python script に移しているところ
bash language server もある: https://github.com/bash-lsp/bash-language-server/
よい。初めて実行してみた本番用の
/bin/shスクリプトで、すぐにいくつか学びがあったし、こういうスクリプトは 80 年代から触ってきたBash で書くには長すぎて、本来そうすべきではない状況なら、https://github.com/bach-sh/bach もおすすめ
ShellCheck は素晴らしいが、source/import の扱い は本当に苦痛。ShellCheck が悪いのではなく、
shが悪夢だからだ# shellcheck source=./deployment/deployment-example.envの後に. "${1}"を使う方法ただし複数の下位 shell script と source するファイルが増えてくると、なぜ苦痛なのかは理解できる