golangの日記

Go言語を中心にプログラミングについてのブログ

Shellscript コマンドライン引数のパースとサブコマンド(Bash

shellscript.png


shellscriptでコマンドライン引数の解析とサブコマンド(Bashのみ) 前回の Shellscript コマンドライン引数のパース(Bash) を改良したサブコマンド版。





コード

#!/bin/bash

COMMAND_USAGE=$(
    cat <<HELP
Usage: $NAME [command] [options] [arguments]

Command:
  $NAME add      Add file contents to the index.
  $NAME commit   Record changes to the repository.
  $NAME help     Display this help and exit.
  $NAME version  Output version information and exit.
HELP
)
readonly COMMAND_USAGE

func_output_usage() {
    printf "\n%s\n\n" "$1"
    exit 2
}

func_output_version() {
    echo "$NAME: $VERSION"
    exit 2
}

func_output_error() {
    echo "Error: $1." 1>&2
    exit 1
}

# オプションの値をイコール(--option=value)で指定した場合の分割
func_split_by_equals() {
    IFS='=' read -ra ARRAY <<<"$1"
    OPTION="${ARRAY[0]}"
    if [[ -n "${ARRAY[1]}" ]]; then
        VALUE="${ARRAY[1]}"
        SKIP=false
    fi
}

# 正規表現を使って指定された長短のオプションが
# 定義されているか確認して存在しなければエラーにする
func_verify_option() {
    [[ "$1" =~ ^-([^-]+|$) ]] && [[ "$1" =~ ^-(.*[^$PATTERN_SHORT]+.*|)$ ]] && func_output_error "invalid option -- ‘$1"
    [[ "$1" =~ ^-{2,} ]] && [[ ! "$1" =~ ^-{2}($PATTERN_LONG)$ ]] && func_output_error "invalid option -- ‘$1"
}

# 値が必須なオプションに有効な値が
# 指定してあるかチェックして無ければエラーにする
func_verify_required_option_error() {
    [[ -z "$VALUE" ]] && func_output_error "required argument ‘$OPTION"
    "${SKIP}" && [[ "$VALUE" =~ ^-+ ]] && func_output_error "invalid argument ‘$VALUE‘ for ‘$OPTION"
}

############################
# subcommands 1
############################

# サブコマンド add のヘルプメッセージ
SUBCMD_ADD_USAGE=$(
    cat <<HELP
Usage: $NAME add [options] [arguments]

Options:
  -h, --help     Display this help and exit.
HELP
)
readonly SUBCMD_ADD_USAGE

# サブコマンド add が使われた場合に
# この関数内に処理を書く
func_subcmd_add() {
    echo "No required : $O_NO_REQUIRED"
    echo "Optional    : $O_OPTIONAL"
    echo "Required    : $O_REQUIRED"
    echo "Length:  $ARGC, (${ARGV[*]})"
}

# オプションを解析してその値をセットする
func_subcmd_add_set_value() {
    # 値が必要ないオプションはこう書かく。
    # 他のオプションを追加する場合は
    # これの n と --no-required と O_NO_REQUIRED の部分を書き換える
    if [[ "$OPTION" =~ ^(-[^-]*n|--no-required$) ]]; then O_NO_REQUIRED=true; fi

    # 値が必須なオプション
    # これも適宜  n と --no-required と O_NO_REQUIRED の部分を書き換えて追加または変更する
    if [[ "$OPTION" =~ ^(-[^-]*r|--required$) ]]; then
        func_verify_required_option_error
        O_REQUIRED="$VALUE"
        "${SKIP}" && SKIP_NEXT=true
    fi

    # 値が必須ではじゃないけど値をつけとることができるオプション
    # これも適宜以下同文
    if [[ "$OPTION" =~ ^(-[^-]*o|--optional$) ]]; then
        O_OPTIONAL=true
        if [[ -n "$VALUE" ]]; then
            if "${SKIP}"; then
                if [[ ! "$VALUE" =~ ^-+ ]]; then
                    O_OPTIONAL="$VALUE"
                    "${SKIP}" && SKIP_NEXT=true
                fi
            else
                O_OPTIONAL="$VALUE"
            fi
        fi
    fi
}

########################
# subcommands 2
########################

# サブコマンド commit のヘルプメッセージ
SUBCMD_COMMIT_USAGE=$(
    cat <<HELP
Usage: $NAME commit [options] [arguments]

Options:
  -h, --help     Display this help and exit.
HELP
)
readonly SUBCMD_COMMIT_USAGE

func_subcmd_commit() {
    echo "No required : $O_NO_REQUIRED"
    echo "Optional    : $O_OPTIONAL"
    echo "Required    : $O_REQUIRED"
    echo "Remaining arguments: Length $ARGC, (${ARGV[*]})"
}

func_subcmd_commit_set_value() {
    # No argument
    if [[ "$OPTION" =~ ^(-[^-]*n|--no-required$) ]]; then O_NO_REQUIRED=true; fi

    # Required argument
    if [[ "$OPTION" =~ ^(-[^-]*r|--required$) ]]; then
        func_verify_required_option_error
        O_REQUIRED="$VALUE"
        "${SKIP}" && SKIP_NEXT=true
    fi

    # Optional argument
    if [[ "$OPTION" =~ ^(-[^-]*o|--optional$) ]]; then
        O_OPTIONAL=true
        if [[ -n "$VALUE" ]]; then
            if "${SKIP}"; then
                if [[ ! "$VALUE" =~ ^-+ ]]; then
                    O_OPTIONAL="$VALUE"
                    "${SKIP}" && SKIP_NEXT=true
                fi
            else
                O_OPTIONAL="$VALUE"
            fi
        fi
    fi
}

func_subcmd_parse_arguments() {
    # オプションとその値以外の引数の数と配列
    local -i ARGC=0
    local -a ARGV=()

    local USAGE=""
    local PATTERN_SHORT=""
    local PATTERN_LONG=""

    case "$SUBCMD" in
    add)
        # サブコマンド add のヘルプメッセージをセットしておく
        USAGE="$SUBCMD_ADD_USAGE"

        # サブコマンド add で使うオプションの変数
        local O_NO_REQUIRED=""
        local O_OPTIONAL=""
        local O_REQUIRED=""

        # サブコマンド add 用の正規表現
        # func_verify_option 関数でチェックするときに使う
        # 設定の仕方は -a, --alpha というオプションを追加したければ
        # PATTERN_SHORT に "nora" と "a" を追加して
        # PATTERN_LONG に "no-required|optional|required|alpha" を追加
        PATTERN_SHORT="nor"
        PATTERN_LONG="no-required|optional|required"
        ;;
    commit)
        # 上と同じ
        USAGE="$SUBCMD_COMMIT_USAGE"

        local O_NO_REQUIRED=""
        local O_OPTIONAL=""
        local O_REQUIRED=""

        PATTERN_SHORT="nor"
        PATTERN_LONG="no-required|optional|required"
        ;;
    esac

    while (($# > 0)); do
        case "$1" in
        -h | --help)
            func_output_usage "$USAGE"
            ;;
        -*)
            # イコールで区切られたオプション(--option=value)の場合は
            # shift の回数を減らす必要があるので
            # この変数が false のときはイコールで区切って
            # 値が指定されたことになる
            local SKIP=true

            # ここ書き始めて SKIP 変数の役割が紛らわしいことに
            # 気づいたから後で自分のテンプレートは変数名を変えることにする
            # SKIPはイコールで区切られてたかどうかで、
            # 実際に shift を2回実行するのは SKIP_NEXT 変数が true のとき
            local SKIP_NEXT=false

            local OPTION="$1"
            local VALUE="$2"
            func_split_by_equals "$OPTION"
            func_verify_option "$OPTION"

            # サブコマンドに応じて値をセットする
            case "$SUBCMD" in
            add) func_subcmd_add_set_value ;;
            commit) func_subcmd_commit_set_value ;;
            esac

            # SKIP_NEXT が true の場合は shift を一回増やす
            "${SKIP_NEXT}" && shift
            shift
            ;;
        *)
            # 残りの引数の数(ARGC)をカウントして値を配列(ARGV)に入れる
            ((++ARGC))
            ARGV+=("$1")
            shift
            ;;
        esac
    done

    # サブコマンドの実行
    case "$SUBCMD" in
    add) func_subcmd_add ;;
    commit) func_subcmd_commit ;;
    esac
}

func_parse_arguments() {
    local SUBCMD="$1"
    shift

    case "$SUBCMD" in
    add | commit)
        func_subcmd_parse_arguments "$@"
        ;;
    version)
        func_output_version
        ;;
    *)
        func_output_usage "$COMMAND_USAGE"
        ;;
    esac
}

func_main() {
    local NAME=$(basename "$0")
    local VERSION="v0.0.1"

    func_parse_arguments "$@"
}

func_main "$@"



実行

$ command help

Usage: command [command] [options] [arguments]

Command:
   add      Add file contents to the index.
   commit   Record changes to the repository.
   help     Display this help and exit.
   version  Output version information and exit.


$ command add --help

Usage: command add [options] [arguments]

Options:
  -h, --help     Display this help and exit.


$ command commit -r foo -n bar baz -o
No required : true
Optional    : true
Required    : foo
Remaining arguments: Length 2, (bar baz)



前の記事 から改良したとこは $ command -abc の様に指定できるようにしたことと $ command --option=-100 の様にイコールを使って値を指定できるようにしたこと。このイコールを使った指定の何がいいのかというと -100 の様に - から始まっていてもエラーにならない