0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

手持ちのシェルスクリプト25本に ShellCheck を掛けたら、24件中22件が同じ指摘でした

0
Last updated at Posted at 2026-09-11

コメントで「手持ちのスクリプト全部に 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つ抜けていたことです。

  1. 書き直して消せないか(名前、書き方、慣習)
  2. 消すと検証の意味が失われるか
  3. 失われるなら抑制する。理由を添えて

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 が黙る条件を確かめる前に、抑制の書き方を説明していました。

手順

教えてもらった形をそのまま書いておきます。

  1. shebang を書く(POSIX sh なら #!/bin/sh、Bash なら #!/bin/bash)
  2. ShellCheck で検査する
  3. 指摘が出たら、まず書き直して消せないかを試す
  4. 消すと意味が失われるものだけ、理由を添えて抑制する
  5. 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まわりのことを書いています。

0
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?