getoptsでロングオプションその他

/ bash

1文字オプション、ロングオプション、イコールを用いたロングオプション、後置き、中置きオプションをすべて考慮したgetoptsのすぐに使える使用例。

オプション例

例として次のようなオプションを受け入れることにする。

オプション指定 キー 引数
-q / --quiet quite 無し
-v / --verbose verbose 無し
-qv quite / verbose 無し
-ofilename / -o filename / --output filename / --output=filename output filename
-ifilename / -i filename / --input filename / --input=filename input filename

これでほぼすべてのケースを含んでいるはず。

スクリプト

上記のオプション例をパースするプログラム。

#!/usr/bin/env bash

while getopts ":-:qvo:i:" key; do

    # part1
    if [[ $key == - ]]; then
        key=${OPTARG%%=*}
        if [[ $key == "$OPTARG" ]]; then
            keyarg=${!OPTIND}
        else
            keyarg=${OPTARG#*=}
        fi
    else
        keyarg=$OPTARG
    fi

    # part2
    case $key in
    output | input)
        if [[ $key == "$OPTARG" ]]; then
            OPTIND=$((OPTIND + 1))
        fi
        ;;&
    q | quiet)
        option_quiet=1
        ;;
    v | verbose)
        option_verbose=1
        ;;
    o | output)
        option_output=$keyarg
        ;;
    i | input)
        option_input=$keyarg
        ;;
    ? | *)
        echo "help message"
        exit 0
        ;;
    esac

    # part3
    while [[ -n ${!OPTIND} && ${!OPTIND} != -* ]]; do
        args+=(${!OPTIND})
        OPTIND=$((OPTIND + 1))
    done
done

set -- "${args[@]}"

動作テスト

テスト用に上記スクリプトの末尾に次を追加して動作させてみる。

echo "option_quiet:$option_quiet"
echo "option_verbose:$option_verbose"
echo "option_output:$option_output"
echo "option_input:$option_input"
echo "args:$@"

すべてのパターンを網羅はできていないが適当に動かしてみる。

./getopts.sh -qv -ifilename -ofilename a b c
option_quiet:1
option_verbose:1
option_output:filename
option_input:filename
args:a b c
./getopts.sh -i file1 -o file2 a b c --quiet --verbose  d e
option_quiet:1
option_verbose:1
option_output:file2
option_input:file1
args:a b c d e
./getopts.sh --input=file1 a b c --quiet --verbose --output=file2 a c
option_quiet:1
option_verbose:1
option_output:file2
option_input:file1
args:a b c a c

想定通りに動いていることが確認できる。

解説

本スクリプトを応用して別なオプションを受け入れるよう改変を加えるために、説明が必要と思われる部分について解説していく。

getopts

getoptsの第1引数は":-:qvo:i:"となっているが、これは基本的にアルファベット1文字+コロンの有無を必要回数だけ続けるというルールになっている。この場合は分解すると、

  • 先頭コロンあり
  • ハイフン+コロンあり
  • q+コロンなし
  • v+コロンなし
  • o+コロンあり
  • i+コロンあり

となる。先頭コロンなしの場合、未知のオプションキーに遭遇した際にgetopts側でエラーが表示される。本スクリプトは未知のオプション遭遇時にはヘルプメッセージを表示することを想定しているため行頭コロンありとしている。

以降は機械的に「アルファベット1文字+必要ならコロン」の組み合わせをつなげるだけである。コロンありはそのオプションに引数があることを意味する。「ハイフン+コロンあり」はロングオプションを処理するために必要で、そうでなければ不要である。

本スクリプトではoキーとiキーは1文字キーとロングオプションの2通りがあるがinput/outputはロングオプションのみを受け付けるとした場合はgetoptsの第1引数での指定は不要である(ハイフンで処理される)。

getoptsはあくまで1文字キーのオプションを処理してくれるもの。今回の例だと、-qvはqキーとvキーだが、-iqvはiキーの引数qvである、などの解釈をそれなりにやってくれるため自前のパースよりは効率が良い。

whileのpart1

if [[ $key == - ]]; then
    key=${OPTARG%%=*}
    if [[ $key == "$OPTARG" ]]; then
        keyarg=${!OPTIND}
    else
        keyarg=${OPTARG#*=}
    fi
else
    keyarg=$OPTARG
fi

この部分はロングオプションを受け付けない場合は単にkeyarg=$OPTARGとすればいい(直接$OPTARGを参照するなら何もなくていい)。ロングオプションの場合はオプションキー$keyがハイフンの時であるのでそこだけ場合分けをする。イコールによるロングオプションを受け付けない場合は、key=${OPTARG}keyarg=${!OPTIND}のみでよい。

さらなる場合分けでイコールによるロングオプションを処理している。イコールによるロングオプションでは$OPTARGoutput=filenameといった文字列が格納されているのでこれをイコールでkeyとkeyargに分割する。

key=${OPTARG%%=*}
keyarg=${OPTARG#*=}

これはシェル展開で後方からイコールが現れるまでをカット(最長一致)、前方からイコールが現れるまでをカット(最短一致)しており、結果としてそれぞれにイコールの前部分と後ろ部分が格納される。

イコールが含まれていない場合は$key=$OPTARGとなるのでそこで場合分けをしている。

whileのpart2

ここでは特筆すべきところは以下。

output | input)
    if [[ $key == "$OPTARG" ]]; then
        OPTIND=$((OPTIND + 1))
    fi
    ;;&

イコールを用いないロングオプションを使用しないならこの部分はまるまる不要である。

そうでない場合、caseの冒頭ですべてのロングオプションを拾うような項目を用意する必要がある。イコールを用いないロングオプションでは${!OPTIND}をkeyargのために消費しているので次に処理されるオプションのインデックスである$OPTINDに1加算しておく必要がある。イコールにの有無による場合分けが例によって必要になる。

その他の箇所は極めて自然だろう。ここに関する例は一般的なgetoptsの解説でもよく見ることがある。

caseの最後はbreakを意味する;;ではなく、次にマッチするcaseを処理せよ、を意味する;;&なことは注意。

whileのpart3

後置きと中置きオプションを考慮しないならこの部分はまるまる不要である。

getoptsは$OPTINDがで表される次に処理すべき引数がハイフンから始まっていないとそこで処理を終了してしまうため、通常のオプションをargsに貯めながら中置き・後置きのオプションのある所まで$OPTINDを進めておく。これによって、中置き・後置きのオプションが次のwhileでgetoptsによって処理される。

whileのあと

$argsに通常の引数が貯められている。set --であたかも普通の引数のようにしてその後のプログラムを書いてもよいし直接$argsを見てもいい。後置き・中置きオプションを考慮しない場合(part3をまるまるカットした場合)はset --したときと同じ動作はshift $((OPTIND - 1))で達成できる。