web-dev-qa-db-ja.com

getoptsを使用したオプションのオプション引数

while getopts "hd:R:" arg; do
  case $arg in
    h)
      echo "usgae" 
      ;;
    d)
      dir=$OPTARG
      ;;
    R)
      if [[ $OPTARG =~ ^[0-9]+$ ]];then
        level=$OPTARG
      else
        level=1
      fi
      ;;
    \?)
      echo "WRONG" >&2
      ;;
  esac
done
  • levelは-Rのパラメーターを参照し、dirは-dのパラメーターを参照します

  • ./count.sh -R 1 -d test/を入力すると、正しく動作します

  • ./count.sh -d test/ -R 1を入力すると、正しく動作します

  • ./count.sh -d test/ -Rまたは./count.sh -R -d test/を入力したときに機能するようにしたい

これは、-Rにデフォルト値が必要であり、コマンドのシーケンスがより柔軟になる可能性があることを意味します。

33
iverson

getoptsは実際にはこれをサポートしていません。しかし、独自の代替を書くのは難しくありません。

while true; do
    case $1 in
      -R) level=1
            shift
            case $1 in
              *[!0-9]* | "") ;;
              *) level=$1; shift ;;
            esac ;;
        # ... Other options ...
        -*) echo "$0: Unrecognized option $1" >&2
            exit 2;;
        *) break ;;
    esac
done
16
tripleee

違う。実際、getoptsはオプションの引数をサポートします! bashのmanページから:

If  a  required  argument is not found, and getopts is not silent, 
a question mark (?) is placed in name, OPTARG is unset, and a diagnostic
message is printed.  If getopts is silent, then a colon (:) is placed in name 
and OPTARG is set to the option character found.

マニュアルページに「サイレント」と表示されている場合は、サイレントエラーレポートを意味します。有効にするには、optstringの最初の文字がコロンである必要があります。

while getopts ":hd:R:" arg; do
    # ...rest of iverson's loop should work as posted 
done

Bashのgetoptはオプションリストを終了する--を認識しないため、-Rが最後のオプションで、その後にパス引数が続く場合は機能しない場合があります。

追伸:伝統的に、getopt.cは2つのコロン(::)を使用してオプションの引数を指定します。ただし、Bashで使用されるバージョンはそうではありません。

22

トリプリーに同意します。getoptsはオプションの引数処理をサポートしていません。

私が解決した妥協案は、同じオプションフラグの大文字/小文字の組み合わせを使用して、引数を取るオプションと受け取らないオプションを区別することです。

例:

COMMAND_LINE_OPTIONS_HELP='
Command line options:
    -I          Process all the files in the default dir: '`pwd`'/input/
    -i  DIR     Process all the files in the user specified input dir
    -h          Print this help menu

Examples:
    Process all files in the default input dir
        '`basename $0`' -I

    Process all files in the user specified input dir
        '`basename $0`' -i ~/my/input/dir

'

VALID_COMMAND_LINE_OPTIONS="i:Ih"
INPUT_DIR=

while getopts $VALID_COMMAND_LINE_OPTIONS options; do
    #echo "option is " $options
    case $options in
        h)
            echo "$COMMAND_LINE_OPTIONS_HELP"
            exit $E_OPTERROR;
        ;;
        I)
            INPUT_DIR=`pwd`/input
            echo ""
            echo "***************************"
            echo "Use DEFAULT input dir : $INPUT_DIR"
            echo "***************************"
        ;;
        i)
            INPUT_DIR=$OPTARG
            echo ""
            echo "***************************"
            echo "Use USER SPECIFIED input dir : $INPUT_DIR"
            echo "***************************"
        ;;
        \?)
            echo "Usage: `basename $0` -h for help";
            echo "$COMMAND_LINE_OPTIONS_HELP"
            exit $E_OPTERROR;
        ;;
    esac
done
10
Akos Cz

これは実際には非常に簡単です。 Rの後に末尾のコロンをドロップして、OPTINDを使用します

