↓ メインコンテンツへスキップ

Shell でオプションを作る方法 | 個人的な備忘録

·1796 文字·4 分
わて
著者
わて

この記事はめちゃめちゃ自分用の備忘録です。
shell スクリプトのオプションについての記録です。

最近会社でshell スクリプトを組んでいるのですが、いつでも再利用できるようにここに残す感じの記事です。

そんな感じですが、参考にする場合は実際に手を動かしながら書いてみると理解が早いと思うので、時間があればやってみてください。

やり方と解説
#

とりあえずこれできる
#

基本的には以下のようにすることで設定できます。

test.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# default
NUM=1
WORD="Hello"

# find options
while [[ $# -gt 0 ]]; do
    case "$1" in
        -n|--number)
            NUM=$2
            shift 2
            ;;
        -w|--word)
            WORD=$2
            shift 2
            ;;
        -h|--help)
            show_help
            exit 0
            ;;
        -*)
            echo "Error: Unknown option '$1'" >&2
            exit 1
            ;;
        *)
            break
            ;;
    esac
done

echo $NUM
echo $WORD

試してみましょう。
一度目はデフォルト(なんのオプションも指定しない)で、二度目はオプションを渡してみます。

# $ ./test.sh
1
Hello
# $ ./test.sh -n 3 -w Hi
3
Hi

正しく動いてることがわかります。

解説
#

特殊文字がたくさんあるので解説します。先にまとめると以下の通りです。

  • $#とは 引数の数
  • $1と$2とは 1つ目の引数、2つ目の引数
  • shift 2とは 絵で解説 $# も2減る

$#は引数の数のことで、例えばtest.sh -n 1の場合は2を返します。
続いて、$1は一個目の、$2は二個目の引数を示します。
なので、同じ例では$1 → -n、$2 → 1となります。

./test.sh -n 1

# $# -> 2
# $1 -> -n
# $2 -> 1

したがって、while [[ $# -gt 0 ]]; doと書くことで、指定されたすべての引数について処理していることがわかります。
あとはcaseを使って合致したものに処理を加えるという算段です。

caseの処理中にshift 2というコマンドがあります。
これは引数を二つずらすという意味で$#の値も2だけ減ります。
なので、該当のオプションの処理が終わったら次に行くということですね。
また、オプションではなく引数の場合はshift 1にします。

exit 0とexit 1とは終了ステータスのことで、0なら正常な終了、1以上はエラーを示します。正直終了のさせ方の話なのでプログラムそのものにはあまり影響を及ぼさないのですが、普段Linuxを使っているなかで前回のプログラムの終了のステータスを図ることはたまにあるのでexit で終了時のエラー有無を示しときます。

また、>&2も通常のメッセージではなく、エラーメッセージ(標準エラー出力)として出力するものです。

標準エラー出力をすると>でファイルに出力する際にエラー分を入力させなかったり、|で後続につなげる再にgrepで渡されてしまい、変な影響が出かねないので、通常のメッセージと切り分けたいときにこのような方法を取ることがあります。

もう少しこだわる
#

数値の制限
#

数値を入れる前提のものは、数字以外が入ったときにエラーを出すようにしておくと安全です。
以下は、引数の数がゼロではなく(なんらかの入力があるとき)、かつ数字のみが入力されたらNUMを更新し、それ以外はエラーを返すというものです。

test.sh
    -n|--number)
        if [[ -n "$2" && "$2" =~ ^[1-9][0-9]*$ ]]; then
            NUM=$2
            shift 2
        else
            echo "Error: -n / --number requires a natural number (integer >= 1)." >&2
            exit 1
        fi
        ;;

試しにnに文字を入れてみます。

# $ ./test.sh -n a
Error: -n / --number requires a natural number (integer >= 1).

また、数が入るオプションを複数使う場合は、以下のようにテンプレ化して使いまわすとコードがスッキリしてメンテしやすいです。

test.sh
check_natural_num(){
        local opt_name="$1"
        local opt_value="$2"

        if [[ -z "$opt_value" || ! "$opt_value" =~ ^[1-9][0-9]*$ ]]; then
            echo "Error: $opt_name requires a natural number (integer >= 1)." >&2
            exit 1
        fi
}

# ~~~~

    -n|--number)
        check_natural_num "$1" "$2"
        NUM=$2
        shift 2
        ;;
    -c|--count)
        check_natural_num "$1" "$2"
        COUNT=$2
        shift 2
        ;;

# ~~~~

共存できない引数の作り方
#

例えば繰り返し作業があって、時間で止まるモードと回数で止まるモードがあるとして、この二つは共存できないとき、以下のようなやり方で、共存の制限をかけます。

今回はHOURSとLOOP_COUNTというオプションで例とします。

if [[ -z "$LOOP_COUNT" && -z "$HOURS" ]]; then
    HOURS=1
    is_time_mode=true
elif [[ -n "$LOOP_COUNT" && -n "$HOURS" ]]; then
    echo "-lc and -h can't exist together in a single operation"
    exit 1
elif [[ -n "$HOURS" ]]; then
    is_time_mode=true
elif [[ -n "$LOOP_COUNT" ]]; then
    is_time_mode=false
fi
# $ ./test.sh -ls 1 -h 1
-lc and -h cannot exist together in a single operation

-nは何かしら入力がされてるを意味するので、どちらも入力があるとき、エラーを出すようにするのがシンプルです。

また、-zは入力がないことを意味するので、どちらも指定がない場合はデフォルトの値として振る舞わせることができます。

まとめ
#

今回とくに気をつけたいのは以下の二つです。

  • オプションはwhileとcase, shiftのコンボで処理する
  • エラーのときはexit1や>&2のようなエラー特有の処理をさせる

雑な記事ですが読んでくれてありがとうございました。