Help us understand the problem. What is going on with this article?

備忘録: 最小限の労力でサブコマンド付きのシェルスクリプトが書きたい

More than 3 years have passed since last update.

なるべく労力をかけずにサブコマンド付きのシェルスクリプトを書きたい、ということがあった。

労力をかけないことに力点をおいたので、 POSIX 準拠ではなく、 Bash 限定の要素が入っている。

コマンドをそのまま実行するとヘルプ表示

$ some-command
Usage:
  some-command --help
  some-command sub-command1
  some-command sub-command2

--help オプションでもヘルプを表示

$ some-command --help
Usage:
  some-command --help
  some-command sub-command1
  some-command sub-command2

sub-command1, sub-command2 でそれぞれ違った処理を実行する。

※ 下記の例示はただの echo

$ some-command sub-command1
1
$ some-command sub-command2
2

存在しないサブコマンドにもヘルプ表示

また、エラー扱いとして exit code 1 で終了したい。

$ some-command wrong-command
Usage:
  some-command --help
  some-command sub-command1
  some-command sub-command2
$ echo $?
1

どう書く

サブコマンドをシェルスクリプトの関数として対応関係にあれば、そこそこ可読性が高く、と追加や制御の手間が最小限になると考えた。

some-command
#!/usr/bin/env bash
sub-command1() {
  echo 1
}

sub-command2() {
  echo 2
}

--help() {
  echo 'Usage:'
  compgen -A function \
    | xargs -I % echo ' ' "$(basename "$0")" %
}

main() {
  unset -f -- "${FUNCNAME[0]}"
  ! declare -F -- "$1" >/dev/null && --help && exit 1
  "$@"
}

main "$@"

テクニック解説

テクニック1. main をサブコマンドから消す

unset -f -- "${FUNCNAME[0]}"

${FUNCNAME[0]} で自身の関数名が取得できるので、 unset すると名前空間から main が消えてくれる。後々の compgen -A function でサブコマンドを得るときに main がない方が良い。

テクニック 2. 簡易ヘルプ

本来は入力補間を実現するのための組み込みコマンドとして compgen というものがある。これをヘルプの表示につかう。具体的には、関数を列挙してサブコマンドとして表示する。

大本の実行コマンドは $(basename "$0") から得る。

compgen -A function \
    | xargs -I % echo ' ' "$(basename "$0")" %

テクニック 3. 関数名の存在チェック

declare -F を使って関数名の存在チェックをしている。なければヘルプ表示

  ! declare -F -- "$1" >/dev/null && --help && exit 1

テクニック 4. "$@"

シェル関数を先頭で消費しつつ、残りを引数として関数に渡せる。
なので例えばもし sub-command1

sub-command1() {
  echo "$@"
}

のような定義にすれば、

$ some-command sub-command1 A B C
A B C

となる。
サブコマンドで、実際の処理は他のコマンドに委譲する場合に便利になる。

まとめ

--help から 最後の行まではボイラープレートとして捉えて、コピペすると楽でしょう。

ここはコピペ
# http://qiita.com/kitsuyui/items/4b204963e0ebec53fe3c

--help() {
  echo 'Usage:'
  compgen -A function \
    | xargs -I % echo ' ' "$(basename "$0")" %
}

main() {
  unset -f -- "${FUNCNAME[0]}"
  ! declare -F -- "$1" >/dev/null && --help && exit 1
  "$@"
}

main "$@"
ikyu
「こころに贅沢を」をコンセプトに一休.com、一休レストランなどのサービスを提供しています。
https://www.ikyu.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away