while getopts "hRd:" opt; do
   case $opt in
      h) echo -e $USAGE && exit
      ;;
      d) DIR="$OPTARG"
      ;;
      R)       
        if [[ ${@:$OPTIND} =~ ^[0-9]+$ ]];then
          LEVEL=${@:$OPTIND}
          OPTIND=$((OPTIND+1))
        else
          LEVEL=1
        fi
      ;;
      \?) echo "Invalid option -$OPTARG" >&2
      ;;
   esac
done
echo $LEVEL $DIR

count.sh -dテスト

テスト

count.sh -d test -R

1テスト

count.sh -R -dテスト

1テスト

count.sh -d test -R 2

2テスト

count.sh -R 2 -dテスト

2テスト

8
Aaron Sua

この回避策は、引数なし(「:」なし)で「R」を定義し、「-R」(コマンドラインの最後のオプションを管理)の後の引数をテストし、既存の引数がダッシュで始まるかどうかをテストします。

# No : after R
while getopts "hd:R" arg; do
  case $arg in
  (...)
  R)
    # Check next positional parameter
    eval nextopt=\${$OPTIND}
    # existing or starting with dash?
    if [[ -n $nextopt && $nextopt != -* ]] ; then
      OPTIND=$((OPTIND + 1))
      level=$nextopt
    else
      level=1
    fi
    ;;
  (...)
  esac
done
2
calandoa

私は自分でこれに遭遇し、既存のソリューションはどれも本当にきれいではないと感じました。少し作業してさまざまなことを試した後、getoptsを使用すると:) ...は、OPTINDの同期を維持しながらトリックを実行したようです。


usage: test.sh [-abst] [-r [DEPTH]] filename
*NOTE: -r (recursive) with no depth given means full recursion

#!/usr/bin/env bash

depth='-d 1'

while getopts ':abr:st' opt; do
    case "${opt}" in
        a) echo a;;
        b) echo b;;
        r) if [[ "${OPTARG}" =~ ^[0-9]+$ ]]; then
               depth="-d ${OPTARG}"
           else
               depth=
               (( OPTIND-- ))
           fi
           ;;
        s) echo s;;
        t) echo t;;
        :) [[ "${OPTARG}" = 'r' ]] && depth=;;
        *) echo >&2 "Invalid option: ${opt}"; exit 1;;
    esac
done
shift $(( OPTIND - 1 ))

filename="$1"
...
0
Terra

試してください:

while getopts "hd:R:" arg; do
  case $arg in
    h)
      echo "usage" 
    ;;
    d)
      dir=$OPTARG
    ;;
    R)
      if [[ $OPTARG =~ ^[0-9]+$ ]];then
        level=$OPTARG
      Elif [[ $OPTARG =~ ^-. ]];then
        level=1
        let OPTIND=$OPTIND-1
      else
        level=1
      fi          
    ;;
    \?)
      echo "WRONG" >&2
    ;;
  esac
done

上記のコードは、getoptsを使用したまま目的に合わせて機能すると思います。 getopts-Rに遭遇した場合、次の3行をコードに追加しました。

      Elif [[ $OPTARG =~ ^-. ]];then
        level=1
        let OPTIND=$OPTIND-1

-Rが検出され、最初の引数が別のgetoptsパラメーターのように見える場合、レベルは1のデフォルト値に設定され、その後$OPTIND変数が1つ減らされます。次回getoptsが引数を取得するとき、それはスキップする代わりに正しい引数を取得します。


このチュートリアルでのJan Schamperaのコメント のコードに基づいた同様の例は次のとおりです。

#!/bin/bash
while getopts :abc: opt; do
  case $opt in
    a)
      echo "option a"
    ;;
    b)
      echo "option b"
    ;;
    c)
      echo "option c"

      if [[ $OPTARG = -* ]]; then
        ((OPTIND--))
        continue
      fi

      echo "(c) argument $OPTARG"
    ;;
    \?)
      echo "WTF!"
      exit 1
    ;;
  esac
done

OPTARG von -cがハイフンで始まることを発見したら、OPTINDをリセットし、getoptsを再実行します(whileループを続行します)。もちろん、これは完全ではなく、さらに堅牢性が必要です。これは単なる例です。

