コメントで「手持ちのスクリプト全部に ShellCheck を掛けたらどうなりますか」と聞かれて、掛けてみました。
先に断っておくと、この記事に新しい発見はありません。ShellCheck の使い方も、どのシェルに対応しているかも、--help と公式サイトに書いてあります。書く前に読みました。
それでも書くのは、自分の25本を実際に掛けた結果の内訳が、自分にしか出せない数字だからです。指摘が何件出て、どこに偏るのか。そこだけを記録します。
検証環境は ShellCheck 0.9.0 / Ubuntu 24.04 です。
25本で24件。そのうち22件が同じ指摘でした。
22 SC2148 Tips depend on target shell and yours is unknown.
Add a shebang or a 'shell' directive.
1 SC2050 This expression is constant.
1 SC2034 i appears unused.
shebang がない、です。
直したら、2件になりました
22本すべてに #!/bin/bash を1行足して、もう一度掛けました。
合計 2 件
1 SC2050
1 SC2034
24件が2件になりました。
減った22件は、中身の問題ではありません。「どのシェル向けか分からないので、助言できない」という意味の指摘です。ShellCheck は shebang を見て、そのシェルの規約で検査します。書いていないと、そこで止まります。
**shebang を書くまで、ShellCheck はほとんど何も言えません。**検査を通したつもりで、検査が始まっていない状態になります。
shebang を書きたくない場合は、コメントで指定できます。
# shellcheck shell=bash
A=(alpha beta)
echo "${A[1]}"
これで通ります。1行目に置く必要があります。
残った2件
どちらも、こちらの書き方に対する指摘でした。ただし、**打つ手が違いました。**片方は書き直せば消え、もう片方は消せません。
SC2034:書き直せば消えます
n=0
for i in {1..3}; do n=$((n+1)); done
# ^-- SC2034 (warning): i appears unused.
ループ変数 i を使っていません。ブレース展開が効くかどうかを見るためのコードなので、i の中身に用がありません。
ここで抑制コメントを書こうとしたのですが、その前に変数名を _ にすれば済みました。
$ echo 'for _ in {1..3}; do n=$((n+1)); done' | shellcheck -s bash - ; echo $?
0
$ echo 'for i in {1..3}; do n=$((n+1)); done' | shellcheck -s bash -
SC2034 (warning): i appears unused.
_ は「使わない値を受ける」という慣習的な名前で、ShellCheck はこれを見逃します。抑制する前に、慣習に沿って書き直せないかを確かめるほうが先でした。
SC2050:これは消せません
if [ "abc" == "abc" ]; then echo "比較できた"; fi
# ^-- SC2050 (warning): This expression is constant.
比較の両辺が定数なので、常に真です。
こちらは書き直しで逃げられません。変数を経由すれば警告は消えますが、それだと「定数同士の比較で == が通るか」を見る検証コードでなくなります。
$ echo 'a=abc; if [ "$a" = "abc" ]; then echo ok; fi' | shellcheck -s bash - ; echo $?
0
警告は消えますが、確かめたかったものも一緒に消えています。
なので、ここは抑制します。
# shellcheck disable=SC2050 # シェルごとの == の扱いを見るための検証コード
if [ "abc" == "abc" ]; then echo "比較できた"; fi
直前の行に置くと、その次の行だけに効きます。ファイル全体に効かせたいなら1行目に置きます。理由をコメントで添えておかないと、あとで見たときに抑制した経緯が分かりません。
抑制は、最後の手段でした
2件を並べて分かったのは、抑制を書く前に確かめる段が1つ抜けていたことです。
- 書き直して消せないか(名前、書き方、慣習)
- 消すと検証の意味が失われるか
- 失われるなら抑制する。理由を添えて
SC2034 は1番で終わる話でした。SC2050 だけが3番まで来ます。
追記:SC2034 の扱いを直しました
公開後、コメントで _ の件を教えていただきました。
当初この記事では、2件とも「指摘は正しく、コードも意図どおりなので抑制を書く」と説明していました。**SC2034 については誤りです。**抑制は要りませんでした。
しかも記事の中で「抑制には理由を添えましょう」と書いています。理由を添える以前に、抑制する必要がなかったわけです。ShellCheck が何を見逃すかを確かめないまま、自分の書き方のほうを正当化していました。
上の節は、それを踏まえて書き直しています。
同じコメントで、seq が POSIX の標準コマンドではないことも教わりました。手元の macOS と Linux には入っていますが、厳密に POSIX の範囲で書くなら while に寄せるか、command -v seq で存在を確かめる必要があります。
n=0
i=0
while [ "$i" -lt 3 ]; do
i=$((i + 1))
n=$((n + 1))
done
dash / bash / zsh の3つで3回まわり、ShellCheck も無警告でした。{1..3} も seq も要りません。
この記事の検証コードは、そもそもブレース展開が dash で効かないことを見るためのものなので {1..3} のままにしています。実務で回数を数えるループは、while に寄せます。
zsh には掛かりませんでした
対応シェルは --help に書いてあります。
$ shellcheck --help | grep SHELLNAME
-s SHELLNAME --shell=SHELLNAME Specify dialect (sh, bash, dash, ksh)
ここに zsh はありません。読めば分かることですが、掛けるまで自分は読んでいませんでした。
$ shellcheck script.sh
SC1071 (error): ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!
zsh は対象外です。#!/bin/zsh と書いたファイルは、1行目で検査が終わります。上の25本のうち2本が zsh で、どちらもこれだけでした。
ただ、シェルを指定して強制すると、意味のある結果になります。
$ shellcheck -s bash script.sh
line 32: for f in $DROP SC2128 (warning) 配列を添字なしで展開している
line 42: for f in $DROP SC2128 (warning) 同上
line 56: ${(j:,:)DROP} SC2296 (error) ( で始まる展開は書けない
3件とも zsh 固有の記法でした。
for f in $DROP は、zsh では配列が展開されて要素ごとにまわりますが、bash では最初の1要素だけになります。${(j:,:)DROP} は zsh の結合記法で、bash では bad substitution です。
**zsh スクリプトに -s bash を掛けると、bash に移したときに壊れる場所の一覧が出ます。**zsh の検査はできませんが、移植の検査にはなります。
コメントディレクティブでも同じことができました。
#!/bin/zsh
# shellcheck shell=bash
zsh として実行しつつ、bash の規約で検査する形です。移植する予定があるなら、これを最初から入れておくと差分が見えます。
何が分かったか
24件のうち22件が shebang だった、というのが結論の全部でした。
SC2148 が何を意味するかは、ドキュメントに書いてあります。新しいのは、自分の場合それが22件だったという偏りのほうです。
掛ける前は、もっと散らばった結果を想像していました。クォート忘れ、変数の未定義参照、危険な rm。そういうものが並ぶと思っていました。
出たのは、検査が始まっていないという指摘だけでした。中身を見てもらう前の段階で止まっていたわけです。
そして、shebang を書いていなかったこと自体は、これより前にコメントで指摘されていました。人に言われたことと、機械が言うことが、同じでした。
先に ShellCheck を掛けていれば、人の手を煩わせずに済んだということです。さらに手前に戻せば、ドキュメントを読んでいれば掛ける前に分かっていました。順序を2つ飛ばしていたことになります。
残った2件の扱いでも、同じことをしています。ShellCheck が黙る条件を確かめる前に、抑制の書き方を説明していました。
手順
教えてもらった形をそのまま書いておきます。
- shebang を書く(POSIX sh なら
#!/bin/sh、Bash なら#!/bin/bash) - ShellCheck で検査する
- 指摘が出たら、まず書き直して消せないかを試す
- 消すと意味が失われるものだけ、理由を添えて抑制する
- POSIX sh のつもりなら、
dash script.shで実行して確かめる
1番を飛ばすと2番が動きません。今回のように、掛けた気になって終わります。
3番は、あとから足した段です。抑制から入ると、自分の書き方を正当化するほうに寄ります。
5番は、静的検査で出ないものを拾う段です。ブレース展開が効かない、$RANDOM が空になる、といった実行時の差は ShellCheck では出ません。逆に、zsh の3件は実行では出ませんでした。両側にあるので、両方やることになります。
CI に入れるなら、こう書けます。
shellcheck *.sh || exit 1
25本で63ミリ秒でした。
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。