michimani.net

shellcheck と shfmt を使ってシェルスクリプトをキレイにしてみる

2020-12-24

シェルスクリプトをキレイにするためのツールとして shellcheck と shfmt というものの存在を教えてもらったので、先日作った ACM の Certificate 発行スクリプトに対してそれらを実行し、キレイにしていきたいと思います。

目次

概要

shellcheck と shfmt というツールを使って、シェルスクリプトをキレイにします。ここで言う キレイに とは、文法上の問題をなくしたり、想定外の実行結果にならないようにしたり、見た目を整えたりすることを指します。

今回使う shellcheck と sh というツールについても簡単に紹介しておきます。

shellcheck

shellcheck (koalaman/shellcheck) は、対象のシェルスクリプトに対して文法上の問題や想定外な実行結果が起きないように、問題点を指摘してくれるツールです。指摘してくれるポイントとしては、 GitHub の README に次のように書かれています。

ざっくり翻訳すると

について指摘してくれます。

クライアントにインストールして使用することもできますし、 Web 上ではシェルスクリプトを貼り付けるだけでチェックしてくれます。

shfmt

shfmtmvdan/sh リポジトリに含まれるツールで、対象のシェルスクリプトをフォーマットしてくれる Go 製のツールです。

shellcheck で文法チェック

では、早速 shellcheck で文法チェックしてみます。

今回はローカルマシン (macOS) にインストールして実行してみます。

インストール

brew install でインストールできます。

$ brew install shellcheck
.
.
.
🍺  /usr/local/Cellar/shellcheck/0.7.1: 7 files, 9.0MB

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

実行

以前作成した ACM の Certificate を発行するスクリプト issue-acm-certificate.sh という名前で保存しておいて、そのファイルに対して shellcheck で文法チェックをかけます。

$ shellcheck issue-acm-certificate.sh

In issue-acm-certificate.sh line 5:
if [ $# != 3 ] || [ $1 = "" ] || [ $2 = "" ] || [ $3 = "" ]; then
                    ^-- SC2086: Double quote to prevent globbing and word splitting.
                                   ^-- SC2086: Double quote to prevent globbing and word splitting.
                                                  ^-- SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
if [ $# != 3 ] || [ "$1" = "" ] || [ "$2" = "" ] || [ "$3" = "" ]; then


In issue-acm-certificate.sh line 24:
  --domain-name ${TARGET_DOMAIN} \
                ^--------------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --domain-name "${TARGET_DOMAIN}" \


In issue-acm-certificate.sh line 26:
  --region ${REGION} \
           ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --region "${REGION}" \


In issue-acm-certificate.sh line 36:
  --certificate-arn ${CERT_ARN} \
                    ^---------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --certificate-arn "${CERT_ARN}" \


In issue-acm-certificate.sh line 38:
  --region ${REGION} \
           ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --region "${REGION}" \


In issue-acm-certificate.sh line 44:
  --certificate-arn ${CERT_ARN} \
                    ^---------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --certificate-arn "${CERT_ARN}" \


In issue-acm-certificate.sh line 46:
  --region ${REGION} \
           ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --region "${REGION}" \


In issue-acm-certificate.sh line 56:
if [ $VALIDATION_RECORD_NAME == $NONE ] || [ $VALIDATION_RECORD_VALUE == $NONE ] || [ $HOSTED_DOMAIN == $NONE ]; then
     ^---------------------^ SC2086: Double quote to prevent globbing and word splitting.
                                             ^----------------------^ SC2086: Double quote to prevent globbing and word splitting.
                                                                                      ^------------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
if [ "$VALIDATION_RECORD_NAME" == $NONE ] || [ "$VALIDATION_RECORD_VALUE" == $NONE ] || [ "$HOSTED_DOMAIN" == $NONE ]; then


In issue-acm-certificate.sh line 63:
  --hosted-zone-id ${HOSTED_ZONE_ID} \
                   ^---------------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
  --hosted-zone-id "${HOSTED_ZONE_ID}" \


In issue-acm-certificate.sh line 82:
if [ $? == 0 ]; then
     ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?.

For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
  https://www.shellcheck.net/wiki/SC2181 -- Check exit code directly with e.g...

…いっぱい出ましたね。と言ってもほぼ同じ内容に関する指摘のようです。

SC2086: Double quote to prevent globbing and word splitting.

まずこの指摘は、 glob や文字列の分割を防ぐためにダブルクオーテーションで囲みましょう というものです。例えば、下記のスクリプトは S3 バケットに Cache-Control 付きのオブジェクトを Put するスクリプトです。

CACHE_CONTROL="public, max-age=1209600"
aws s3api put-object \
--bucket sample-bucket \
--key sample.txt \
--body ./sample.txt \
--cache-control $CACHE_CONTROL

このシェルスクリプト (s3-put-object.sh) を実行すると、下記のようにエラーとなります。

$ sh s3-put-object.sh

usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:

  aws help
  aws <command> help
  aws <command> <subcommand> help

Unknown options: max-age=1209600

これは、変数 CACHE_CONTROL がそのまま展開されて、 AWS CLI の s3api put-object コマンドのオプション内で分割された文字列が指定されたことによるエラーです。このようなことにならないためにも、変数をダブルクオーテーションで囲みましょうという指摘です。

shellcheck には各指摘に関する詳細ページが用意されているので、そちらで内容及び修正方法を確認することができます。

SC2086 · koalaman/shellcheck Wiki

SC2181: Check exit code directly with e.g. ‘if mycmd;’, not indirectly with $?.

これは、スクリプトの終了コードは $? を使わずに直接チェックしましょうという指摘です。詳細ページでは $? = 0 で判定するのが冗長だと書かれています。 この指摘に対する対応としては、今回はコマンドを関数にして、その関数の実行を判定することにしてみます。例えば、 Certificate をリクエストする部分を次のように書き換えます。

request_certificate () {
  # request certificate
  echo "Request certificate for '${TARGET_DOMAIN}' to ACM."
  CERT_ARN=$( \
    aws acm request-certificate \
    --domain-name "${TARGET_DOMAIN}" \
    --validation-method DNS \
    --region "${REGION}" \
    --output text) \
  && sleep 5 \
  && echo -e "\t CERT_ARN = ${CERT_ARN}"
}

if ! request_certificate; then
  echo "Failed to request certificate."
  exit 1
fi

SC2181 · koalaman/shellcheck Wiki

shfmt でフォーマット

続いて、 shfmt を使ってシェルスクリプトをフォーマットしてみます。

インストール

README に従って、下記のコマンドでインストールします。

$ GO111MODULE=on go get mvdan.cc/sh/v3/cmd/shfmt

$ shfmt -version
v3.2.1

実行

色々オプションがありますが、今回は フォーマットを行ったファイルのファイル名を出力する -l と、フォーマット結果を標準出力ではなくファイルに書き出す -w を指定しています。それ以外のオプションについては shfmt -h とかで実行すれば確認できます。

$ shfmt -l -w issue-acm-certificate.sh
issue-acm-certificate.sh

確認してみると、インデントとか諸々調整されててキレイにフォーマットされていました。

まとめ

shellcheck と shfmt というツールを使って、シェルスクリプトをキレイにした話でした。

最終的には下記のようなシェルスクリプトになったので、過去との差分については Revisions を参照してください。

以上、よっしー (michimani) でした。