5 ポイント 投稿者 GN⁺ 2023-11-25 | 1件のコメント | WhatsAppで共有
  • シェルスクリプトでよくあるミスをWeb上ですぐに検査できる ShellCheck ツールで、貼り付けたコードの問題をエディタ下部の出力で確認できる
  • ローカル環境では cabalaptdnfpkgbrew install などでインストールでき、パッケージマネージャベースで利用できる
  • 例では、sh shebang の移植性に関する警告、意味的な問題、クォート処理の誤りなど、実際のスクリプトで見落としがちな点を示している
  • GPLv3 ライセンスの自由ソフトウェアで、GitHub、Wiki、主要エディタの統合リンターとしても提供されている
  • CodeClimate、Codacy、CodeFactor で GitHub リポジトリの自動検査に活用でき、ShellCheck 自体は Haskell で書かれている

シェルスクリプト解析機能

  • ShellCheck はシェルスクリプトのバグを見つける解析ツールである
  • Webサイトにスクリプトを貼り付けると、エディタ下部の出力ウィンドウで検査結果をすぐに確認できる
  • 含まれる例は、複数の種類の問題を検出できることを示している
    • 一般的なスクリプトに潜むさまざまな問題
    • shebang が sh の場合に発生する移植性に関する警告
    • より高いレベルの意味的な問題
    • さまざまなクォート処理の問題

インストール、ライセンス、統合

  • ローカルインストールは cabalaptdnfpkgbrew install で可能
  • GPLv3 ライセンスの自由ソフトウェアである
  • ドキュメントは ShellCheck Wiki で確認できる
  • GitHub で公開されており、Webサイトのコード もあわせて公開されている
  • ディストリビューションまたはパッケージマネージャ向けのパッケージがすでに提供されている
  • 主要エディタで統合リンターとして利用できる
  • CodeClimateCodacyCodeFactor で GitHub リポジトリの自動検査に活用できる
  • ShellCheck は Haskell で書かれている

1件のコメント

 
GN⁺ 2023-11-25
Hacker News の意見
  • 私が使っているコツはこうです。shebang にはほぼ常に -unounset)を入れて、宣言されていない変数をエラーにするのがよいと思います。例外としてよく遭遇するのは、"${arr[@]}" 構文で空の配列を展開すると unbound と見なされる場合です。
    -nnoexec)はコマンドの実行を防ぐので、貧者の dry-run のように使えます。-eerrexit)も便利ですが、実質的に失敗した「そのものの」コマンドだけが終了を引き起こす点に注意が必要なので、個人的には避けて、コマンドの後ろに || fail "..." をよく付けるほうを好みます。

    • "${arr[@]}" の問題は bash 3 以前にだけあり、bash 4 以降では、変数が本当に定義されていない場合でも [@] は unbound variable を投げません。
      それでも macOS がいまだにデフォルトで bash v3 をインストールし、自動更新もしないため問題として残っています。bash 3 の最後のリリースが20年前というのは本当にどうかしています。空配列の展開で unbound になるのは、${var+alter} 展開で回避できます: echo "${arr+${arr[@]}}"
    • shebang に Bash/shell のオプションを入れるな、というよくある助言もあります。誰かがスクリプトを ./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 を使ってみたが、結局やめて typed-process を選んだ
      私の理解では 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 に縛られすぎないようにするうえでも役立つ
    • ハイタッチ。私もごく最近、似たものを作った: https://gitlab.com/engmark/shellcheck-gitlab-ci-scripts-hook
      使用例は .gitlab-ci.yml を対象にする pre-commit フックで、設定例はここにある: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...
    • 本当に素晴らしい。以前、別の観点からこの問題を解こうとしたことがある。「通常の」shell script を受け取り、ビルド時にそのジョブの 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 するファイルが増えてくると、なぜ苦痛なのかは理解できる