シェルスクリプトの引数処理をモジュール化

別にシェルスクリプトに限らないが、プログラムを作る時に引数の処理は大体、後回しになってしまう。そうするとプログラムを書いている途中では定数(や特定の値を代入した変数)を使ってハードコーディングしてしまって、後で直すのが なお更面倒になってしまう。そこで次のような組み込み用のシェルスクリプトを考えた。

  • オプションや引数の値をそれぞれ対応するシェル変数に取り込んでくれる
  • オプションや引数を出来る限りシンプルに定義でき、直ぐに変更できる
  • シェルスクリプト本体で行なう作業は最小限にできる(出来る限り自動化する)
  • 定義に従ってUsageメッセージも自動生成して、定義を変更してもプログラムを変更しなくて済む
  • 設定によっては色々と柔軟な付加機能を持つ
  • LinuxFedora 8)の標準機能だけで実現する

引数の処理をしてくれるライブラリとか関数とかモジュールとかがあれば良いのだけども、引数の処理はプログラム毎に違うのでライブラリ的に作るのはちょっと大変。と思っていたがシェルスクリプトに限定すればある程度汎用的に作れるのではと思って作って見た。(探せばあるのだろうが、シェルプログラミングの練習も兼ねて作成してみた。「追記(2008/04/05)」参照)

UNIXLinux)流の引数やオプションの定義にもとづいて、引数を次のように分類する:

  • 引数は"-"、"--"によって指定されるオプション(Option)と、出現の順番で指定されるもの(Ordered)がある
  • "-"が先頭にあるのは"文字型"のオプション
  • "--"が先頭にあるのは"単語型"のオプション
  • オプションは、値を持たないフラグ型と値を持つバリュー型がある
  • 出現順の引数は必ず必要なものと任意のものがある(必須引数の数を指定する)

これに基本ルールとして次の6項目を決めれば引数を(ある程度)自動的に処理できるシェルスクリプトが作れる。(カッコ内は下の例との対応)

  1. 文字型フラグオプションの文字の集合 ("ab")
  2. 文字型バリューオプションの文字の集合 ("cd")
  3. 単語型フラグオプションの単語の集合("help")
  4. 単語型バリューオプションの単語の集合("config")
  5. 出現順引数の単語の集合("out_file in_file ...")
  6. 必須となる出現順引数の数("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-3
        1. +
ARGS_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 ...]'
        1. +
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 bbb
    1. +
ARGS_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]'
    1. +

また、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 -c
    1. +
ARGS_0=./testargs ARGS_count=0 ARGS_error=0 ARGS_opt_a=1 ARGS_opt_c=1 ARGS_usage='Usage: ./testargs [-abc]'
    1. +

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行目:引数の定義に間違いが無いか確認している。
    • 35行目〜44行目:引数の名前の文字として"a-z"、"A-Z"、"0-9"、"_"(アンダースコア)だけが利用できる。
    • 46行目〜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/07/20)ソフトウェアの名前を"shargs"に変更した

ソフトウェアの名前を args から shargs に変更した。Solarisに args というソフトウェアがあるので名前のコンフリクトが起きないように変更した。変更はソフトウェアの名前のみで、環境変数の名前はそのままにしておいたので、呼び出すプログラムは、". /usr/local/bin/args" から ". /usr/local/bin/shargs" と1行変更すればいい。
なお、このページもargsからshargsへ変更してたが、もし"args"という表記が残っていたら、適宜読み替えてほしい。

追記(2008/04/05)

getopt(1)(バイナリコマンド)、getopts(1)(bash組み込み)っていうのがあった。恥ずかしながら知らなかった。やっぱり欲しい機能は誰でも同じみたいだ。アプローチは似ているが、getopt系だとその後、スクリプト側で更に後処理(シェル変数への格納など)が必要になって来る見たいだが、この辺は趣味の問題か。多分、getoptコマンドもgetoptsコマンドもC言語の引数処理関数getopt(3)の影響を強く受けているように思われる。Cで引数を処理する汎用関数を作るとなるとgetoptのようになると思う。しかし、シェルスクリプト内で使うことを前提としたシェルスクリプトで組むのであれば、shargsのような実装もあっても良いのでは?と思う。
もし、私が先にgetopt(3)を知っていたら固定観念からgetoptsのような実装になったかも知れない。(もっとも、getoptsを知っていたらshargsは作らなかったかも知れない。もしくは、プリプロセッサ的に使ったか。)