こんにちは、山田ハヤオです。シェルスクリプトでコマンドラインツールを作ります。
コマンドラインツールを作る上で欠かせないのが引数解析です。
世の中の言語にはコマンドライン解析を行うライブラリがたくさんあります。
そしてそれはBashも例外ではありません。
この記事で言いたいこと
いろいろ前提知識としてのシェルスクリプトでの引数解析の方法を書きます。
ですが、この記事で言いたいことは結局「自前で引数解析のコード書いたほうが良いよ」ということです。時間がない人は下までスクロールしてください。
今回はGNUでもBSDでも動き、getoptの代替になるparseoptsという引数解析のための関数を作成しました。
組み込みの引数解析用コマンドgetopts
Bashの組み込みコマンドとして引数解析に使えるgetoptsというコマンドがあります。
以下のような感じでgetoptsはwhileとcaseを組み合わせて書くのが定石です。
実例
while getopts 'c:p:C:L:P:A:D:w:m:o:g:G:vh?' arg; do
case "${arg}" in
p) read -r -a override_pkg_list <<< "${OPTARG}" ;;
C) override_pacman_conf="${OPTARG}" ;;
L) override_iso_label="${OPTARG}" ;;
P) override_iso_publisher="${OPTARG}" ;;
A) override_iso_application="${OPTARG}" ;;
D) override_install_dir="${OPTARG}" ;;
c) read -r -a override_cert_list <<< "${OPTARG}" ;;
w) override_work_dir="${OPTARG}" ;;
m) read -r -a override_buildmodes <<< "${OPTARG}" ;;
o) override_out_dir="${OPTARG}" ;;
g) override_gpg_key="${OPTARG}" ;;
G) override_gpg_sender="${OPTARG}" ;;
v) override_quiet="n" ;;
h|?) _usage 0 ;;
*)
_msg_error "Invalid argument '${arg}'" 0
_usage 1
;;
esac
done
shift $((OPTIND - 1))
if (( $# < 1 )); then
_msg_error "No profile specified" 0
_usage 1
fi
これはarchisoというオープンソースソフトウェアのmkarchisoというコマンドのソースコードの一部です。上のソースコードはGNU 2ライセンスです。
getopts “<利用可能な引数の一覧>” “<変数名>”
のようにして使います。
オプションの引数(例えば-o ~/out
の~/out
)はOPTARG環境変数に代入されます。
また、解析された引数の数はOPTINDに代入されます。
良い点と悪い点
Bash組み込みコマンドなのでシェルスクリプトが動く環境ならどこでも動くというのが大きなメリットです。
反面、ロングオプションを利用できなかったり、-hoge
のような指定はできません。
また、引数解析が一度終了したあとに引数解析を行えません。
具体的にはオプションとして「-a hoge -b」と指定し、その後の引数で「item1 item2」と指定したあとは「-h -c」などと指定しても解釈されません。
個人で気にはこの制限があまりにも辛かったのですぐに使うのを辞めてしまいました。
外部コマンドのgetopt
シェル用の引数解析には他にもgetoptという外部コマンドがあります。
これは上記のgetoptsよりも高機能で様々なことができます。
ですが、getoptはコマンドラインの並び替えとエラーの判定しか行いません。
その後の引数の解析はすべて自分で実装する必要があります。
といってもちょっとコード量が増えるだけでgetoptsとほとんど変わりません。
実例
OPT=$(getopt -o "a:bc:" -l "arch:,boot-splash,channel:" -- "${@}") || exit 1
eval set -- "${OPT}"
unset OPT
while true; do
case "${1}" in
-a | --arch)
arch="${2}"
shift 2
;;
-b | --boot-splash)
boot_splash=true
shift 1
;;
-c | --channel)
channel_dir="${2}"
shift 2
;;
esac
done
Alter Linuxのソースコードの一部です。
ショートオプションもロングオプションも適切に解釈してくれます。
詳細な使い方
以前に解説しているのでどうぞ
良い点と悪い点
良い点はやはり自由度の高さです。解析部分は自分で実装するのでかなり細かく制御できます。
例えば、--clean
と--cleanup
と-u
で同じ処理を行う、ということもできます。
ですが、問題点はロングオプションのサポートです。
実はgetoptはGNU版の実装とBSD版の実装で仕様が異なっています。
そしてBSD版ではロングオプションの解析が行なえません。
つまりBSD版ではgetoptsと同等レベルの機能しかありません。
自分でgetoptを実装する
そんなわけで、BSDではgetoptのロングオプション機能が使えません。
ということでgetoptと同等のことを行う関数を自分で実装しました。
ただし、シェルスクリプトの「関数は配列を返せない」という制限があるので実行結果はOPTRETというグローバルな配列を作成するようになっています。
ソースコード
#!/usr/bin/env bash
# parseopt LONG="<GNU getopt's long option>" SHORT="<GNU getopt's short option>" -- "${@}"
# This function does not support the option argument (Example: 'a::')
# It will return an array named OPTRET
parseopt(){
_error(){ echo "$*" >&2; }
local _Arg _Chr _Cnt # Temporary variable for loop
local _Long=() _LongWithArg=() _Short=() _ShortWithArg=()
local _OutArg=() _NoArg=() # Parsed array
local _ParseFinished=false
# 引数一覧を取得
for _Arg in "${@}"; do
local _TempArray=()
case "${_Arg}" in
"LONG="* )
readarray -t _TempArray < <(tr -d "\"" <<< "${_Arg#LONG=}" | tr "," "\n")
for _Chr in "${_TempArray[@]}"; do
if [[ "${_Chr}" = *":" ]]; then
_LongWithArg+=("${_Chr%":"}")
else
_Long+=("${_Chr}")
fi
done
shift 1
;;
"SHORT="*)
readarray -t _TempArray < <(tr -d "\"" <<< "${_Arg#SHORT=}" | grep -o .)
for (( _Cnt=0; _Cnt<= "${#_TempArray[@]}" - 1; _Cnt++ )); do
if [[ "${_TempArray["$(( _Cnt + 1))"]-""}" = ":" ]]; then
_ShortWithArg+=("${_TempArray["${_Cnt}"]}")
_Cnt=$(( _Cnt + 1 ))
else
_Short+=("${_TempArray["${_Cnt}"]}")
fi
done
shift 1
;;
"--")
shift 1
break
;;
esac
done
# Parse actually argument
while (( "$#" > 0 )); do
if [[ "${1}" = "--" ]]; then
shift 1
_NoArg+=("${@}")
shift "$#"
_ParseFinished=true
break
elif [[ "${1}" = "--"* ]]; then # Long option
# Long option with argument
if printf "%s\n" "${_LongWithArg[@]}" | grep -qx "${1#--}"; then
# Check argument
if [[ "${2}" = "-"* ]]; then
_error "${1} の引数が指定されていません"
return 1
else
_OutArg+=("${1}" "${2}")
shift 2
fi
elif printf "%s\n" "${_Long[@]}" | grep -qx "${1#--}"; then
_OutArg+=("${1}")
shift 1
else
_error "${1} は不正なオプションです。-hで使い方を確認してください。"
return 1
fi
elif [[ "${1}" = "-"* ]]; then
local _Shift=0 # 連続したショートオプションの解析後にshiftする数
while read -r _Chr; do # 引数を1文字ずつループ
if printf "%s\n" "${_ShortWithArg[@]}" | grep -qx "${_Chr}"; then
# 連続したショートオプションの場合、自分が最後かどうか
if [[ "${1}" = *"${_Chr}" ]] && [[ ! "${2}" = "-"* ]]; then
_OutArg+=("-${_Chr}" "${2}")
_Shift=2
else
_error "-${_Chr} の引数が指定されていません"
return 2
fi
elif printf "%s\n" "${_Short[@]}" | grep -qx "${_Chr}"; then
_OutArg+=("-${_Chr}")
_Shift=1
else
_error "-${_Chr} は不正なオプションです。-hで使い方を確認してください。"
return 1
fi
done < <(grep -o . <<< "${1#-}")
shift "${_Shift}"
else
_NoArg+=("${1}")
shift 1
fi
done
OPTRET=("${_OutArg[@]}" -- "${_NoArg[@]}")
}
実例
parseopts SHORT="fhdn:" LONG="help,architecture,disk,name:" -- "${@}" || exit 1
eval set -- "${OPTRET[@]}"
unset OPTRET
while true; do
case "${1}" in
--architecture)
ListMode="Arch"
shift 1
;;
-d | --disk)
ListMode="Disk"
shift 1
;;
-f | --file)
ListMode="VMFile"
shift 1
;;
-n | --name)
TargetName="${2}"
shift 2
;;
-h | --help)
HelpDoc
exit 0
;;
--)
shift 1
break
;;
*)
MsgError "Argument exception error '${1}'"
MsgError "Please report this error to the developer." 1
;;
esac
done
上記は自分が書いてるとあるオープンソースソフトウェアのコードを一部改変したものです。
使い方の大半はgetoptと一緒ですが、グローバルなOPTRET配列を定義するのが特徴です。
良い点と悪い点
getoptsと違ってロングオプションを解釈できますしgetoptと違ってBSDでもGNUでも動きます。
なので移植性を維持しつつ光度な引数解析が可能です。
ただし、set -x
でデバッグを行ったときに余計な引数解析のコードまで入ってしまうのが欠点です。
また、事前に100行ほどの関数を定義する必要があるのでコードが長くなってしまいます。
最後に
シェルスクリプトで引数解析を行う方法と問題点を解決する新しい関数を紹介しました。
なかなかにいい感じに解析できるようになったと思います。
Archiso以外の上記のコードはすべてハヤオが書いたコードです。ライセンスがいろいろ混在していて面倒なのでArchiso以外はWTFPLで再ライセンスしようと思います。好きに使ってください。
参考にしたサイト
この記事と関数作成にあたって参考にしたサイトを掲載しておきます。



コメント