Edited at

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

More than 1 year has 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 "$@"