BSD上のBash 5.xだけでロングオプションの解析をする

Shell Script

こんにちは、山田ハヤオです。シェルスクリプトでコマンドラインツールを作ります。

コマンドラインツールを作る上で欠かせないのが引数解析です。

世の中の言語にはコマンドライン解析を行うライブラリがたくさんあります。

そしてそれは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で再ライセンスしようと思います。好きに使ってください。

参考にしたサイト

この記事と関数作成にあたって参考にしたサイトを掲載しておきます。

404 Not Found - Qiita - Qiita
【シェル芸人への道】Bashの変数展開と真摯に向き合う - Qiita
はじめに個人的なシェル(スクリプト)あるあるなんですが、変数操作に悩んでいるとBashの 変数展開 って思った以上に色んなことができてしまうことに気がつきます。「なんかいい感じの書き方ないかなー…
【 getopts 】コマンド――bashのシェルスクリプト内でオプションを解析する
本連載は、Linuxのコマンドについて、基本書式からオプション、具体的な実行例までを紹介していきます。今回は、bashのシェルスクリプト内でオプションを解析する「getopts」コマンドです。
archiso/mkarchiso · master · Arch Linux / archiso · GitLab
Official archiso scripts Repository

コメント

タイトルとURLをコピーしました