実行前に Linux Bash スクリプトの構文を検証する方法

Linux Bash スクリプトのバグやタイプミスは、スクリプトを実行するとひどい事態を招く可能性があります。スクリプトを実行する前に構文を確認する方法をいくつかご紹介します。

厄介なバグ

コードを書くのは難しいことです。正確に言うと、バグのない、複雑ではないコードを書くのは難しいことです。また、プログラムやスクリプトのコード行数が多いほど、バグが含まれる可能性が高くなります。

プログラミングで使用する言語がこの問題に直接影響します。アセンブリ言語でプログラミングするのは C 言語でプログラミングするよりもはるかに難しく、C 言語でプログラミングするのは Python でプログラミングするよりも困難です。プログラミングで使用する言語のレベルが低いほど、自分で行う作業が多くなります。Python は組み込みのガベージコレクション ルーチンを利用できますが、C 言語とアセンブリ言語は利用できません。

Linux シェル スクリプトの作成には独自の課題があります。C 言語のようなコンパイル言語では、コンパイラと呼ばれるプログラムがソース コード (テキスト ファイルに入力する人間が判読できる命令) を読み取り、バイナリ実行可能ファイルに変換します。バイナリ ファイルには、コンピュータが理解して実行できる機械語命令が含まれています。

コンパイラは、読み取り、解析しているソース コードが言語の構文やその他のルールに従っている場合にのみバイナリ ファイルを生成します。予約語 (言語のコマンド ワードの 1 つ) または変数名を誤って記述すると、コンパイラはエラーをスローします。

たとえば、言語によっては使用する前に変数を宣言する必要がありますが、他の言語ではそれほどうるさくありません。作業している言語で変数を宣言する必要があるのに忘れた場合、コンパイラは別のエラー メッセージをスローします。これらのコンパイル時のエラーは煩わしいですが、多くの問題を検出して対処を強制します。ただし、構文エラーのないプログラムがあるからといって、バグがないという意味ではありません。そんなことはありません。

論理的な欠陥によるバグは通常、発見するのがはるかに困難です。プログラムに 2 と 3 を足すように指示したのに、実際には 2 と 2 を足すようにしたかった場合、期待した答えは得られません。しかし、プログラムは指示されたとおりに実行されています。プログラムの構成や構文には問題ありません。問題はあなたにあります。必要なことを実行しない、形式の整ったプログラムを作成しました。

テストは難しい

単純なプログラムであっても、徹底的にテストするには時間がかかります。数回実行するだけでは十分ではありません。コード内のすべての実行パスをテストして、コードのすべての部分が検証されるようにする必要があります。プログラムが入力を求める場合は、許容できない入力も含めて、すべての条件をテストするのに十分な範囲の入力値を提供する必要があります。

より高度な言語では、単体テストと自動化テストにより、徹底的なテストを管理可能な作業にすることができます。したがって、バグのない Bash シェル スクリプトを作成するために使用できるツールはあるのでしょうか?

答えは「はい」で、Bash シェル自体も含まれます。

Bash を使用してスクリプトの構文を確認する

Bash の-n (noexec) オプションは、Bash にスクリプトを読み取って構文エラーをチェックし、スクリプトを実行せずに指示します。スクリプトの目的にもよりますが、これはスクリプトを実行して問題を探すよりもはるかに安全な場合があります。

確認するスクリプトを次に示します。複雑ではなく、主にifステートメントのセットです。月は数字で表される入力を求めて受け入れます。スクリプトは、月がどの季節に属するかを判断します。当然のことながら、ユーザーが入力しなかったり、数字ではなく文字などの無効な入力をした場合には機能しません。

