bash 用コマンドラインオプションパーサ

bash で使えるコマンドラインオプションパーサとしては,組み込みの getopts とかコマンド getopt が代表的だが,もっと手軽に使えるのはないものか.ということで作ってみた.

細かいことは置いておいて*1,まず使用例を.

使用例1

#!/bin/bash
source ~/lib/bash/option-parser.sh
parse_opts "$@" # パース

optx=`opt x 10`
opty=`opt y hoge`

# ...

2〜3行目がパースを行っている部分, `opt x 10` は -x というオプションの値を取得する関数(10 はデフォルト値).

./test.sh -x 100

を実行すると, `opt x 10` は 100 を返し,`opt y hoge` はデフォルトの hoge を返す.ちなみに `opt y` のようにデフォルト値を省略した場合は空白を返す.
単純.

使用例2

"-help" のような,単体で指定されるオプションもパースするには:

#!/bin/bash
source ~/lib/bash/option-parser.sh
set_single_opts help
parse_opts "$@" # パース

if [ `opt help 0` -eq 1 ]; then
  echo "つかいかた!"
  exit 1
fi

optx=`opt x 10`
opty=`opt y hoge`

# ...

3行目で,単体で指定されるオプションを指定している(複数でもOK).この場合,`opt help 0` はコマンドラインで指定されていれば 1 を,そうでなければ 0 を返す(デフォルト値の指定が無ければ空白).

./test.sh -x 100 -help

で「つかいかた!」と出力される.

使用例3

rm コマンドの "rm -i a.txt b.txt" みたいなオプションをパースするには:

#!/bin/bash
source ~/lib/bash/option-parser.sh
set_single_opts help i
parse_opts "$@" # パース

echo "number of fargs=${#fargs[@]}"
echo "fargs[0]=${fargs[0]}"
echo "fargs[1]=${fargs[1]}"

