はじめに
コマンドの使い方を表示する usage 関数、書いていますか?
書いていますよね?
でも不満がありますよね?
それは・・・
$\huge{インデント}$
普通に書くと
usage() {
cat<<USAGE
Usage: command [-h | --help]
USAGE
}
みたいな感じになってインデントがすこぶる気持ち悪いです。私には他にも細かい要望がいくつもあって、でも良い書き方が思いつかなかったのでずっとモヤモヤしていたのですがようやく思いついたのでインデントに始まるいろいろな要望を叶えた usage 関数の書き方を紹介したいと思います。
要望
私の細かい要望を羅列していきます。説明は省略しますが多分、既存の usage の書き方に不満があった人ならわかるんじゃないかと。
- インデントをきちんと揃えたい
- 本物のタブは使いたくない(インデントはスペース2文字派)
- タブを使ったとしてもヒアドキュメントの終わりはインデントできず揃わない
- ヒアドキュメント後の
}が気持ち悪い - コメントの形で埋め込むやり方は頭の
#が嫌 - ファイルの一番上に書きたい
- とは言うもののバージョン番号を入れた変数よりかは下にしたい時もある
-
set -euとどちらを上にするか悩む - ヘルプの中で変数やコマンド置換を使いたい
- ヘルプの中でも未定義の変数を参照したら落ちてほしい(つまり
set -uを機能させたい) - ヘルプが表示されない場合のパフォーマンス低下はできるだけ避けたい
- なるべく短くシンプルに書きたい
- 複数のヘルプを持たせたい(例えばサブコマンド専用のヘルプ)
- POSIX 準拠の範囲でやりたい
実装
hello.sh
# !/bin/sh
VERSION=0.0.1
set -eu && :<<'USAGE'
Usage: $(basename "$0") [-h | --help] [NAME]
Options:
-h | --help Display this help
Version: $VERSION
USAGE
usage() {
sed -n "/<<'$1'/,/^$1\$/ { s/.*<<'$1'/cat<<$1/; p }"
}
case ${1:-} in (-h | --help)
eval "$(usage "USAGE" < "$0")"
exit 0
esac
echo "Hello ${1:-World}"
みたまんまですが、一応解説すると、
-
&& :<<'USAGE'の左にset -eu等の簡単なコマンドを追加実行でき、一行で書くことが出来ます。 -
VERSION変数等はヘルプの上に書いても下に書いても&& :<<'USAGE'の左に書いても構いません。 -
:コマンドにヘルプ内容を渡してますが、何もしないはずなのでパフォーマンス低下はわずかです。 -
'USAGE'とシングルクォートでくくっているのでヒアドキュメントの中身はただのテキスト扱いです。- つまり
basenameコマンドは(この時点では)実行されずパフォーマンス低下はありません。
- つまり
usage 関数
- 現在のスクリプトファイルを読み取りヒアドキュメントの開始から終わりの行までを出力しています。
- 途中の
set -eu && :<<'USAGE'はcat<<USAGEに変換しています。 - 最終的に
eval "$(usage "USAGE" < "$0")"で出力されたコードを実行します。 -
evalで実行される際は'USAGE'をUSAGEに変更してるので変数やコマンド置換が行われます。 -
USAGEの文字を変更すれば別のヘルプを出力することが出来ます。別のファイルにあっても構いません。
おまけ
シェルスクリプト実装版(sed 未使用)の usage 関数です。
usage() {
while IFS= read -r line && [ ! "${line#*:}" = "<<'$1'" ]; do :; done
while IFS= read -r line && [ ! "$line" = "$1" ]; do set "$@" "$line"; done
shift && [ $# -eq 0 ] || printf '%s\n' "cat<<$line" "$@" "$line"
}