#! /bin/bash
read -p "Enter a month (1 to 12): " month
# did they enter anything?
if [ -z "$month" ]
then
echo "You must enter a number representing a month."
exit 1
fi
# is it a valid month?
if (( "$month" < 1 || "$month" > 12)); then
echo "The month must be a number between 1 and 12."
exit 0
fi
# is it a Spring month?
if (( "$month" >= 3 && "$month" < 6)); then
echo "That's a Spring month."
exit 0
fi
# is it a Summer month?
if (( "$month" >= 6 && "$month" < 9)); then
echo "That's a Summer month."
exit 0
fi
# is it an Autumn month?
if (( "$month" >= 9 && "$month" < 12)); then
echo "That's an Autumn month."
exit 0
fi
# it must be a Winter month
echo "That's a Winter month."
exit 0

このセクションでは、ユーザーが何かを入力したかどうかを確認します。$month変数が未設定かどうかをテストします。

if [ -z "$month" ]
then
  echo "You must enter a number representing a month."
  exit 1
fi

このセクションでは、1 から 12 までの数字を入力したかどうかを確認します。また、数字ではない無効な入力もトラップします。これは、文字や句読点記号は数値に変換されないためです。

# is it a valid month?
if (( "$month" < 1 || "$month" > 12)); then
  echo "The month must be a number between 1 and 12."
  exit 0
fi

他のすべての If 節は、$month変数の値が 2 つの値の間にあるかどうかを確認します。その場合、月はその季節に属します。たとえば、ユーザーが入力した月が 6、7、または 8 の場合、それは夏の月です。

# is it a Summer month?
if (( "$month" >= 6 && "$month" < 9)); then
echo "That's a Summer month."
exit 0
fi

例を実際に試してみたい場合は、スクリプトのテキストをエディタにコピーして貼り付け、「seasons.sh」という名前で保存します。次に、chmodコマンドを使用してスクリプトを実行可能にします。

chmod +x seasons.sh

スクリプトは次の方法でテストできます。

  • 入力をまったく行わない。
  • 数値以外の入力を提供する。
  • 1 から 12 までの範囲外の数値を提供する。
  • 1 から 12 までの範囲内の数値を提供する。

いずれの場合も、同じコマンドでスクリプトを開始します。唯一の違いは、ユーザーがスクリプトによって昇格されたときに提供する入力です。

./seasons.sh

期待どおりに機能しているようです。Bash にスクリプトの構文をチェックさせましょう。-n (noexec) オプションを呼び出して、スクリプトの名前を渡すことでこれを行います。

bash -n ./seasons.sh

これは「ニュースがないのが良いニュース」というケースです。Bash がコマンド プロンプトに何も返さずに終了するのは、すべてが正常であることを示しています。スクリプトを改ざんしてエラーを発生させます。

最初のif節からthenを削除します。

# is it a valid month?
if (( "$month" < 1 || "$month" > 12)); # "then" が削除されました
echo "The month must be a number between 1 and 12."
exit 0
fi

最初にユーザーからの入力なしでスクリプトを実行し、次にユーザーからの入力ありで実行してみましょう。

./seasons.sh

スクリプトが最初に実行されたとき、ユーザーは値を入力しないため、スクリプトは終了します。改ざんしたセクションには到達しません。スクリプトは Bash からのエラー メッセージなしで終了します。

スクリプトが 2 回目に実行されたとき、ユーザーは入力値を提供し、最初の if 節が実行されてユーザーの入力が正気かどうかがチェックされます。これにより、Bash からのエラー メッセージがトリガーされます。

Bash はスクリプトのロジックには関与しないため、その節の構文とそのほかのすべてのコード行をチェックすることに注意してください。Bash がスクリプトをチェックするとき、ユーザーは数字を入力するよう求められません。これは、スクリプトが実行されていないためです。

スクリプトの実行パスが異なる場合でも、Bash が構文をチェックする方法には影響しません。Bash はスクリプトの上から下までシンプルかつ методи的に処理し、すべての行の構文をチェックします。

ShellCheck ユーティリティ