0
Menzies

@calandoaの answer (実際に機能する唯一のもの!)に触発されて、複数回使用しやすい単純な関数を作成しました。

getopts_get_optional_argument() {
  eval next_token=\${$OPTIND}
  if [[ -n $next_token && $next_token != -* ]]; then
    OPTIND=$((OPTIND + 1))
    OPTARG=$next_token
  else
    OPTARG=""
  fi
}

使用例:

while getopts "hdR" option; do
  case $option in
  d)
    getopts_get_optional_argument $@
    dir=${OPTARG}
    ;;
  R)
    getopts_get_optional_argument $@
    level=${OPTARG:-1}
    ;;
  h)
    show_usage && exit 0
    ;;
  \?)
    show_usage && exit 1
    ;;
  esac
done

これにより、getopts "欠落している機能"を取得する実用的な方法が得られます。

[〜#〜] note [〜#〜]それにもかかわらず、オプションの引数を持つコマンドラインオプションは 明示的に検出

ガイドライン7:オプション引数はオプションであってはなりません。

しかし、これなしで私のケースを実装する直感的な方法はありません。1つのフラグまたは他のいずれかを使用してアクティブ化される2つのモードがあり、それらには明確なデフォルトの引数があります。明確にするためだけに3番目のフラグを導入すると、悪いCLIスタイルに見えます。

@ aaron-suaの答えのすべてを含め、これを多くの組み合わせでテストし、うまく機能しています。

0
nandilugio

オプションは、小文字または大文字でいつでも区別できます。

しかし、私の考えは、getoptsを2回呼び出し、引数を無視して1回目の解析(R)を行い、2回目の引数サポート付きのオプションのみを解析(R:)。唯一のトリックは、処理中にOPTIND(インデックス)を変更する必要があることです。これは、現在の引数へのポインターを保持するためです。

コードは次のとおりです。

#!/usr/bin/env bash
while getopts ":hd:R" arg; do
  case $arg in
    d) # Set directory, e.g. -d /foo
      dir=$OPTARG
      ;;
    R) # Optional level value, e.g. -R 123
      OI=$OPTIND # Backup old value.
      ((OPTIND--)) # Decrease argument index, to parse -R again.
      while getopts ":R:" r; do
        case $r in
          R)
            # Check if value is in numeric format.
            if [[ $OPTARG =~ ^[0-9]+$ ]]; then
              level=$OPTARG
            else
              level=1
            fi
          ;;
          :)
            # Missing -R value.
            level=1
          ;;
        esac
      done
      [ -z "$level" ] && level=1 # If value not found, set to 1.
      OPTIND=$OI # Restore old value.
      ;;
    \? | h | *) # Display help.
      echo "$0 usage:" && grep " .)\ #" $0
      exit 0
      ;;
  esac
done
echo Dir: $dir
echo Level: $level

動作するシナリオのテストは次のとおりです。

$ ./getopts.sh -h
./getopts.sh usage:
    d) # Set directory, e.g. -d /foo
    R) # Optional level value, e.g. -R 123
    \? | h | *) # Display help.
$ ./getopts.sh -d /foo
Dir: /foo
Level:
$ ./getopts.sh -d /foo -R
Dir: /foo
Level: 1
$ ./getopts.sh -d /foo -R 123
Dir: /foo
Level: 123
$ ./getopts.sh -d /foo -R wtf
Dir: /foo
Level: 1
$ ./getopts.sh -R -d /foo
Dir: /foo
Level: 1

動作しないシナリオ(したがって、コードにはもう少し調整が必要です):

$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123

getoptsの使用に関する詳細は、man bash

参照: Small getopts tutorial at Bash Hackers Wiki

0
kenorb

次のコードは、先頭のダッシュをチェックすることでこの問題を解決し、見つかった場合はOPTINDを減らして、スキップされた処理オプションを指すようにします。これは通常、ユーザーがコマンドラインにオプションを配置する順序がわからないことを除いて、正常に機能します。オプションの引数オプションが最後で、引数を指定しない場合、getoptsはエラーになります。

