3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

わがままな要望を叶えたシェルスクリプトのusage関数の書き方

Last updated at Posted at 2020-04-15

はじめに

コマンドの使い方を表示する 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"
}
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?