ハイフンなしのコマンドライン引数("rm -i a.txt b.txt" の a.txt や b.txt)は,すべて fargs という配列に格納される.よって ${#fargs[@]} で要素数の取得(2)が,${fargs[0]} で最初の要素が取得(a.txt)できる.もちろん,for ですべての要素に対する処理も可能だ:

for ((i=0;i<${#fargs[@]};i++)); do
  echo "fargs[$i]=${fargs[$i]}"
done

マニュアル

option-parser.sh スクリプトで解析できるのは

  • single: -XX
  • pair: -XX YY
  • floating: YY

の3タイプのオプション.これらのオプションが,いくつでも,どんな順で並んでいてもよい.

まず,

source ~/lib/bash/option-parser.sh
set_single_opts AAA BBB
parse_opts "$@"

の順でパーサを実行する. set_single_opts は single (-XX) タイプのオプションを指定するコマンド(いくつでも指定可)で,指定しなければ pair (-XX YY) タイプのオプションとして認識されてしまう.

パーサを実行後,オプションの内容が以下のように取得できる.

  • pair (-XX YY) オプションは,`opt XX` で YY を取り出せる.コマンドラインで指定が無い場合,空白
  • single (-XX) オプションは,`opt XX` で 1 が返る.コマンドラインで指定が無い場合,空白
  • floating (YY) オプションは,配列 fargs に,出現順に YY が格納される

opt コマンド(関数)は opt OPTION_NAME DEFAULT のようにデフォルト値 DEFAULT を指定できる.この場合,コマンドライン引数にオプションが無ければ opt 関数は DEFAULT を返す(pair でも single でも同じ).

NOTE: オプション名の記号はすべてアンダーライン(_)と同一視される.例えば -x+y と -x-y はいずれも内部で -x_y に置き換えられる.

NOTE: -- 以降はすべて fargs に格納される(i.e. floating (YY) オプションとみなされる).ただし,-- が pair (-XX YY) オプションの YY に位置する場合,オプション -XX の内容として扱われる.

スクリプトソース (option-parser.sh)

修正@Oct.14,2009: 一部の echo を printf -- に置き換えた(cf. echo -E トラブル).
追加@Oct.14,2009: -- 以降はすべて fargs に格納されるようにした.

#!/bin/bash
# コマンドラインオプション解析スクリプト
# 解析できるオプションは以下の3タイプ:
#   single:   -XX
#   pair:     -XX YY
#   floating: YY
# これらのオプションが,いくつでも,どんな順で並んでいてもよい
# usage:
#   source ~/lib/bash/option-parser.sh
#   set_single_opts AAA BBB # single (-XX) オプションを指定
#   parse_opts "$@" # パース
# pair (-XX YY) オプションは,解析(parse_opts)後 `opt XX` で YY を取り出せる
# single (-XX) オプションは, `opt XX` で 1 が返る (オプションで指定されていなければ空白)
# single (-XX) のオプションを使う場合は, parse_opts を呼び出す前に set_single_opts を使う必要がある
# floating (YY) オプションは,配列 fargs に YY が格納される
# NOTE opt でコマンドライン引数にないオプションを取得しようとした場合 空白 "" が返る
# NOTE オプション中の記号はすべてアンダーライン(_)と同一視される
# NOTE -- 以降はすべて fargs に格納される(i.e. floating (YY) オプションとみなされる)

# オプション名を内部名に変換する
function option_name_conv() # OPTION_NAME
{
  printf -- "$1" | sed 's/[^a-zA-Z0-9_]/_/g'
}

# OPTION_NAME の内容を取り出す
# (何も格納されない場合は DEFAULT_VALUE が得られる)
function opt() # OPTION_NAME [DEFAULT_VALUE]
{
  eval "local res=\"\$commandline_option_`option_name_conv $1`\""
  if [ "$res" == "" ] && [ "$2" != "" ]; then printf -- "$2"; fi
  printf -- "$res"
}

# 単体で使われるオプション(-XX タイプ)を指定する
function set_single_opts() # OPTION_NAME [OPTION_NAME [OPTION_NAME] ...]
{
  local N=$#
  for ((i=0; i<$N; i++)); do
    eval "commandline_option_single_`option_name_conv $1`=1"
    shift
  done
}

# 使用する変数を指定する
function using_opts() # OPTION_NAME [OPTION_NAME [OPTION_NAME] ...]
{
  local N=$#
  for ((i=0; i<$N; i++)); do
    eval "commandline_option_used_`option_name_conv $1`=1"
    shift
  done
}

# 使われていないオプション(-XX YY タイプのみ)の一覧を出力する
# 使われていないオプションがあれば 1, なければ 0 を返す
function check_unused_opts()
{
  local unused_opt_exists=0
  for on in ${commandline_option_list[@]};do
    eval "local used=\$commandline_option_used_`option_name_conv $on`"
    if [ $used -eq 0 ];then
      unused_opt_exists=1
      eval "res=\"\$commandline_option_`option_name_conv $on`\""
      echo "unused option: -$on $res"
    fi
  done
  return $unused_opt_exists
}

# すべての -XX YY タイプのオプションを出力
function list_options()
{
  for on in ${commandline_option_list[@]};do
    eval "local res=\"\$commandline_option_`option_name_conv $on`\""
    echo "$on=$res"
  done
}

function __register_opt()  # optname optcontents
{
  local optname=$1
  local ioptname=`option_name_conv $1`
  local optcontents=$2
  local duplicated=0
  if [ $# -ne 2 ];then echo "invalid options for __register_opt! (bug!)"; fi
  eval "local oldcontents=\"\$commandline_option_$ioptname\""
  if [ "$oldcontents" != "" ]; then
    echo "warning: option -$optname is redefined (note: internal name the option is -$ioptname)"
    duplicated=1
  fi
  eval "commandline_option_$ioptname='$optcontents'"
  eval "commandline_option_used_$ioptname=0"
  if [ $duplicated -ne 1 ];then
    commandline_option_list[$commandline_option_count]=$optname
    commandline_option_count=$((commandline_option_count+1))
  fi
}

function __register_fargs()  # optcontents
{
  if [ $# -ne 1 ];then echo "invalid options for __register_fargs! (bug!)"; fi
  fargs[$fargs_count]="$1"
  fargs_count=$((fargs_count+1))
}

function parse_opts()
{
  local optname=''
  local commandline_option_count=0
  local fargs_count=0
  local flag_ign_opt=0
  local N=$#
  for ((i=0; i<$N; i++)); do
    if [ "$optname" == "" ];then
      if [ $flag_ign_opt -eq 1 ];then
        __register_fargs "$1"
      elif [ "$1" == "--" ];then
        flag_ign_opt=1
      elif [ "`printf -- $1|sed 's/^\-.\+//g'`" == "" ];then
        optname="`printf -- $1|sed 's/^\-//g'`"
        eval "local is_single=\$commandline_option_single_`option_name_conv $optname`"
        if [ "$is_single" == "1" ];then
          __register_opt $optname 1
          optname=""
        fi
      else
        __register_fargs "$1"
      fi
    else
      local optcontents=$1
      __register_opt $optname "$optcontents"
      optname=""
    fi
    shift
  done
  if [ "$optname" != "" ];then
    echo "incomplete option: -$optname"
    exit 1
  fi
}

このソースのパス(上の例では ~/lib/bash/option-parser.sh)を source コマンドで呼び出して使う.



*1:本当は getopts とか getopt の存在を知らなかったという事実もほっておきます.ブログ書こうとして調べたら,すぐ見つかったよ.