VSCode に ShellCheck を導入してシェルスクリプトのまずい書き方を検出してみよう

普段の作業の効率化に欠かせないシェルスクリプトですが、変数の扱いや条件式のミスで、思わぬバグや危険な動作が起こることもあります。

そういったトラブルを未然に防ぐために、シェルスクリプトの解析ツール「ShellCheck」を使ってチェックする方法を紹介します。

ShellCheck とは?

ShellCheck とは、シェルスクリプトコードを静的解析によりチェックし、誤りや危険な書き方を教えてくれるツール(Linter)です。構文の問題点やバグだけでなく、推奨されない書き方も教えてくれます。

初心者はもちろん、普段からシェルスクリプトを書く開発者にも非常に役立ちます。

VSCode と連携させることで、スクリプトを書きながらリアルタイムにチェックすることもできます。

安心して品質の高いスクリプトを書くために、ほぼ必須なツールと言えるでしょう。

ShellCheck がチェックしてくれること

ShellCheck がチェックしてくれる内容は とても多岐に渡ります。例えば…

  • クォーテーションの取り扱いに関する指摘:
    • ダブルクォーテーションで変数を囲わずに展開している場合の指摘など。
    • 予期せぬ単語分割やパス名展開による事故を抑止する。
  • 危うい条件判定の書き方に対する指摘:
    • 意図しない判定ミスの抑止。
  • よくある文法ミスの指摘:
    • 変数定義のイコールの前後にスペースがある場合の指摘など。
  • 危険な書き方の指摘:
    • 破壊的な rm 実行の指摘など。
  • ポータビリティ(実行環境に依存しない書き方)に関する指摘:
    • 一行目のシバンに #!/bin/sh と書いてるのに bash 依存のコマンドを書いた場合など。

これだけでも心強いですが、上記はほんの一例です。

導入手順

1. ShellCheck をインストールする

Mac の場合:

brew install shellcheck

Windows の場合:

# chocolatey 環境の場合:
choco install shellcheck

# winget 環境の場合:
winget install --id koalaman.shellcheck

# scoop 環境の場合:
scoop install shellcheck

Ubuntu (Debian based distros) の場合:

sudo apt install shellcheck

もしくはシステムに直接インストールしたくない場合、Docker イメージとしても提供されています。

インストールが終わったら shellcheck コマンドが使用できるようになったことを確認します。

$ shellcheck --version
ShellCheck - shell script analysis tool
version: 0.10.0
license: GNU General Public License, version 3
website: https://www.shellcheck.net

例えば、./myscript.sh に置かれているスクリプトをチェックしたい場合は以下のように実行します。

$ shellcheck ./myscript.sh

2. エディターと連携させる

毎回コマンドを打つのは面倒なので、エディターに拡張機能を導入して連携させると便利です。

VSCode と連携させる場合は、ShellCheck の拡張機能 をインストールします。

以上で導入完了です。

適当にシェルスクリプトを書いてリアルタイムにチェックされるか確認してみる

下の図は適当にシェルスクリプトを書いてみたときの様子です。

ShellCheck により問題点が指摘されている様子

何やら4行目に波下線が出ています。これは検出された問題のあるコードの箇所を示しています。ウィンドウ下部の PROBLEMS の欄に、具体的な問題の内容が表示されています。

Double quote to prevent globbing and word splitting. shellcheck(SC2086)

一般的に変数を使うときは二重引用符 “” で囲うべき とされているので、その指摘となります。(囲わないと意図しない単語分割やグロブ展開が起きる危険性があります。)

# NG例(ダブルクォートなし)
echo $filename

# OK例(ダブルクォートあり)
echo "$filename"

メッセージの末尾にある SC2086 というのは ShellCheck の各チェック事項に対して割り当てられたコード番号で、このコード番号で調べると問題の詳細を知ることができます。例えば SC2086 の Wiki ページ を見ると直し方のコード例と共に、なぜこの書き方が問題なのか、解説を確認することができます。とても親切ですね。

まとめ

今回は二重引用符で囲うべきという指摘がされる例を挙げましたが、この他にも様々なことをチェックしてくれます。実際に ShellCheck を入れて書いてみるとその安心感が分かるかと思います。

