rcmdnk's blog

20240924_shellopt_200_200

シェルスクリプトでのオプション引数の管理について。

たくさん引数を持つシェルスクリプトで一つ一つ管理していると色々と面倒だなと思うことがあり、 一箇所で管理できる方法を考えてみました。

シェルスクリプトでのオプション引数の管理

シェルスクリプトでオプション引数を管理する場合、 getoptsを使うと

1
2
3
4
5
6
7
8
9
10
11
12
_help="Usage: $0 [-a <arg>] [-b <arg>] [-c <arg>] [-h]"

while getopts "a:b:c:h" OPT
do
  case $OPT in
    a) __a=$OPTARG;;
    b) __b=$OPTARG;;
    c) __c=$OPTARG;;
    h) echo "$_help";exit 0;;
    *) echo "$_help";exit 1;;
  esac
done

みたいな感じでオプション引数を取得することができます。

ただし、この方法だとダブルハイフン--でのオプション引数の取得ができないので、 ダブルハイフンなオプションを使いたい場合は、

1
2
3
4
5
6
7
8
9
10
11
12
_help="Usage: $0 [--aaa|-a <aaa>] [--bbb-ccc|-b <bbb_ccc>] [--ddd <dddd>] [--help|-h]"

while [ $# -gt 0 ];do
  case $1 in
    --aaa|-a) __aaa=$2; shift ;;
    --bbbb-ccc|-u) __bbb_ccc=$2; shift ;;
    --ddd) __ddd=$2; shift ;;
    --help|-h) echo "$_help";exit 0;;
    *) echo "Unknown option: $1";echo "$_help";exit 1;;
  esac
  shift
done

みたいな感じで自分で引数を回す必要があります。

やりたいことは

  • オプション引数として全てはダブルハイフンのオプションがある。
  • 一部のオプションはシングルハイフンで1文字のオプションもある。
  • オプション引数は基本的に値を取る。
    • --helpだけ例外。
  • 値はダブルハイフンのオプションをハイフンをアンダーバーに変えた変数に格納する。
  • それぞれの引数の説明を--helpで表示する。

といった感じのこと。

とくにヘルプを作ろうと思うと通常は別途全部書くことになりますが、 数が増えてくると新たに追加したオプションのヘルプを追加し忘れたり管理が面倒になってきます。

なので一箇所で管理したい、と。

オプション引数をコメント含めてまとめて管理する

管理用変数

以下のような文字列変数を用意します。

1
2
3
4
__options="
--name|-n Set name.
--greet_msg Set greet message.\nDefault is 'Hello'.
"
  • --オプション名|-短縮オプション名でオプション名を記述する。
  • 短縮オプションがない場合はダブルハイフンのオプションのみ記述。
  • 一文字空白入れて説明を記述する。
  • 説明が複数行の場合は\nで改行する。

ヘルプ表示

ヘルプは以下のように作ります。

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
_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"
}

_print_options() {
  echo "Options:"
  local line
  echo "$__options" | while read -r line; do
    if [ -z "$line" ];then
      continue
    fi
    local opt="$(echo "${line%% *}" | sed 's/|/, /g')"
    local comment="$(echo -e "${line#* }")"
    _print_explain "$opt" 2 35 "$comment"
  done
  _print_explain "--help, -h" 2 35 "Show help."
}
  • _print_explain
    • 第一引数に関数名。
    • 第二引数に表示のインデントのスペース数。
    • 第三引数に関数名の表示幅(説明文の開始を揃えるため。左詰め)。
    • 第四引数にコメント。
  • _print_options
    • __optionsからオプション名とコメントを取り出して表示する。
      • オプション名は--all|-a-all, -aのように表示する。

引数処理関数

case文で処理したいわけですが、文字列になってるものから変数も作るので evalを使います。

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
_generate_read_options() {
  cat << EOF
_read_options() {
  __positional=()
  while [[ \$# -gt 0 ]]; do
    if [ \$1 = "--" ]; then
      shift
      __positional+=("\$@")
      break
    fi
    local opt="\${1//_/-}"
    case \$opt in
EOF

  echo "$__options" | while read -r line; do
    if [ -z "$line" ];then
      continue
    fi
    local opt=$(echo "$line" | awk '{print $1}')
    local val_name=$(echo "${opt//-/_}" | cut -d '|' -f 1)
    echo "      $opt) ${val_name}=\"\$2\"; shift; shift;;"
  done

cat << EOF
      --help|-h) help; exit;;
      -*)
        echo "Unknown option: \$1" 1>&2
        exit 1
        ;;
      *)
        if [ "\$1" = "help" ]; then
          help
          exit
        fi
        __positional+=("\$1")
        shift
        ;;
    esac
  done
}
EOF
}

