シェルスクリプトで-vや-f fileのようなオプションを扱いたい

シェルスクリプトに-v-f fileのようなオプションを渡したい場合、$1$2を自力で判定すると、オプションの順序やまとめ書きへの対応が煩雑になる。シェルのビルトインコマンドgetoptsを使うと、UNIXコマンドと同じ形式のオプション解析を簡潔に書ける。

getoptsの基本

getoptswhileループと組み合わせて使う。

#!/bin/bash

while getopts "vf:" opt; do
  case "$opt" in
    v) verbose=1 ;;
    f) file="$OPTARG" ;;
  esac
done

echo "verbose=$verbose"
echo "file=$file"

第1引数の"vf:"はオプション文字列で、受け付けるオプション文字を並べる。文字の後ろにコロンを付けると、そのオプションが引数を取る。上記の例では-vは引数なし、-fは引数ありとして解釈される。

第2引数のoptは、解析したオプション文字を格納する変数名である。getoptsは呼び出しごとにオプションを1つずつ読み進め、次のオプションがなくなると終了ステータス1を返してループを抜ける。

オプションの引数はシェル変数OPTARGに入る。上記のスクリプトをexample.shとして保存し、オプションを付けて実行すると次の結果になる。

$ ./example.sh -v -f data.txt
verbose=1
file=data.txt

オプション以外の引数を取り出す

オプションを解析した後、ファイル名などの操作対象を受け取りたい場合はシェル変数OPTINDを使う。OPTINDには次に処理する引数の位置が入るため、OPTIND - 1個の引数をshiftすればオプション部分を捨てられる。

while getopts "vf:" opt; do
  case "$opt" in
    v) verbose=1 ;;
    f) file="$OPTARG" ;;
  esac
done
shift $((OPTIND - 1))

echo "rest=$*"
$ ./example.sh -v -f data.txt extra1 extra2
verbose=1
file=data.txt
rest=extra1 extra2

エラー処理

オプション文字列の書き方によって、未定義のオプションや引数不足を検知したときの挙動が変わる。

デフォルトモード

オプション文字列の先頭にコロンを付けない場合、getopts自身がエラーメッセージを標準エラー出力へ表示する。変数には?が入る。

$ ./example.sh -x
./example.sh: illegal option -- x
verbose=
file=

$ ./example.sh -f
./example.sh: option requires an argument -- f
verbose=
file=

エラーを検知してもループは止まらないため、処理を中断したい場合はcase文で?を捕まえてexitする。なお、デフォルトモードのエラー時はOPTARGがunsetになる。set -uを指定したスクリプトがOPTARGを参照するとunbound variableエラーが発生する。

サイレントモード

オプション文字列の先頭にコロンを付けると、getoptsはメッセージを表示せず、エラーの種類を呼び出し側へ伝える。

ケース変数の値OPTARG
未定義のオプション?オプション文字
引数が不足:オプション文字

2つのケースをcase文で分ければ、独自のエラーメッセージを出せる。

while getopts ":vf:" opt; do
  case "$opt" in
    v) verbose=1 ;;
    f) file="$OPTARG" ;;
    :) echo "Error: -$OPTARG requires an argument" >&2; exit 1 ;;
    \?) echo "Error: unknown option -$OPTARG" >&2; exit 1 ;;
  esac
done
$ ./example.sh -x
Error: unknown option -x

$ ./example.sh -f
Error: -f requires an argument

caseのパターンとして?は任意の1文字にマッチするため、バックスラッシュでエスケープして\?と書く。

シェルによるエラーメッセージの違い

デフォルトモードのエラーメッセージの文面はシェルの実装ごとに異なる。同じスクリプトをDebianの/bin/shであるdashで実行すると、bashとは別の文面を出力する。

$ sh example.sh -x
Illegal option -x
verbose=
file=

文面を揃えたい場合や、使い方の表示とあわせて独自のエラーを出したい場合はサイレントモードを使う。

オプションのまとめ書きと--

引数を取らないオプションは連結して書ける。末尾に限り、引数を取るオプションも置ける。