シェルスクリプトには事故を引き起こしかねない危険な書き方のパターンが無数にありますが、すべて把握するのは上級者でも難しいため、このようなチェックツールの導入が現実的です。

ShellCheck が指摘してくれた問題点を直していくだけでベストプラクティスとされる書き方を反映したスクリプトを書くことができますし、上達にも繋がるのでおすすめです。

関連記事

CakePHP + MySQL アプリのテスト時間を 72% 削減した話

この記事は CakePHP Advent Calendar 2018 の23日目の記事です。

CakePHP アプリケーションのユニットテストでボトルネックになりがちなのがデータベースへのディスク I/O 時間ですが、そういったプロジェクトでは FriendsOfCake/fixturize プラグイン を導入すると大幅に高速化できる可能性があります。

実例として、テストに18分も掛かっていた MySQL を使ったプロジェクトありましたが、導入後なんと5分にまで短縮されました。(約 72% の高速化🎉)

結果として早いサイクルで CI を回せるようになり開発スピードも上がりました。

何がボトルネックになっていたか?

テストケースごとに実行されるテストデータのリロードクエリがボトルネックになっていました。リロードクエリとは TRUNCATE TABLE クエリと INSERT クエリのセットのことで、積み重なると高コストなクエリになります。

テストデータのリロード時間はテストケースが増えるほど支配的なものになる

CakePHP のユニットテストは、テストデータを Fixture クラスとして定義しておき、テストクラスの側で使用したい Fixture 名の一覧を記述します。

例として、テストクラスは以下のようになります。

<?php
namespace App\Test\TestCase;
use Cake\TestSuite\TestCase;

class ArticlesTest extends TestCase
{
    // 3テーブル分のテストデータの使用を宣言
    public $fixtures = [
        'app.articles',
        'app.comments',
        'app.users',
    ];

    public function testCase1() {
        // テストコード
    }

    public function testCase2() {
        // テストコード
    }
}

このテストを実行すると testCase1()testCase2() のテストケースの実行前に、それぞれ3テーブル分のリロードクエリが実行されます。同じように、このクラスに100個のテストケースがあったとしたらリロードクエリは最低で300回実行されます。

極端な例を挙げましたが、リロード回数はテスト数とフィクスチャ数の掛け算で増えていくため、簡単に無視できない I/O コストになることは想像に難くないと思います。

毎回テストデータをリロードする理由

無意味に何度もリロードしている訳ではありません。毎回リロードすることで、テストが書きやすくなるというメリットがあります。

テスト中にいくらテストデータを書き換えたとしても、次のテストでは元の状態にリセットされているので、テストケース間でテストデータの状態について副作用を気にする必要がなくなります。

この高速化プラグインがすること

FriendsOfCake/fixturize のソースを見ると分かりますが、このプラグインのすることは非常にシンプルです。上述の「毎回同じテストデータの状態でテストを開始したい!」という要求を満たしつつ、必要なときだけリロードするようにし、リロードクエリの実行回数を削減します。

具体的には、初回のフィクスチャロード後に CHECKSUM TABLE クエリ でテーブルのチェックサム値を取っておき、次以降のリロードは差分があったときだけ行うようにします。

チェックサム値に差分がなければテストデータが変更されていないということなので、リロードもスキップして問題ないということになります。

プラグインで高速化できる可能性のあるプロジェクト

ということで、以下に該当するプロジェクトであればプラグインを導入するだけで大幅にテスト時間を削減できる可能性があります。

  • CakePHP 3.x 製アプリ
  • MySQL / MariaDB / Percona のいずれかを使用している
  • テストケース数とテーブル数が多いプロジェクト
  • 複数テーブルを参照する結合的なテストをしているプロジェクト

また、もしこのプラグインが使えない環境のプロジェクトであっても、不要なロードを減らすというアプローチ自体は有効かもしれません。

おまけ: その他のアプローチ

他のアドベントカレンダーになりますが、素晴らしい記事を見かけましたのでそちらも紹介しておきます。

DB ストレージを tmpfs というオンメモリのファイルシステムに差し替え、高コストなディスク IO を無くすることで高速化するというアプローチです。私も手元のプロジェクトで試してみましたが、実際に 40% ほど高速化できました。FriendsOfCake/fixturize プラグイン と併用することで更に高速化が見込めます。