eval "$(_generate_read_options)"

_read_options "$@"

こんな感じに。helpだけは値なしのオプションで常に使うものだとして別途付け加えてあります。

evalを使わずにやろうと思うと、Bash 4.0以降であれば変数の格納部分で連想配列を使うことで出来そうですが 上のように文字列をparseしようと思うと結構面倒なコードになるので やるなら最初からオプジョンの定義を連想配列などで綺麗にまとめられれば良いかもしれません。

ただシェルスクリプトで複雑な構造を作ろうと思うとやはりごちゃごちゃしてしまうので、 手を加える部分がオプションの定義部分だとすれば上のように文字列で定義するのがわかりやすいと思います。

これを昨日のものと組み合わせると、

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env bash

__options="
--name|-n Set name.
--greet-msg Set greet message.\nDefault is 'Hello'.
"

# Default parameters
__name="Alice"
__greet_msg="Hello"

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

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

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

# Functions for help
_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 () {
  local func="$1"
  awk "/^${func} \(\)/{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
}

_print_options() {
  echo "Options:"
  local line
  echo "$__options" | while read -r line; do
    if [ -z "$line" ];then
      continue
    fi
    local opt="$(echo "${line%% *}" | sed 's/|/, /g')"
    local comment="$(echo -e "${line#* }")"
    _print_explain "$opt" 2 35 "$comment"
  done
  _print_explain "--help, -h" 2 35 "Show help."
}

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

# Generate options parser
_generate_read_options() {
  cat << EOF
_read_options() {
  __positional=()
  while [[ \$# -gt 0 ]]; do
    if [ "\$1" = "--" ]; then
      shift
      __positional+=("\$@")
      break
    fi
    local opt="\${1//_/-}"
    case \$opt in
EOF

  echo "$__options" | while read -r line; do
    if [ -z "$line" ];then
      continue
    fi
    local opt=$(echo "$line" | awk '{print $1}')
    local val_name=$(echo "${opt//-/_}" | cut -d '|' -f 1)
    echo "      $opt) ${val_name}=\"\$2\"; shift; shift;;"
  done

cat << EOF
      --help|-h) help; exit;;
      -*)
        echo "Unknown option: \$1" 1>&2
        exit 1
        ;;
      *)
        if [ "\$1" = "help" ]; then
          help
          exit
        fi
        __positional+=("\$1")
        shift
        ;;
    esac
  done
}
EOF
}

# Start main

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

# No arguments
if [ $# -eq 0 ];then
  help
fi

# Read arguments
eval "$(_generate_read_options)"
_read_options "$@"

# Check sub command
__subcommand="${__positional[0]}"
shift

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

# 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
22
23
24
25
26
$ ./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.
Options:
  --name, -n                         Set name.
  --greet-msg                        Set greet message.
                                     Default is 'Hello'.
  --help, -h                         Show help.
$ ./my_command.sh commands
commands greet help
$ ./my_command.sh greet
Hello Alice!
$ ./my_command.sh greet --name Bob --greet-msg "Good morning"
Good morning Bob!
$ ./my_command.sh greet --name 太郎君 --greet-msg "こんにちは"
こんにちは 太郎君!
$ ./my_command.sh greet -n 太郎君 --greet-msg "こんにちは"
こんにちは 太郎君!
$ ./my_command.sh greet -n 太郎君 -g "こんにちは"
Unknown option: -g
$

こんな感じで使えます。

Sponsored Links
Sponsored Links

« シェルスクリプトで関数につけたコメントをヘルプとして表示できるようにする Win/Macそれぞれで特定のウィンドウを最前面に固定する »

}