rcmdnk's blog

20240908_subcommand_200_200

シェルスクリプトでサブコマンドを持つようなものを作りたいときに、 関数を定義するとそれがそのままサブコマンドとして使えるようにする実装について。

やりたいこと

1
$ ./my_command.sh <subcommand> [options]

みたいに git commit, git pushみたいな感じで最初の引数にサブコマンドを渡して色々出来るようにするスクリプトを作りたいとします。

このサブコマンド部分を、スクリプト内で関数を実装するとその関数名のサブコマンドがそのまま使えるようにするというもの。

引数と関数の結びつけの一手間を省けるというが利点です。

実装例

my_command.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env bash

# Default parameters
__name="Alice"

# Non sub command
_hello () {
  echo "hello $1!"
}

# sub commands
greet () {
  _hello "$__name"
}

commands () {
  echo "$__subcommands"
}

help () {
  echo "Usage $0 <subcommand> [options]"
  echo ""
  echo "Subcommands: $(commands)"
}

# Make sub command list
__subcommands=$(set | grep -v "^_" | grep -v "^ " | grep " () $" | cut -d' ' -f1 | tr '\n' ' ')

# Start main
if [ $# -eq 0 ];then
  help
fi

# Check sub command
__subcommand="$1"
shift

if ! echo " $__subcommands "|grep -q " $__subcommand ";then
  echo "$__subcommand is unknown subcommand."
  echo ""
  help
  exit 1
fi

# Get other arguments
while getopts n:h OPT;do
  case $OPT in
    "n")__name="$OPTARG";;
    "h")help; exit 0;;
    *) echo "Unknown argument: $OPT"; echo; help; exit 1;;
  esac
done

# Run sub command
$__subcommand

こんな感じのスクリプト。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ./my_command.sh
Usage ./my_command.sh <subcommand> [options]

Subcommands: commands greet help
$ ./my_command.sh commands
commands greet help
$ ./my_command.sh help
Usage ./my_command.sh <subcommand> [options]

Subcommands: commands greet help
$ ./my_command.sh greet
hello Alice!
$ ./my_command.sh greet -n Bob
hello Bob!
$ ./my_command.sh abc
abc is unknown subcommand.

Usage ./my_command.sh <subcommand> [options]

Subcommands: commands greet help
$

こんな感じで使えます。

説明

肝となるのはこの部分。

1
__subcommands=$(set | grep " () $" | grep -v "^_" | grep -v "^ " | cut -d' ' -f1 | tr '\n' ' ')

setコマンドによって定義されている変数や関数を全て列挙して、そこから関数だけ抜き出しています。

関数は

1
2
3
4
greet () 
{ 
  _hello "$__name"
}

の様に表示されます。

スクリプトの中ではfunctionを使って

1
2
3
4
function greet
{
  _hello "$__name"
}

と書くこともできますが、この場合でもsetの出力はfunctionなしで()つきのものになります。 {に関しても同じ行に書いても出力は必ず次の行に書かれます。

したがって、()を持つ行を探しますが、ちょっと注意が必要なのが表示上()で終わってるように見えますが後ろにスペースが一つ入ってます。 (下の{のところにも一つスペースが入ります。)

なのでgrep " () $"の様に一つスペースを挟んで終了する行を探しています。

アンダーバー(_)で始まる関数は隠し関数としてサブコマンドには入れないようにします。

また、関数の中で関数を定義しているような場合、関数の中身の関数が出てくるのでスペースから始まる行は無視で。 ただ、関数内でも行頭から書いてもエラーにはならないのでその場合は例外的になり、動作が上手く行かなくなりますが そういった例外処理は今回はスキップで。

後は使いやすいように()を消してすべてのコマンドをスペース区切りに直して文字列として保存してあります。

これでリストが出来たので、 第一引数をサブコマンドとみなして、

1
if ! echo " $__subcommands "|grep -q " $__subcommand ";then

によって使えるサブコマンドかチェックして、必要であれば残りの引数を処理し、 あとはサブコマンドで指定された関数を実行しています。

Sponsored Links
Sponsored Links

« GNU Screen 5.0.0リリース(20年ぶりのメジャーアップデート?) シェルスクリプトで関数につけたコメントをヘルプとして表示できるようにする »

}