rcmdnk's blog

20240923_shellfunc_200_200

シェルスクリプトで関数につけたコメントを取得してヘルプとして表示したい、という話。

関数に書いたコメントをそのままヘルプにしたい

前回書いたシェルスクリプトで関数を定義したらそのままサブコマンドとして使えるようにするという話。

このようなスクリプトを作ってヘルプを作ろうと思った時、 普通にやろうと思うと関数とそれを使うサブコマンドのヘルプは 別の部分に書くことになります。

ただ、せっかく関数を実装すると自動的にサブコマンドになるのに、 別の部分でそのサブコマンド用の何かを追加しないといけない、となるとメリットが減ってしまいます。

関数先頭とかに関数の説明コメントを書いて、それがそのままヘルプとして表示できるようになれば サブコマンドの定義とヘルプの記述が同時に出来ることになり、メンテナンスするのも楽になるのではないか、ということで そういったことが出来るようにしてみます。

コメントの抽出方法

<func> ()な関数

シェルスクリプトでこんな関数があるとします。

1
2
3
4
5
greet () {
  # Call the _hello function,
  # passing the value of the $__name variable.
  _hello "$__name"
}

このコメント部分をヘルプとして表示するべく抽出します。

これ、実行中の状態だけから抽出できると良いのですが、うまいこと思いつかなかったので ちょっと無理やりですが以下のような方法で読み出します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_extract_comments () {
  local script="$0"
  local func="$1"

  local in_func=$(
    awk "/^${func} *\(\) *{/ {
      flag = 1;
      next;
    }
    flag && /^}/ {
      exit;
    }
    flag" "$script"
  )

  echo "$in_func" | awk "/^ *#/ {
      sub(/^ *# ?/, \"\");
      print;
      next;
    }
    {
      exit;
    }"
}

ワンライナーにするとこんな感じ。

1
  awk "/^${1} *\(\) *{/{flag=1;next}/^}/{flag=0}flag" "$0" | awk '/^ *#/{sub(/^ *# ?/, ""); print; next} {exit}'

外部コマンドのawkを使いますが、awkなしだとかなり面倒になるのでawkは使えるとします。

awkを使って現在実行中のスクリプト$0の中から引数で与えられたfuncを探しそこからコメント部分を抽出しています。

コメント部分に関しては 通常のファイルとして再度読み込むような形になっているのがあまり綺麗では無いところですが 今のところ 実行中のスクリプトから直接参照する方法が思いつかないのでこんな感じで。

やっていることは最初の部分が<func> () {で始まる行を探し、それ以降で}の行までを抜き出し、 パイプのあとはその中から最初の行から書かれているコメント部分を抜き出しています。

greet.sh
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash
greet () {
  # Call the _hello function,
  # passing the value of the $__name variable.
  _hello "$__name"
}

_extract_comments () {
  awk "/^${1} *\(\) *{/{flag=1;next}/^}/{flag=0}flag" "$0" | awk '/^ *#/{sub(/^ *# ?/, ""); print; next} {exit}'
}

_extract_comments greet

を実行すれば

1
2
3
$ ./greet.sh
Call the _hello function,
passing the value of the $__name variable.

先頭からの連続コメント行だけを見るので、

1
2
3
4
5
6
greet () {

  # Call the _hello function,
  # passing the value of the $__name variable.
  _hello "$__name"
}

のように一行開けると何も取れなくなります。

また、

1
2
3
4
5
6
7
greet () {
  # Call the _hello function,
  # passing the value of the $__name variable.

  # This is ignored
  _hello "$__name"
}

の様に途中で一行空けばその前までの取得になります。

関数内で定義される関数などを除くために関数の定義の前にスペースとか何もないように^を指定してますが、 もし関数内で定義されているようなものも見たかったら

\^${1}...

\ *${1}...

の様にしてください。

また、このスクリプトだと

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fucntion func1 {
  # comment1
  echo func1
}

fucntion func2 () {
  # comment2
  echo func2
}

func3 ()
{
  # comment3
  echo func3
}

