シェルスクリプトで-vや-f fileのようなオプションを扱いたい
シェルスクリプトに-vや-f fileのようなオプションを渡したい場合、$1や$2を自力で判定すると、オプションの順序やまとめ書きへの対応が煩雑になる。シェルのビルトインコマンドgetoptsを使うと、UNIXコマンドと同じ形式のオプション解析を簡潔に書ける。
getoptsの基本
getoptsはwhileループと組み合わせて使う。
#!/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を渡すと-、v、e、r、b、o、s、eが個別のオプション文字として解釈される。
$ ./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と宣言すれば、関数を抜けたときに元の値へ戻る。
参考
- Bash Reference Manual - Bourne Shell Builtins
- 【シェルスクリプト】変数や引数のデフォルト値 ${var:-default} ${1:-default}
- 【Shell Script】set -euo pipefailでエラーに強いシェルスクリプトを書く