Unix の全盛期に C ソース コードをチェックするツールとして名付けられたリンターは、プログラミング エラー、スタイル エラー、言語の疑わしい使用や不審な使用を検出するために使用されるコード分析ツールです。リンターは多くのプログラミング言語で使用可能であり、うるさいことで有名です。リンターが見つけたものがすべてバグというわけではありませんが、注意すべき点はすべてリンターが指摘します。

ShellCheck は、シェル スクリプト用のコード分析ツールです。Bash 用のリンターのように動作します。

スクリプトの欠落しているthen予約語を元に戻して、別のことを試してみましょう。最初のif節から開始角カッコ「[」を削除します。

# did they enter anything?
if -z "$month" ] # 開始角カッコ「[」が削除されました
then
echo "You must enter a number representing a month."
exit 1
fi

Bash を使用してスクリプトをチェックしても、問題は見つかりません。

bash -n seasons.sh

./seasons.sh

しかし、スクリプトを実行しようとするとエラー メッセージが表示されます。そして、エラー メッセージにもかかわらず、スクリプトは実行を続行します。これが一部のバグが非常に危険な理由です。スクリプトの後半で実行されるアクションがユーザーからの有効な入力に依存している場合、スクリプトの動作は予測不可能になります。データが危険にさらされる可能性があります。

Bash の-n (noexec) オプションがスクリプトのエラーを見つけない理由は、開始角カッコ「[」が[という外部プログラムであるためです。Bash の一部ではありません。これはtestコマンドを使用する略語です。

Bash はスクリプトを検証するときに外部プログラムの使用をチェックしません。

ShellCheck のインストール

ShellCheck はインストールが必要です。Ubuntu にインストールするには、次のように入力します。

sudo apt install shellcheck

Fedora に ShellCheck をインストールするには、このコマンドを使用します。パッケージ名は小文字と大文字が混在していますが、ターミナル ウィンドウでコマンドを実行するとすべて小文字になります。

sudo dnf install ShellCheck

Manjaro および同様の Arch ベースのディストリビューションでは、pacmanを使用します。

sudo pacman -S shellcheck

ShellCheck の使用

スクリプトで ShellCheck を実行してみましょう。

shellcheck seasons.sh

ShellCheck は問題を発見して報告し、詳細情報のためのリンクのセットを提供します。リンクを右クリックして表示されたコンテキスト メニューから「リンクを開く」を選択すると、リンクがブラウザで開きます。

ShellCheck はそれほど深刻ではない別の問題も見つけます。緑色のテキストで報告されます。これはエラーではなく警告であることを示しています。

エラーを修正して、欠落している「[.」を置き換えます。1 つのバグ修正戦略は、優先順位の高い問題を最初に修正し、警告などの優先順位の低い問題を後で修正することです。

欠落している「[」を置き換えて、もう一度 ShellCheck を実行しました。

shellcheck seasons.sh

ShellCheck からの唯一の出力は前回の警告を参照しているため、問題ありません。修正が必要な優先順位の高い問題は何もありません。

警告は、-r (as-is として読み取る) オプションなしでreadコマンドを使用すると、入力内のバックスラッシュがエスケープ文字として扱われることを示しています。これは、リンターが生成できる細かすぎる出力のタイプの良い例です。私たちのケースでは、ユーザーはそもそもバックスラッシュを入力すべきではありません。数字を入力してもらう必要があります。

このような警告は、プログラマーの判断を必要とします。修正する努力をするか、そのままにしておくか。2 秒で修正できます。また、警告が ShellCheck の出力を乱雑にするのを防ぐことができるため、アドバイスに従うことができます。readコマンドのフラグに「r」を追加して、スクリプトを保存します。

read -pr "Enter a month (1 to 12): " month

もう一度 ShellCheck を実行すると、健全であることがわかります。

ShellCheck はあなたの友人です

ShellCheck は、さまざまな問題を検出し、報告し、アドバイスすることができます。検出できる問題のタイプを多数示す、悪いコードのギャラリーをご覧ください。

無料で高速であり、シェル スクリプトの記述の多くの苦労を取り除きます。気に入らない点はないでしょう。