$ ./example.sh -vf data.txt
verbose=1
file=data.txt

--を渡すと、そこでオプション解析を打ち切る。ハイフンで始まるファイル名を操作対象として渡す場合に使う。

$ ./example.sh -v -- -f data.txt
verbose=1
file=
rest=-f data.txt

具体例

バックアップスクリプトを想定し、オプションのデフォルト値、使い方の表示、エラー処理をまとめると次のとおり。

#!/bin/bash
set -eu

usage() {
  echo "Usage: $0 [-v] [-o output_dir] [-n count] target..." >&2
  exit "${1:-1}"
}

verbose=0
output_dir="/var/backup"
count=3

while getopts ":vo:n:h" opt; do
  case "$opt" in
    v) verbose=1 ;;
    o) output_dir="$OPTARG" ;;
    n) count="$OPTARG" ;;
    h) usage 0 ;;
    :) echo "Error: -$OPTARG requires an argument" >&2; usage ;;
    \?) echo "Error: unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))

if [ $# -eq 0 ]; then
  echo "Error: target is required" >&2
  usage
fi

echo "verbose=$verbose output_dir=$output_dir count=$count targets=$*"

-hを指定したときだけ終了ステータス0でusageを抜けるように、exit "${1:-1}"でデフォルト値を切り替えている。

オプションを指定して実行すると、対応する変数へ値が入る。

$ ./backup.sh -v -o /tmp/bk -n 5 /etc /home
verbose=1 output_dir=/tmp/bk count=5 targets=/etc /home

引数を取らない-vと引数を取る-nはまとめて書ける。指定しなかったオプションはデフォルト値のままになる。

$ ./backup.sh -vn 5 /etc
verbose=1 output_dir=/var/backup count=5 targets=/etc

エラー時は使い方を表示して終了する。

$ ./backup.sh -o
Error: -o requires an argument
Usage: ./backup.sh [-v] [-o output_dir] [-n count] target...

$ ./backup.sh -z /etc
Error: unknown option -z
Usage: ./backup.sh [-v] [-o output_dir] [-n count] target...

なお、-nに渡された値をそのまま変数へ代入しているため、数値以外も受け付ける。実運用では値の妥当性を別途チェックする。

getoptsの制限と注意点

ロングオプションは扱えない

getoptsが扱えるのは1文字の短いオプションのみで、--verboseのようなロングオプションには対応しない。--verboseを渡すと-verboseが個別のオプション文字として解釈される。

$ ./example.sh --verbose
./example.sh: illegal option -- -
./example.sh: illegal option -- e
./example.sh: illegal option -- r
./example.sh: illegal option -- b
./example.sh: illegal option -- o
./example.sh: illegal option -- s
./example.sh: illegal option -- e

vだけは定義済みのオプションとして解釈されるため、エラーメッセージが出ない。

ロングオプションを扱いたい場合は、util-linuxに含まれる外部コマンドのgetoptを使うか、whileループとcase文により自前で解析する。ただしmacOS標準の/usr/bin/getoptはBSD版でロングオプションに対応しないため、移植性を考えるなら自前の解析が無難である。

オプションは操作対象より前に置く

オプション以外の引数が現れた時点で解析を終了するため、後ろに書いたオプションは解析されない。

$ ./example.sh file.txt -v
verbose=
file=
rest=file.txt -v

GNU版のgetoptは非オプション引数を並べ替えて後ろのオプションも解析するが、getoptsは並べ替えをしない。

OPTINDは自動で戻らない

OPTINDはシェルの起動時に1へ初期化されるだけで、getoptsのループを抜けても元へ戻らない。同じシェルの中でgetoptsを2回使うと、2回目は何も解析しない。

parse() {
  while getopts "v" opt; do echo "got $opt"; done
}

parse -v          # got v
parse -v          # 何も出力されない
OPTIND=1
parse -v          # got v

複数回解析する場合は、事前にOPTIND=1を代入する。関数の中で使う場合はlocal OPTINDと宣言すれば、関数を抜けたときに元の値へ戻る。

参考