BashスクリプトのUsageを書くのがめんどくさい。

LinuxUNIXを使っていると、自前のBashスクリプトを書くことが多いと思いますが、そのスクリプトが何の引数を受け取るのか覚えていないことがよくあります。 こんなときに、わざわざスクリプトの中身を見て判断するのはめんどくさいので、使い方を間違っているときにUsageを出力するようにするといいのですが、スクリプトの変更を繰り返した結果、このUsageが正しい引数を提示しているかどうかも怪しいことがあります。

例えば、引数を2つ取るspamham.shという以下のようなスクリプトを作るとします。

#!/usr/bin/env bash

readonly program=$(basename $0)

function print_usage_and_exit() {
  echo >&2 "Usage: ${program} SPAM HAM"
  exit 1
}

if [ $# -ne 2 ]; then
  print_usage_and_exit
fi

readonly spam=$1
readonly ham=$2

echo "SPAM: ${spam}"
echo "HAM:  ${ham}"

このスクリプトは引数を2つしか取らず、それ以外の数の引数を渡すとUsageを吐いて終了します。

$ ./spamham.sh
Usage: spamham.sh SPAM HAM

$ ./spamham.sh hoge
Usage: spamham.sh SPAM HAM

$ ./spamham.sh hoge piyo
SPAM: hoge
HAM:  piyo

$ ./spamham.sh 1 2 FIZZ
Usage: spamham.sh SPAM HAM

このスクリプトのままでは引数についての記述が少し冗長で、引数の数なり順番なりを変更するのがめんどくさくなります。 ですので、(spam ham)のように引数を受け取る変数名を列挙するだけで引数を受け取れるようにしましょう。

過程を省きますが、(僕の中では)最終的にこのような形に落ち着きました。

#!/usr/bin/env bash

readonly program=$(basename $0)
readonly args=(spam ham)

function print_usage_and_exit() {
  echo >&2 "Usage: ${program} $(IFS=' '; echo ${args[*]^^})"
  exit 1
}

if [ $# -ne ${#args[@]} ]; then
  print_usage_and_exit
fi

for arg in ${args[@]}; do
  eval "readonly ${arg}=$1"
  shift
done

echo "SPAM: ${spam}"
echo "HAM:  ${ham}"

引数をargs=(spam ham)として定義して、print_usage_and_exitの中では

IFS=' '; echo ${args[*]^^}

として引数の名前を大文字表記で列挙しています。

また、引数の数は配列argsの要素数なので${#args[@]}で取得できます。

最後に、各引数変数に引数の値を代入するところは、evalshiftを使って、次々に変数に$1を代入してはshiftしてを繰り返して実現しています。

こうすることで、引数を変更するときは、args=()の中身を書き換えるだけで済むようになりました。