最後の引数が見つからないという問題を修正するために、「$ @」配列には単に空の文字列「$ @」が追加されているため、getoptsは別のオプション引数を飲み込んだことを満たします。この新しい空の引数を修正するために、処理されるすべてのオプションの総数を保持する変数が設定されます-最後のオプションが処理されると、trimと呼ばれるヘルパー関数が呼び出され、利用される値の前に空の文字列を削除します。

これは機能するコードではなく、プレースホルダーのみがありますが、簡単に変更できます。少し注意すれば、堅牢なシステムを構築するのに役立ちます。

#!/usr/bin/env bash 
declare  -r CHECK_FLOAT="%f"  
declare  -r CHECK_INTEGER="%i"  

 ## <arg 1> Number - Number to check
 ## <arg 2> String - Number type to check
 ## <arg 3> String - Error message
function check_number() {
  local NUMBER="${1}"
  local NUMBER_TYPE="${2}"
  local ERROR_MESG="${3}"
  local FILTERED_NUMBER=$(sed 's/[^.e0-9+\^]//g' <<< "${NUMBER}")
  local -i PASS=1
  local -i FAIL=0
    if [[ -z "${NUMBER}" ]]; then 
        echo "Empty number argument passed to check_number()." 1>&2
        echo "${ERROR_MESG}" 1>&2
        echo "${FAIL}"          
  Elif [[ -z "${NUMBER_TYPE}" ]]; then 
        echo "Empty number type argument passed to check_number()." 1>&2
        echo "${ERROR_MESG}" 1>&2
        echo "${FAIL}"          
  Elif [[ ! "${#NUMBER}" -eq "${#FILTERED_NUMBER}" ]]; then 
        echo "Non numeric characters found in number argument passed to check_number()." 1>&2
        echo "${ERROR_MESG}" 1>&2
        echo "${FAIL}"          
  else  
   case "${NUMBER_TYPE}" in
     "${CHECK_FLOAT}")
         if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then
            echo "${PASS}"
         else
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"
         fi
         ;;
     "${CHECK_INTEGER}")
         if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then
            echo "${PASS}"
         else
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"
         fi
         ;;
                      *)
         echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
         echo "${FAIL}"
         ;;
    esac
 fi 
}

 ## Note: Number can be any printf acceptable format and includes leading quotes and quotations, 
 ##       and anything else that corresponds to the POSIX specification. 
 ##       E.g. "'1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054
 ## <arg 1> Number - Number to print
 ## <arg 2> String - Number type to print
function print_number() { 
  local NUMBER="${1}" 
  local NUMBER_TYPE="${2}" 
  case "${NUMBER_TYPE}" in 
      "${CHECK_FLOAT}") 
           printf "${CHECK_FLOAT}" "${NUMBER}" || echo "Error printing Float in print_number()." 1>&2
        ;;                 
    "${CHECK_INTEGER}") 
           printf "${CHECK_INTEGER}" "${NUMBER}" || echo "Error printing Integer in print_number()." 1>&2
        ;;                 
                     *) 
        echo "Invalid number type format: ${NUMBER_TYPE} to print_number()." 1>&2
        ;;                 
   esac
} 

 ## <arg 1> String - String to trim single ending whitespace from
function trim_string() { 
 local STRING="${1}" 
 echo -En $(sed 's/ $//' <<< "${STRING}") || echo "Error in trim_string() expected a sensible string, found: ${STRING}" 1>&2
} 

 ## This a hack for getopts because getopts does not support optional
 ## arguments very intuitively. E.g. Regardless of whether the values
 ## begin with a dash, getopts presumes that anything following an
 ## option that takes an option argument is the option argument. To fix  
 ## this the index variable OPTIND is decremented so it points back to  
 ## the otherwise skipped value in the array option argument. This works
 ## except for when the missing argument is on the end of the list,
 ## in this case getopts will not have anything to gobble as an
 ## argument to the option and will want to error out. To avoid this an
 ## empty string is appended to the argument array, yet in so doing
 ## care must be taken to manage this added empty string appropriately.
 ## As a result any option that doesn't exit at the time its processed
 ## needs to be made to accept an argument, otherwise you will never
 ## know if the option will be the last option sent thus having an empty
 ## string attached and causing it to land in the default handler.
