VSCode 上でシェルスクリプトのまずい書き方を検出してみよう (with ShellCheck on Docker)

UNIX/Linux での作業の効率化に欠かせないシェルスクリプトですが、手軽に書ける反面、様々な罠があることでも知られています。

そんな罠にはまらないよう、シェルスクリプトの解析ツール「ShellCheck」を使ってチェックしましょう、というお話です。

ShellCheck とは?

静的解析によりシェルスクリプトの問題点を分析し、より良い書き方を提案してくれる CLI の Linter ツールです。

構文の問題点やバグだけでなく、推奨されない書き方も教えてくれます。

シェルスクリプトの初心者・上級者に関わらず、入れておくだけで安心してスクリプトを書けるようになります。

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

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

  • クォーティング処理に関する指摘:
    • ダブルクォーテーションで囲わずに変数展開している場合など。
  • 条件判定の書き方に関する指摘:
    • 意図しない判定ミスが起きそうな書き方など。
  • 初心者がやりがちなミスの指摘:
    • 変数定義のイコールの前後にスペースがある場合など。
  • 危険な書き方の指摘:
    • 破壊的な rm 実行など。
  • ポータビリティ(実行環境に依存しない書き方)に関する指摘:
    • 一行目のシバンに #!/bin/sh と書いてるのに bash 依存のコマンドを書いている場合など。

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

ShellCheck を入れてみよう

この記事では Visual Studio CodeShellCheck 連携プラグイン により ShellCheck を呼び出し、コーディングの際にリアルタイムにチェックできる環境の作り方を紹介します。

尚、ShellCheck の導入は Docker イメージとして提供されているものを使います。そのため事前に Docker のインストール が必要です。

なぜ ShellCheck を直接インストールせずに Docker イメージを使うの?

ShellCheck は brew や yum や apt-get などのパッケージ管理ツールでシステムに直接インストールすることもできます。

しかし、環境によってビルドにめちゃくちゃ時間がかかります。macOS では1時間ほど要することもあるようです。Docker イメージならビルドなしですぐ使えます。

もし時間がかかってもシステムに直接インストールしたい場合は 公式のインストール方法 を参照してみてください。

VSCode に ShellCheck を導入する手順

1. ShellCheck の Docker イメージを取得する

docker pull koalaman/shellcheck:v0.7.1

これだけでコマンドラインで ShellCheck を使えるようになります。

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

cd /path/to
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:v0.7.1 myscript.sh

2. VSCode から実行するためのキッカースクリプトを作る

VSCode 上で shellcheck を使うには docker run コマンドなしで実行できるようにする必要があります。

そのため、公式で説明されているように以下のようなキッカースクリプトを用意しておきます。

#!/bin/bash

exec docker run --rm -i -v "$PWD:/mnt:ro" \
  koalaman/shellcheck:v0.7.1 "$@"

作成したスクリプトは chmod +x コマンドで実行権限のパーミッションを付与しておきます。スクリプトはどこに置いてもいいですが、ファイルパスは控えておいてください。

3. VSCode に拡張機能をインストールする

次に VSCode に ShellCheck を動かすための拡張機能「vscode-shellcheck」をインストールします。

インストールできたら VSCode の設定を開き、shellcheck.executablePath の設定に先ほど作成したキッカースクリプトのパスをフルパスで指定します。(システムに ShellCheck をインストールした場合はこの設定は不要です。)

VSCode 上の設定例

以上で導入完了です。

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

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

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

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

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

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

メッセージの末尾にある 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 プラグイン と併用することで更に高速化が見込めます。