みたいな形の functionを使った宣言をしている、 {が別の行にある、 といった場合には取得できません。

function <func> [()]な関数

func1/func2の場合であれば、 awk

\^${1} *\(\) *{\

で探していた部分をfunctionから始まるようにして探せば良く、

^function ${1} *(\(\))? *{

のようにすればOK。

1
2
3
_extract_comments () {
  awk "/^function ${1} *(\(\))? *{/{flag=1;next}/^}/{flag=0}flag" "$0" | awk '/^ *#/{sub(/^ *# ?/, ""); print; next} {exit}'
}

のようにすればfunctionで始まる書き方なら()があってもなくても取ることが出来ます。

function で始まればほぼ関数なので

^function ${1}

でも殆どの場合は問題なく動くはずです。

{ が次の行にくる定義の関数

{が別行に来るのもを見つけるには 以下のように変更します。

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
_extract_comments () {
  local script="$0"
  local func="$1"

  local in_func=$(
    awk "/^${func} *\(\)/ {
      flag = 1;
      getline;
      next;
    }
    flag && /^}/ {
      exit;
    }
    flag" "$script"
  )

  echo "$in_func" | awk "/^ *#/ {
      sub(/^ *# ?/, \"\");
      print;
      next;
    }
    {
      exit;
    }
  "
}

最初のin_func(関数内の文字列)を探す部分で getlineが追加されています。 これによって次の{の行をスキップする形。

ワンライナーにするとこんな感じ。

1
  awk "/^${1} *\(\) *{/{flag=1;getline;next}/^}/{flag=0}flag" "$0" | awk '/^ *#/{sub(/^ *# ?/, ""); print; next} {exit}'

これにfunction付きの書き方などまだ組み合わせはありますが、 とりあえずこんな感じで対応出来ます。

違うものに色々対応しようと思うと正規表現を複雑にしたり条件分岐追加したりで awkだけだと結構辛いことになるかも。

とりあえず自分のスクリプトでやる際には関数の書き方決めればそれに対応するものを用意すれば良いかと。

ヘルプを作る

シェルスクリプト内の関数のヘルプを編集中以外でわざわざ表示することはめったにないと思いますが、 前回やったサブコマンドとして使う場合、各サブコマンドのヘルプとして関数のコメントを使えます。

上で使った例に対してsubcommandとして使う関数にコメントを書いてそれをヘルプ表示できるようにします。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env bash

# Default parameters
__name="Alice"

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

# sub commands
greet () {
  # Call the _hello function,
  # passing the value of the $__name variable.
  _hello "$__name"
}

commands () {
  # Show subcommands.
  echo "$__subcommands"
}

_print_explain () {
  local value="$1"
  local spaces="$2"
  local name_length="$3"
  local comments="$4"

  local prefix=$(printf "%${spaces}s%-${name_length}s%s\n" "" "$value ")
  while IFS= read -r line; do
    printf "%s%s\n" "$prefix" "$line"
    prefix="${prefix//?/ }"
  done <<< "$comments"
}

_extract_comments () {
  awk "/^${1} *\(\) *{/{flag=1;next}/^}/{flag=0}flag" "$0" | awk '/^ *#/{sub(/^ *# ?/, ""); print; next} {exit}'
}

_print_subcommands () {
  echo "Subcommands:"
  for func in $__subcommands;do
    local comments=$(_extract_comments "$func")
    _print_explain "$func" 2 35 "$comments"
  done
}

help () {
  # Show this help.
  echo "Usage $0 <subcommand> [options]"
  echo ""
  _print_subcommands
}

# 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

追加されたのは以下の関数。

  • _print_explain
    • 第一引数に関数名。
    • 第二引数に表示のインデントのスペース数。
    • 第三引数に関数名の表示幅(説明文の開始を揃えるため。左詰め)。
    • 第四引数にコメント。
  • _extract_comments
    • 関数名を引数に取り、その関数のコメントを取得する。
  • _print_subcommands
    • サブコマンドの一覧を表示する。

で、helpの中で_print_subcommandsを呼んでいます。

これで、

1
2
3
4
5
6
7
8
$ ./my_command.sh
Usage ./my_command.sh <subcommand> [options]

Subcommands:
  commands                           Show subcommands.
  greet                              Call the _hello function,
                                     passing the value of the $__name variable.
  help                               Show this help.

といった感じにsubcommandのそれぞれのヘルプを表示出来るようになります。

あまり大きなスクリプトを想定してないのでコマンド毎に毎回ファイルを読み込むようにしていますが、 気になるようなら helpの最初で、

1
  __script=$(cat "$0")

として変数として確保しておいて

1
2
3
_extract_comments () {
  echo "$__script" | awk "/^${1} *\(\) *{/{flag=1;next}/^}/{flag=0}flag" | awk '/^ *#/{sub(/^ *# ?/, ""); print; next} {exit}'
}

のようにして使うとかもありかと。

Sponsored Links
Sponsored Links

« シェルスクリプトで関数をそのままサブコマンドとして使う シェルスクリプトでオプション引数をコメント含めてまとめて管理する »

}