function process_options() {
local OPTIND OPTERR=0 OPTARG OPTION h d r s M R S D
local ERROR_MSG=""  
local OPTION_VAL=""
local EXIT_VALUE=0
local -i NUM_OPTIONS
let NUM_OPTIONS=${#@}+1
while getopts “:h?d:DM:R:S:s:r:” OPTION "$@";
 do
     case "$OPTION" in
         h)
             help | more
             exit 0
             ;;
         r)
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG="Invalid input: Integer or floating point number required."
             if [[ -z "${OPTION_VAL}" ]]; then
               ## can set global flags here 
               :;
             Elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
               let OPTIND=${OPTIND}-1
               ## can set global flags here 
             Elif [ "${OPTION_VAL}" = "0" ]; then
               ## can set global flags here 
               :;               
             Elif (($(check_number "${OPTION_VAL}" "${CHECK_FLOAT}" "${ERROR_MSG}"))); then
               :; ## do something really useful here..               
             else
               echo "${ERROR_MSG}" 1>&2 && exit -1
             fi
             ;;
         d)
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             [[  ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]] && let OPTIND=${OPTIND}-1            
             DEBUGMODE=1
             set -xuo pipefail
             ;;
         s)
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             if [[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]]; then ## if you want a variable value that begins with a dash, escape it
               let OPTIND=${OPTIND}-1
             else
              GLOBAL_SCRIPT_VAR="${OPTION_VAL}"
                :; ## do more important things
             fi
             ;;
         M)  
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG=$(echo "Error - Invalid input: ${OPTION_VAL}, Integer required"\
                              "retry with an appropriate option argument.")
             if [[ -z "${OPTION_VAL}" ]]; then
               echo "${ERROR_MSG}" 1>&2 && exit -1
             Elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
               let OPTIND=${OPTIND}-1
               echo "${ERROR_MSG}" 1>&2 && exit -1
             Elif (($(check_number "${OPTION_VAL}" "${CHECK_INTEGER}" "${ERROR_MSG}"))); then
             :; ## do something useful here
             else
               echo "${ERROR_MSG}" 1>&2 && exit -1
             fi
             ;;                      
         R)  
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG=$(echo "Error - Invalid option argument: ${OPTION_VAL},"\
                              "the value supplied to -R is expected to be a "\
                              "qualified path to a random character device.")            
             if [[ -z "${OPTION_VAL}" ]]; then
               echo "${ERROR_MSG}" 1>&2 && exit -1
             Elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
               let OPTIND=${OPTIND}-1
               echo "${ERROR_MSG}" 1>&2 && exit -1
             Elif [[ -c "${OPTION_VAL}" ]]; then
               :; ## Instead of erroring do something useful here..  
             else
               echo "${ERROR_MSG}" 1>&2 && exit -1
             fi
             ;;                      
         S)  
             STATEMENT=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG="Error - Default text string to set cannot be empty."
             if [[ -z "${STATEMENT}" ]]; then
               ## Instead of erroring you could set a flag or do something else with your code here..  
             Elif [[ "${STATEMENT}" =~ ^-. ]]; then ## if you want a statement that begins with a dash, escape it
               let OPTIND=${OPTIND}-1
               echo "${ERROR_MSG}" 1>&2 && exit -1
               echo "${ERROR_MSG}" 1>&2 && exit -1
             else
                :; ## do something even more useful here you can modify the above as well 
             fi
             ;;                      
         D)  
             ## Do something useful as long as it is an exit, it is okay to not worry about the option arguments 
             exit 0
             ;;          
         *)
             EXIT_VALUE=-1
             ;&                  
         ?)
             usage
             exit ${EXIT_VALUE}
             ;;
     esac
done
}

process_options "$@ " ## extra space, so getopts can find arguments  
0
user4401178