シェルスクリプトの引数処理をモジュール化
別にシェルスクリプトに限らないが、プログラムを作る時に引数の処理は大体、後回しになってしまう。そうするとプログラムを書いている途中では定数(や特定の値を代入した変数)を使ってハードコーディングしてしまって、後で直すのが なお更面倒になってしまう。そこで次のような組み込み用のシェルスクリプトを考えた。
- オプションや引数の値をそれぞれ対応するシェル変数に取り込んでくれる
- オプションや引数を出来る限りシンプルに定義でき、直ぐに変更できる
- シェルスクリプト本体で行なう作業は最小限にできる(出来る限り自動化する)
- 定義に従ってUsageメッセージも自動生成して、定義を変更してもプログラムを変更しなくて済む
- 設定によっては色々と柔軟な付加機能を持つ
- Linux(Fedora 8)の標準機能だけで実現する
引数の処理をしてくれるライブラリとか関数とかモジュールとかがあれば良いのだけども、引数の処理はプログラム毎に違うのでライブラリ的に作るのはちょっと大変。と思っていたがシェルスクリプトに限定すればある程度汎用的に作れるのではと思って作って見た。(探せばあるのだろうが、シェルプログラミングの練習も兼ねて作成してみた。「追記(2008/04/05)」参照)
UNIX(Linux)流の引数やオプションの定義にもとづいて、引数を次のように分類する:
- 引数は"-"、"--"によって指定されるオプション(Option)と、出現の順番で指定されるもの(Ordered)がある
- "-"が先頭にあるのは"文字型"のオプション
- "--"が先頭にあるのは"単語型"のオプション
- オプションは、値を持たないフラグ型と値を持つバリュー型がある
- 出現順の引数は必ず必要なものと任意のものがある(必須引数の数を指定する)
これに基本ルールとして次の6項目を決めれば引数を(ある程度)自動的に処理できるシェルスクリプトが作れる。(カッコ内は下の例との対応)
- 文字型フラグオプションの文字の集合 ("ab")
- 文字型バリューオプションの文字の集合 ("cd")
- 単語型フラグオプションの単語の集合("help")
- 単語型バリューオプションの単語の集合("config")
- 出現順引数の単語の集合("out_file in_file ...")
- 必須となる出現順引数の数("1" out_fileだけが必須とする)
command [-ab] [--help] [-c argument] [-d argument] [--config argument] out_file [in_file ...]
これだけ定義すれば、引数を処理して次のようなシェル変数に変換する。
- 文字型フラグオプションが指定されたら「ARGS_opt_X」(Xは対応する文字)を"Y"にする
- 文字型バリューオプションが指定されたら「ARGS_opt_X」(Xは対応する文字)に値を代入する
- 単語型フラグオプションが指定されたら「ARGS_opt_XXX」(XXXは対応する単語)を"Y"にする
- 単語型バリューオプションが指定されたら「ARGS_opt_XXX」(XXXは対応する単語)に値を代入する
- 出現順引数は出現順に「ARGS_1、ARGS_2 …」に値を代入する(ARGS_0はコマンド名とする)
- 必須出現順引数は名前が定義されているので「ARGS__XXX」(XXXは対応する単語)にも値を代入する
あとはシェルスクリプトの本体の中で、"ARGS_..."という名前のシェル変数を参照すればいい。
program list of "shargs"
色々と手を加えていたらいつの間にか150行を越えてしまったが、やっていることは非常に単純なので特に説明は要らないと思う。
1 #! /bin/sh 2 3 # The shell script for shell script's argument processing: 4 # shargs Ver 0.003 (2008/07/20) 5 # Copyright (C) 2008 Adsaria 6 7 # This program is free software; you can redistribute it and/or modify it. 8 # This program is distributed in the hope that it will be useful, but 9 # WITHOUT ANY WARRANTY. 10 11 # 12 ### 13 ##### Following variables are environment dependent, modify them to your environment. 14 15 if [ -z "$ARGS_DEF_opt_char_flg" ];then ARGS_DEF_opt_char_flg="" ; fi 16 if [ -z "$ARGS_DEF_opt_char_val" ];then ARGS_DEF_opt_char_val="" ; fi 17 if [ -z "$ARGS_DEF_opt_word_flg" ];then ARGS_DEF_opt_word_flg="" ; fi 18 if [ -z "$ARGS_DEF_opt_word_val" ];then ARGS_DEF_opt_word_val="" ; fi 19 if [ -z "$ARGS_DEF_arg_namelist" ];then ARGS_DEF_arg_namelist="" ; fi 20 if [ -z "$ARGS_DEF_arg_num_must" ];then ARGS_DEF_arg_num_must=0 ; fi 21 22 if [ -z "$ARGS_DEF_true" ]; then ARGS_DEF_true="Y" ; fi 23 if [ -z "$ARGS_DEF_false" ]; then ARGS_DEF_false="N" ; fi 24 25 ##### 26 ### 27 # 28 29 ARGS_count=0 30 ARGS_0=$0 31 ARGS_error=$ARGS_DEF_false 32 33 ##### Checking the characters of option names and argument names 34 35 ARGS_tmp_1=$ARGS_DEF_opt_char_flg 36 ARGS_tmp_1="$ARGS_tmp_1 $ARGS_DEF_opt_char_val" 37 ARGS_tmp_1="$ARGS_tmp_1 $ARGS_DEF_opt_word_flg" 38 ARGS_tmp_1="$ARGS_tmp_1 $ARGS_DEF_opt_word_val" 39 ARGS_tmp_1="$ARGS_tmp_1 `echo $ARGS_DEF_arg_namelist | sed 's/ \.\.\.$//'`" 40 ARGS_tmp_1=`echo -n $ARGS_tmp_1 | tr -d " a-zA-Z0-9_" | wc -c` 41 if [ $ARGS_tmp_1 -ne 0 ]; then 42 ARGS_fatal_error=$ARGS_DEF_true 43 ARGS_message="shargs: the argument's names should be the letter of \"a-z\" \"A-Z\" \"0-9\"and \"_\"." 44 fi 45 46 if [ $ARGS_DEF_arg_num_must -gt `echo $ARGS_DEF_arg_namelist | wc -w` ]; then 47 ARGS_fatal_error=$ARGS_DEF_true 48 ARGS_message="shargs: At least the number of mandatory arguments should be in arguments list." 49 fi 50 51 if [ "$ARGS_fatal_error" = $ARGS_DEF_true ]; then 52 ARGS_error=$ARGS_DEF_true 53 else 54 55 ##### The body of processing the arguments 56 57 while [ $# -ne 0 -a $ARGS_error = $ARGS_DEF_false ]; do 58 case $1 in 59 --*) 60 ARGS_tmp_current=${1:2} 61 ARGS_tmp_1=`echo "$ARGS_DEF_opt_word_val" | wc -w` 62 ARGS_tmp_2=`echo "${ARGS_DEF_opt_word_val/"$ARGS_tmp_current"/}" | wc -w` 63 if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then 64 if [ $# -eq 1 ]; then ARGS_error=$ARGS_DEF_true; break; fi 65 eval "ARGS_opt_$ARGS_tmp_current=\"$2\"" 66 shift; shift 67 else 68 ARGS_tmp_1=`echo "$ARGS_DEF_opt_word_flg" | wc -w` 69 ARGS_tmp_2=`echo "${ARGS_DEF_opt_word_flg/"$ARGS_tmp_current"/}" | wc -w` 70 if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then 71 eval "ARGS_opt_$ARGS_tmp_current=$ARGS_DEF_true" 72 shift 73 else 74 ARGS_error=$ARGS_DEF_true 75 shift 76 break 77 fi 78 fi 79 ;; 80 -[^-]*) 81 ARGS_tmp_current=${1:1} 82 ARGS_tmp_index=0 83 while [ $ARGS_tmp_index -lt ${#ARGS_tmp_current} ]; do 84 ARGS_tmp_char=${ARGS_tmp_current:$ARGS_tmp_index:1} 85 ARGS_tmp_1=${#ARGS_DEF_opt_char_val} 86 ARGS_tmp_2=`echo -n ${ARGS_DEF_opt_char_val/$ARGS_tmp_char/} | wc -c` 87 if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then 88 if [ $# -eq 1 ]; then ARGS_error=$ARGS_DEF_true; break; fi 89 eval "ARGS_opt_$ARGS_tmp_char=\"$2\"" 90 shift 91 else 92 ARGS_tmp_1=${#ARGS_DEF_opt_char_flg} 93 ARGS_tmp_2=`echo -n ${ARGS_DEF_opt_char_flg/$ARGS_tmp_char/} | wc -c` 94 if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then 95 eval "ARGS_opt_$ARGS_tmp_char=$ARGS_DEF_true" 96 else 97 ARGS_error=$ARGS_DEF_true 98 break 99 fi 100 fi 101 ARGS_tmp_index=`expr $ARGS_tmp_index + 1` 102 done 103 shift 104 if [ "$ARGS_error" = $ARGS_DEF_true ]; then break; fi 105 ;; 106 *) 107 ARGS_count=`expr $ARGS_count + 1` 108 eval "ARGS_$ARGS_count=\"$1\"" 109 ARGS_tmp_word_name=`echo -n "$ARGS_DEF_arg_namelist" | awk "{print \\$$ARGS_count}"` 110 if [ "$ARGS_tmp_word_name" != "" -a "$ARGS_tmp_word_name" != "..." ]; then 111 eval "ARGS__$ARGS_tmp_word_name=\"$1\"" 112 fi 113 shift 114 ;; 115 esac 116 done 117 118 if [ $ARGS_count -lt $ARGS_DEF_arg_num_must ]; then ARGS_error=$ARGS_DEF_true; fi 119 120 121 122 ##### Message generation part 123 124 ARGS_usage="Usage: $ARGS_0" 125 126 if [ -n "$ARGS_DEF_opt_char_flg" ]; then 127 ARGS_usage="$ARGS_usage [-$ARGS_DEF_opt_char_flg]" 128 fi 129 130 for ARGS_tmp_word in $ARGS_DEF_opt_word_flg ; do 131 ARGS_usage="$ARGS_usage [--$ARGS_tmp_word]" 132 done 133 134 ARGS_tmp_index=0 135 while [ $ARGS_tmp_index -lt ${#ARGS_DEF_opt_char_val} ]; do 136 ARGS_tmp_char=${ARGS_DEF_opt_char_val:$ARGS_tmp_index:1} 137 ARGS_usage="$ARGS_usage [-$ARGS_tmp_char argument]" 138 ARGS_tmp_index=`expr $ARGS_tmp_index + 1` 139 done 140 141 for ARGS_tmp_word in $ARGS_DEF_opt_word_val ; do 142 ARGS_usage="$ARGS_usage [--$ARGS_tmp_word argument]" 143 done 144 145 ARGS_tmp_count=0 146 ARGS_tmp_first_opt=$ARGS_DEF_true 147 for ARGS_tmp_word in $ARGS_DEF_arg_namelist ; do 148 if [ $ARGS_tmp_count -lt $ARGS_DEF_arg_num_must ]; then 149 ARGS_usage="$ARGS_usage $ARGS_tmp_word" 150 else 151 if [ $ARGS_tmp_first_opt = $ARGS_DEF_true ]; then 152 ARGS_tmp_first_opt=$ARGS_DEF_false 153 ARGS_usage="$ARGS_usage [$ARGS_tmp_word" 154 else 155 if [ $ARGS_tmp_word = "..." ]; then 156 ARGS_usage="$ARGS_usage $ARGS_tmp_word" 157 else 158 ARGS_usage="$ARGS_usage] [$ARGS_tmp_word" 159 fi 160 fi 161 fi 162 let ARGS_tmp_count++ 163 done 164 if [ $ARGS_tmp_first_opt = $ARGS_DEF_false ]; then ARGS_usage="$ARGS_usage]"; fi 165 166 ##### Display the message if required 167 168 if [ "$ARGS_error" = $ARGS_DEF_true ]; then 169 if [ "$ARGS_DEF_show_message" = $ARGS_DEF_true ];then 170 echo $ARGS_usage 171 fi 172 fi 173 174 175 fi 176 177 ##### Unset the temporary shell variables 178 179 unset ${!ARGS_tmp*} 180 unset ${!ARGS_DEF*}
行番号付だとコピペできないというコメントを頂いたが、長いプログラムは行番号があった方が説明しやすい。もしコピペでファイルに落とすときは、一旦、行番号付きでコピペして、vi コマンドで次のように処理する:
:%s/^ \+[0-9]\+ // ^空白 ^タブ(タブを入力すると"^I"と表示される) :wq但し、コピペする時にタブ文字が空白文字に置き換わってしまう環境では次の様にする:
:%s/^ \+[0-9]\+ // ^空白 ^空白2つ :wq
shargsの使い方
shargsは単体でも実行できるが、勿論、これだけでは何の意味もない。このshargsのスクリプトをメインとなるシェルスクリプトに組み込んで使うか、メインとなるシェルスクリプトから"source"コマンド(もしくは"."コマンド)で呼び出して使う。(shargsは150行を越えてしまったので、直接組み込むよりは、呼び出したほうが良いだろう。)例えば /usr/local/bin/shargsなどに保存しておいて呼び出す。sourceで呼び出して使う場合は次のようなパーミッションにしておく(呼び出して使う場合は"実行"許可は要らない。実行許可を与えなければ単体では実行できないので、余計なトラブルは少なくなると思う。
% ls -l /usr/local/bin/shargs -rw-r--r-- 1 root root 5406 2008-07-20 16:18 /usr/local/bin/shargs
次にこのshargsを呼び出して使うメインとなるシェルスクリプトのサンプル、testargsを紹介する。
1 #! /bin/sh 2 3 ARGS_DEF_opt_char_flg="ab" 4 ARGS_DEF_opt_char_val="cd" 5 ARGS_DEF_opt_word_flg="help" 6 ARGS_DEF_opt_word_val="config" 7 ARGS_DEF_arg_namelist="out_file in_file ..." 8 ARGS_DEF_arg_num_must=1 9 10 . /usr/local/bin/shargs 11 12 echo +++++ 13 set | grep ARGS_ 14 echo +++++ 15 16 if [ -n "$ARGS_fatal_error" ]; then echo $ARGS_message; exit 1; fi 17 18 echo 19 echo $ARGS_usage
3行目〜8行目は使用する引数のタイプ毎に名前を列挙しシェル変数に格納する。各シェル変数の意味は以下の通り:
シェル変数 | 意味 | 例 |
---|---|---|
ARGS_DEF_opt_char_flg | 文字型フラグオプションの集合 | "aAbh" |
ARGS_DEF_opt_char_val | 文字型バリューオプションの集合 | "io" |
ARGS_DEF_opt_word_flg | 単語型フラグオプションの集合 | "help list" |
ARGS_DEF_opt_word_val | 単語型バリューオプションの集合 | "config logfile" |
ARGS_DEF_arg_namelist | 出現順引数の単語の集合 | "out_file in_file ..." |
ARGS_DEF_arg_num_must | 必須となる出現順引数の数 | 1 |
基本的にこの6つのシェル変数を定義すれば、大体の引数の処理は行なえる。(必要に応じて6つ全てを定義する必要もない。)
これらのシェル変数を定義しておいて shargsを sourceコマンド(上の例では"."コマンド)で呼び出している(10行目)。その後、shargsの動作確認のためにシェル変数を表示している(13行目)。実際にこのプログラムを実行すると次のようになる:
$ ./testargs -a --help -c / -d /dev --config .config output file-1 file-2 file-3ARGS_0=./testargs ←出現順引数 0はコマンド名 ARGS_1=output ←出現順引数 1の値は "output" ARGS_2=file-1 ←出現順引数 2の値は "file-1" ARGS_3=file-2 ←出現順引数 3の値は "file-2" ARGS_4=file-3 ←出現順引数 4の値は "file-3" ARGS__in_file=file-1 ←出現順引数 2の "in_file" の値は "file-1" ARGS__out_file=output ←出現順引数 1の"out_file" の値は "output" ARGS_count=4 ←読み込んだ出現順引数の数は4(output〜file-3) ARGS_error=N ←引数処理でエラーは無かったので "N"(No)となっている ARGS_opt_a=Y ←オプション文字型フラグの "a" は "Y"(Yes)となっている ARGS_opt_c=/ ←オプション文字型バリューの "c" の値は "/" となっている ARGS_opt_config=.config ←オプション単語型バリューの "config" の値は ".config" となっている ARGS_opt_d=/dev ←オプション文字型バリューの "d" の値は "/dev" となっている ARGS_opt_help=Y ←オプション単語型フラグの "help" は "Y"(Yes)となっている ARGS_usage='Usage: ./testargs [-ab] [--help] [-c argument] [-d argument] [--config argument] out_file [in_file ...]'
- +
Usage: ./testargs [-ab] [--help] [-c argument] [-d argument] [--config argument] out_file [in_file ...]
- +
このようにshargsの処理でコマンドの引数を対応する名前のシェル変数に格納するので、あとは必要に応じてシェル変数を呼び出して使うだけである。
なお、shargsでは引数の"名前"に使えるのはアルファベットの小文字と大文字、数字、アンダーバー("_")だけに限定されてしまう。(引数の値にはこういった制限はない。)これは、引数の名前がそのままシェル変数として使われるため、シェル変数の名前に使用できる文字がに限定されてしまうからである。実用上は問題ないかと思う。もし、これ以外の文字を使って引数名の定義をすると、shargsの方でチェックして "ARGS_fatal_error" というシェル変数を定義するので、もし、この変数が定義されていればメッセージ(ARGS_message)を見て引数の名前を再確認する。
また、必須となる出現順引数の数(ARGS_DEF_arg_num_must)が出現順引数の名前リスト(ARGS_DEF_arg_namelist)の要素数よりも多い場合もFatalエラーとした。例えば、必須出現順引数の数が2の場合、名前リストには最低限2つの名前が列挙されていなければならない。"ARGS_fatal_error" というシェル変数が定義されるのでメッセージ(ARGS_message)を見て修正する。
一方、定義は問題ないが、シェルスクリプトの引数の与え方に問題があった場合(例えば、定義されていないオプション文字や名前を使った場合、必須引数に値が与えられなかった等の場合)には "ARGS_error" というシェル変数が定義される。このシェル変数が定義されていたら、Usageメッセージ(コマンドの使い方メッセージ、"ARGS_usage" に格納される)を出力するなどの処理を行なう必要がある。
shargsのプログラミング哲学(?)
shargsは次のような基本的な考え方に基づき作成してみた
- 引数の読み込み処理だけを行い、エラー処理を含めて、その結果に伴うロジックはメインとなるシェルスクリプトの方で行なう。エラーがあった場合、無視して処理を続けるのか、Usageメッセージを出して終了するのか、Usageメッセージの他に何か説明文を出すのかなどの選択ができる。
- 一方でメインのスクリプトにおける引数の操作の負担を極力減らすように、Usageメッセージを自動生成し、さらに任意選択でメッセージの出力までできるように実装した。
- 出来る限り "Bourne Shell"の機能だけで作った。C ShellなどのB Shellから拡張されたbashの機能は極力避けた。(但し、何処までがB Shellオリジナルか分からないので、厳密ではない。"シェル変数の修飾"やsourceもオリジナルのB Shellにはなかったかも。確かsourceはC Shellの機能だったような....)そのため、"for ( ( ; ; ; ) )"やシェル変数の配列は使っていない。プログラミング・テクニックで配列と同等の機能実現している。(B Shellフリークなので。)
- 出来るだけ汎用的に使えるように心がけた
shargsのサンプルと留意点
名前付き出現順引数のシェル変数は"_"が2つ
出現順引数の値は基本的にシェル変数 ARGS_1、ARGS_2と順番に格納されるが、対応する名前付き引数(ARGS_DEF_arg_namelist に定義されているもの)は"ARG__名前"というシェル変数にも格納されるが、その時アンダスコア"_"が2つであることに注意。これは他のシェル変数との競合を避けるために必要だった。
文字型バリューのオプションは混在可能
例えば、
command -a value_a -b value_b
は
command -ab value_a value_b
と入力しても処理出来る。また、文字型フラグとの混在も可能。例えば、
command -a value_a -b value_b -c -d
は
command -abcd value_a value_b
と入力してもOK。勿論順番が変わってもOK
command -dbca value_b value_a
但し、単語型バリューのオプションは一つ一つのオプションと値のペアーで記述する必要がある。つまり、
command --opt_x value_x --opt_y value_y
は次のようには書けない:
command --opt_x --opt_y value_x value_y : これはNG
出現順引数で数が未定のもの
例えばcatやlsのように引数として幾つでも渡せるコマンド作成する場合は、シェル変数 ARGS_DEF_arg_namelist の最後に" ..."を追加しておく。例えば:
ARGS_DEF_arg_namelist="file ..." ARGS_DEF_arg_num_must=0
とすれば出現順引数は無くても良いし、幾つあっても良い。読み込まれた値は ARGS_1 から順番に格納されていく。また、
ARGS_DEF_arg_namelist="file ..." ARGS_DEF_arg_num_must=1
では出現順引数は必ず1つは指定しなければならず、複数あっても良い。
ただし、ARGS_DEF_arg_namelistに" ..."の記述がなくても、出現順引数は全て読み込みエラーは発生しない。" ..."の記述はUsageメッセージの生成のためである。上の2番目のケース(ARGS_DEF_arg_num_must=1のケース)で生成されるUsageメッセージは
Usage: command file [...]
引数名の大文字と小文字は区別する
当たり前のようだが、変数名の大文字と小文字は区別する。例えば、次のような簡単な例で
#! /bin/sh ARGS_DEF_arg_namelist="InputFile inputfile" . /usr/local/bin/shargs echo +++ set | grep ARGS_ echo +++
これを実行すると、次のようになる。
$ ./testargs aaa bbbARGS_0=./testargs ARGS_1=aaa ARGS_2=bbb ARGS__InputFile=aaa ARGS__inputfile=bbb ARGS_count=2 ARGS_error=N ARGS_usage='Usage: ./testargs [InputFile] [inputfile]'
- +
- +
また、argsでは引数名に"-"等が使えないので "input-file"とう名前は使えないが、代わりに "InputFile"という名前を割り振ることができる。
「真」「偽」のシンボルを変更できる
シェル変数"ARGS_DEF_true"、ARGS_DEF_false"の設定により、真偽のシンボルを変えられる。デフォルトでは真は"Y"、偽は"N"にしてあるが、真を"t"、偽を"f"にしてもよいし、"1"、"0"でもよい。(その他何でも構わない。)定義した値に従ってフラグオプションの値にARGS_DEF_trueの値が代入される。
#! /bin/sh ARGS_DEF_true="1" ARGS_DEF_false="0" ARGS_DEF_opt_char_flg="abc" . /usr/local/bin/shargs echo +++ set | grep ARGS_ echo +++
という定義で実行すると以下の様な結果が得られる。
$ ./testargs -a -cARGS_0=./testargs ARGS_count=0 ARGS_error=0 ARGS_opt_a=1 ARGS_opt_c=1 ARGS_usage='Usage: ./testargs [-abc]'
- +
- +
program list of "shargs" without line-number
コピペしやすいように行番号なしのプログラムを載せておく。
#! /bin/sh # The shell script for shell script's argument processing: # shargs Ver 0.003 (2008/07/20) # Copyright (C) 2008 Adsaria # This program is free software; you can redistribute it and/or modify it. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY. # ### ##### Following variables are environment dependent, modify them to your environment. if [ -z "$ARGS_DEF_opt_char_flg" ];then ARGS_DEF_opt_char_flg="" ; fi if [ -z "$ARGS_DEF_opt_char_val" ];then ARGS_DEF_opt_char_val="" ; fi if [ -z "$ARGS_DEF_opt_word_flg" ];then ARGS_DEF_opt_word_flg="" ; fi if [ -z "$ARGS_DEF_opt_word_val" ];then ARGS_DEF_opt_word_val="" ; fi if [ -z "$ARGS_DEF_arg_namelist" ];then ARGS_DEF_arg_namelist="" ; fi if [ -z "$ARGS_DEF_arg_num_must" ];then ARGS_DEF_arg_num_must=0 ; fi if [ -z "$ARGS_DEF_true" ]; then ARGS_DEF_true="Y" ; fi if [ -z "$ARGS_DEF_false" ]; then ARGS_DEF_false="N" ; fi ##### ### # ARGS_count=0 ARGS_0=$0 ARGS_error=$ARGS_DEF_false ##### Checking the characters of option names and argument names ARGS_tmp_1=$ARGS_DEF_opt_char_flg ARGS_tmp_1="$ARGS_tmp_1 $ARGS_DEF_opt_char_val" ARGS_tmp_1="$ARGS_tmp_1 $ARGS_DEF_opt_word_flg" ARGS_tmp_1="$ARGS_tmp_1 $ARGS_DEF_opt_word_val" ARGS_tmp_1="$ARGS_tmp_1 `echo $ARGS_DEF_arg_namelist | sed 's/ \.\.\.$//'`" ARGS_tmp_1=`echo -n $ARGS_tmp_1 | tr -d " a-zA-Z0-9_" | wc -c` if [ $ARGS_tmp_1 -ne 0 ]; then ARGS_fatal_error=$ARGS_DEF_true ARGS_message="shargs: the argument's names should be the letter of \"a-z\" \"A-Z\" \"0-9\"and \"_\"." fi if [ $ARGS_DEF_arg_num_must -gt `echo $ARGS_DEF_arg_namelist | wc -w` ]; then ARGS_fatal_error=$ARGS_DEF_true ARGS_message="shargs: At least the number of mandatory arguments should be in arguments list." fi if [ "$ARGS_fatal_error" = $ARGS_DEF_true ]; then ARGS_error=$ARGS_DEF_true else ##### The body of processing the arguments while [ $# -ne 0 -a $ARGS_error = $ARGS_DEF_false ]; do case $1 in --*) ARGS_tmp_current=${1:2} ARGS_tmp_1=`echo "$ARGS_DEF_opt_word_val" | wc -w` ARGS_tmp_2=`echo "${ARGS_DEF_opt_word_val/"$ARGS_tmp_current"/}" | wc -w` if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then if [ $# -eq 1 ]; then ARGS_error=$ARGS_DEF_true; break; fi eval "ARGS_opt_$ARGS_tmp_current=\"$2\"" shift; shift else ARGS_tmp_1=`echo "$ARGS_DEF_opt_word_flg" | wc -w` ARGS_tmp_2=`echo "${ARGS_DEF_opt_word_flg/"$ARGS_tmp_current"/}" | wc -w` if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then eval "ARGS_opt_$ARGS_tmp_current=$ARGS_DEF_true" shift else ARGS_error=$ARGS_DEF_true shift break fi fi ;; -[^-]*) ARGS_tmp_current=${1:1} ARGS_tmp_index=0 while [ $ARGS_tmp_index -lt ${#ARGS_tmp_current} ]; do ARGS_tmp_char=${ARGS_tmp_current:$ARGS_tmp_index:1} ARGS_tmp_1=${#ARGS_DEF_opt_char_val} ARGS_tmp_2=`echo -n ${ARGS_DEF_opt_char_val/$ARGS_tmp_char/} | wc -c` if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then if [ $# -eq 1 ]; then ARGS_error=$ARGS_DEF_true; break; fi eval "ARGS_opt_$ARGS_tmp_char=\"$2\"" shift else ARGS_tmp_1=${#ARGS_DEF_opt_char_flg} ARGS_tmp_2=`echo -n ${ARGS_DEF_opt_char_flg/$ARGS_tmp_char/} | wc -c` if [ $ARGS_tmp_1 -ne $ARGS_tmp_2 ]; then eval "ARGS_opt_$ARGS_tmp_char=$ARGS_DEF_true" else ARGS_error=$ARGS_DEF_true break fi fi ARGS_tmp_index=`expr $ARGS_tmp_index + 1` done shift if [ "$ARGS_error" = $ARGS_DEF_true ]; then break; fi ;; *) ARGS_count=`expr $ARGS_count + 1` eval "ARGS_$ARGS_count=\"$1\"" ARGS_tmp_word_name=`echo -n "$ARGS_DEF_arg_namelist" | awk "{print \\$$ARGS_count}"` if [ "$ARGS_tmp_word_name" != "" -a "$ARGS_tmp_word_name" != "..." ]; then eval "ARGS__$ARGS_tmp_word_name=\"$1\"" fi shift ;; esac done if [ $ARGS_count -lt $ARGS_DEF_arg_num_must ]; then ARGS_error=$ARGS_DEF_true; fi ##### Message generation part ARGS_usage="Usage: $ARGS_0" if [ -n "$ARGS_DEF_opt_char_flg" ]; then ARGS_usage="$ARGS_usage [-$ARGS_DEF_opt_char_flg]" fi for ARGS_tmp_word in $ARGS_DEF_opt_word_flg ; do ARGS_usage="$ARGS_usage [--$ARGS_tmp_word]" done ARGS_tmp_index=0 while [ $ARGS_tmp_index -lt ${#ARGS_DEF_opt_char_val} ]; do ARGS_tmp_char=${ARGS_DEF_opt_char_val:$ARGS_tmp_index:1} ARGS_usage="$ARGS_usage [-$ARGS_tmp_char argument]" ARGS_tmp_index=`expr $ARGS_tmp_index + 1` done for ARGS_tmp_word in $ARGS_DEF_opt_word_val ; do ARGS_usage="$ARGS_usage [--$ARGS_tmp_word argument]" done ARGS_tmp_count=0 ARGS_tmp_first_opt=$ARGS_DEF_true for ARGS_tmp_word in $ARGS_DEF_arg_namelist ; do if [ $ARGS_tmp_count -lt $ARGS_DEF_arg_num_must ]; then ARGS_usage="$ARGS_usage $ARGS_tmp_word" else if [ $ARGS_tmp_first_opt = $ARGS_DEF_true ]; then ARGS_tmp_first_opt=$ARGS_DEF_false ARGS_usage="$ARGS_usage [$ARGS_tmp_word" else if [ $ARGS_tmp_word = "..." ]; then ARGS_usage="$ARGS_usage $ARGS_tmp_word" else ARGS_usage="$ARGS_usage] [$ARGS_tmp_word" fi fi fi let ARGS_tmp_count++ done if [ $ARGS_tmp_first_opt = $ARGS_DEF_false ]; then ARGS_usage="$ARGS_usage]"; fi ##### Display the message if required if [ "$ARGS_error" = $ARGS_DEF_true ]; then if [ "$ARGS_DEF_show_message" = $ARGS_DEF_true ];then echo $ARGS_usage fi fi fi ##### Unset the temporary shell variables unset ${!ARGS_tmp*} unset ${!ARGS_DEF*}
shargsの解説
- 15行目〜23行目:引数の定義。後述する「shargsの使い方」にあるように、shargsを呼び出すメインのシェルスクリプトで定義されている場合は、その定義を使用する。shargsを組み込んで使う場合はここの定義を適宜変更して使う。
- 35行目〜49行目:引数の定義に間違いが無いか確認している。
- 57行目以降:引数の定義にエラーがなければ以降が処理の本体となる。
- 57行目〜118行目:引数の読み込み処理。
- 59行目〜79行目:単語型オプションの処理、61行目〜66行目は単語型バリューオプション、68行目〜72行目は単語型フラグオプションの処理
- 80行目〜105行目:文字型オプションの処理。85行目〜90行目は文字型バリューオプション、92行目〜95行目は文字型フラグオプションの処理。
- 106行目〜114行目:出現順引数の処理。
- 124行目〜164行目:Usageメッセージの生成。
- 168行目〜173行目:任意選択によるUsageメッセージの出力。
- 179行目〜180行目:不必要なシェル変数のクリア。
追記(2008/04/05)
getopt(1)(バイナリコマンド)、getopts(1)(bash組み込み)っていうのがあった。恥ずかしながら知らなかった。やっぱり欲しい機能は誰でも同じみたいだ。アプローチは似ているが、getopt系だとその後、スクリプト側で更に後処理(シェル変数への格納など)が必要になって来る見たいだが、この辺は趣味の問題か。多分、getoptコマンドもgetoptsコマンドもC言語の引数処理関数getopt(3)の影響を強く受けているように思われる。Cで引数を処理する汎用関数を作るとなるとgetoptのようになると思う。しかし、シェルスクリプト内で使うことを前提としたシェルスクリプトで組むのであれば、shargsのような実装もあっても良いのでは?と思う。
もし、私が先にgetopt(3)を知っていたら固定観念からgetoptsのような実装になったかも知れない。(もっとも、getoptsを知っていたらshargsは作らなかったかも知れない。もしくは、プリプロセッサ的に使ったか。)