【问题标题】:Retrieving multiple arguments for a single option using getopts in Bash在 Bash 中使用 getopts 检索单个选项的多个参数
【发布时间】:2011-11-23 16:44:33
【问题描述】:

我需要getopts 方面的帮助。

我创建了一个运行时如下所示的 Bash 脚本:

$ foo.sh -i env -d 目录 -s 子目录 -f 文件

在处理来自每个标志的一个参数时,它可以正常工作。但是当我从每个标志调用几个参数时,我不确定如何从getopts 中的变量中提取多个变量信息。

while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=$OPTARG;;
        f ) files=$OPTARG;;

     esac
done

获取选项后,我想从变量构建目录结构

foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3

那么目录结构就是

/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3

有什么想法吗?

【问题讨论】:

  • 请您选择最佳答案:)

标签: bash command-line-arguments getopts


【解决方案1】:

您可以多次使用相同的选项并将所有值添加到数组中

对于这里非常具体的原始问题,Ryan 的mkdir -p 解决方案显然是最好的。

但是,对于使用 getopts 从同一选项获取多个值这一更一般的问题,这里是:

#!/bin/bash

while getopts "m:" opt; do
    case $opt in
        m) multi+=("$OPTARG");;
        #...
    esac
done
shift $((OPTIND -1))

echo "The first value of the array 'multi' is '$multi'"
echo "The whole list of values is '${multi[@]}'"

echo "Or:"

for val in "${multi[@]}"; do
    echo " - $val"
done

输出将是:

$ /tmp/t
The first value of the array 'multi' is ''
The whole list of values is ''
Or:

$ /tmp/t -m "one arg with spaces"
The first value of the array 'multi' is 'one arg with spaces'
The whole list of values is 'one arg with spaces'
Or:
 - one arg with spaces

$ /tmp/t -m one -m "second argument" -m three
The first value of the array 'multi' is 'one'
The whole list of values is 'one second argument three'
Or:
 - one
 - second argument
 - three

【讨论】:

  • 我很欣赏这个详尽的解释。一个附录是使用 "${multi[@]}" 而不是 ${multi[@]} 可以防止任何不知道和好奇的人出现包含空格的参数的问题。
  • 这是执行 OP 在 bash 中要求的 方法。
【解决方案2】:

我知道这个问题很老了,但我想把这个答案放在这里,以防有人来寻找答案。

像 BASH 这样的 Shell 已经支持像这样递归地创建目录,因此实际上并不需要脚本。例如,原始海报想要这样的东西:

$ foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3
/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3

这个命令行很容易做到:

pong:~/tmp
[10] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/{file1,file2,file3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

甚至更短一点:

pong:~/tmp
[12] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

或者更短,更符合要求:

pong:~/tmp
[14] rmclean$ mkdir -pv test/directory/subdirectory{1,2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

或者最后,使用序列:

pong:~/tmp
[16] rmclean$ mkdir -pv test/directory/subdirectory{1..2}/file{1..3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

【讨论】:

  • 是的,除了它将目录而不是文件作为叶节点。
【解决方案3】:

getopts 选项只能接受零个或一个参数。您可能希望更改界面以删除 -f 选项,并仅遍历剩余的非选项参数

usage: foo.sh -i end -d dir -s subdir file [...]

所以,

while getopts ":i:d:s:" opt; do
  case "$opt" in
    i) initial=$OPTARG ;;
    d) dir=$OPTARG ;;
    s) sub=$OPTARG ;;
  esac
done
shift $(( OPTIND - 1 ))

path="/$initial/$dir/$sub"
mkdir -p "$path"

for file in "$@"; do
  touch "$path/$file"
done

【讨论】:

  • getopt 会是更好的选择,还是您会使用不同的方法?
  • 我会使用我上面写的。如果您希望能够为 -f 选项提供多个参数,或者能够多次为 -f 提供一个参数,我知道您可以在 Perl 中使用 Getopt::Long 模块来做到这一点。
  • 我同意 Glenn 的观点,这通常是我使用的。但是,另一种选择是只使用另一个分隔符,例如逗号分隔多个参数而不是空格,然后用逗号分隔 $OPTARG。例如 -f 文件 1、文件 2、文件 3。我倾向于只对我计划保留给自己的命令执行此操作,因为我不相信其他人会意识到他们不能在逗号后放置空格
  • 还要注意in "$@"可以在不改变语义的情况下删除。
  • @cole,是的,在 case 分支中,您将附加到一个数组:subdirs+=( "$OPTARG" )
【解决方案4】:

我解决了你遇到的同样的问题:

代替:

foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3

这样做:

foo.sh -i test -d directory -s "subdirectory subdirectory2" -f "file1 file2 file3"

使用空格分隔符,您可以通过基本循环运行它。 代码如下:

while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=$OPTARG;;
        f ) files=$OPTARG;;

     esac
done

for subdir in $sub;do
   for file in $files;do
      echo $subdir/$file
   done   
done

这是一个示例输出:

$ ./getopts.sh -s "testdir1 testdir2" -f "file1 file2 file3"
testdir1/file1
testdir1/file2
testdir1/file3
testdir2/file1
testdir2/file2
testdir2/file3

【讨论】:

  • 我最初也使用了这种方法,但很快就遇到了问题,因为我的一个参数有空格,需要引号内的引号。如果您有多个参数并且其中一些参数需要引号,它会很快变得混乱。
【解决方案5】:

如果你想为一个选项指定任意数量的值,你可以使用一个简单的循环来找到它们并将它们填充到一个数组中。例如,让我们修改 OP 的示例以允许任意数量的 -s 参数:

unset -v sub
while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=("$OPTARG")
            until [[ $(eval "echo \${$OPTIND}") =~ ^-.* ]] || [ -z $(eval "echo \${$OPTIND}") ]; do
                sub+=($(eval "echo \${$OPTIND}"))
                OPTIND=$((OPTIND + 1))
            done
            ;;
        f ) files=$OPTARG;;
     esac
done

这接受第一个参数 ($OPTARG) 并将其放入数组 $sub。然后它将继续搜索剩余的参数,直到它遇到另一个虚线参数或没有更多参数要评估。如果它找到更多不是虚线参数的参数,则将其添加到 $sub 数组并增加 $OPTIND 变量。

因此在 OP 的示例中,可以运行以下命令:

foo.sh -i test -d directory -s subdirectory1 subdirectory2 -f file1

如果我们将这些行添加到脚本中进行演示:

echo ${sub[@]}
echo ${sub[1]}
echo $files

输出将是:

subdirectory1 subdirectory2
subdirectory2
file1

【讨论】:

    【解决方案6】:

    实际上有一种方法可以使用getopts 检索多个参数,但它需要使用getopts'OPTIND 变量进行一些手动修改。

    请参阅以下脚本(转载如下):https://gist.github.com/achalddave/290f7fcad89a0d7c3719。可能有更简单的方法,但这是我能找到的最快的方法。

    #!/bin/sh
    
    usage() {
    cat << EOF
    $0 -a <a1> <a2> <a3> [-b] <b1> [-c]
        -a      First flag; takes in 3 arguments
        -b      Second flag; takes in 1 argument
        -c      Third flag; takes in no arguments
    EOF
    }
    
    is_flag() {
        # Check if $1 is a flag; e.g. "-b"
        [[ "$1" =~ -.* ]] && return 0 || return 1
    }
    
    # Note:
    # For a, we fool getopts into thinking a doesn't take in an argument
    # For b, we can just use getopts normal behavior to take in an argument
    while getopts "ab:c" opt ; do
        case "${opt}" in
            a)
                # This is the tricky part.
    
                # $OPTIND has the index of the _next_ parameter; so "\${$((OPTIND))}"
                # will give us, e.g., ${2}. Use eval to get the value in ${2}.
                # The {} are needed in general for the possible case of multiple digits.
    
                eval "a1=\${$((OPTIND))}"
                eval "a2=\${$((OPTIND+1))}"
                eval "a3=\${$((OPTIND+2))}"
    
                # Note: We need to check that we're still in bounds, and that
                # a1,a2,a3 aren't flags. e.g.
                #   ./getopts-multiple.sh -a 1 2 -b
                # should error, and not set a3 to be -b.
                if [ $((OPTIND+2)) -gt $# ] || is_flag "$a1" || is_flag "$a2" || is_flag "$a3"
                then
                    usage
                    echo
                    echo "-a requires 3 arguments!"
                    exit
                fi
    
                echo "-a has arguments $a1, $a2, $a3"
    
                # "shift" getopts' index
                OPTIND=$((OPTIND+3))
                ;;
            b)
                # Can get the argument from getopts directly
                echo "-b has argument $OPTARG"
                ;;
            c)
                # No arguments, life goes on
                echo "-c"
                ;;
        esac
    done
    

    【讨论】:

      【解决方案7】:

      最初的问题涉及 getopts,但还有另一种解决方案,它提供了不带 getopts 的更灵活的功能(这可能有点冗长,但提供了更灵活的命令行界面)。这是一个例子:

      while [[ $# > 0 ]]
      do
          key="$1"
          case $key in
              -f|--foo)
                  nextArg="$2"
                  while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
                      case $nextArg in
                          bar)
                              echo "--foo bar found!"
                          ;;
                          baz)
                              echo "--foo baz found!"
                          ;;
                          *)
                              echo "$key $nextArg found!"
                          ;;
                      esac
                      if ! [[ "$2" =~ -.* ]]; then
                          shift
                          nextArg="$2"
                      else
                          shift
                          break
                      fi
                  done
              ;;
              -b|--bar)
                  nextArg="$2"
                  while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
                      case $nextArg in
                          foo)
                              echo "--bar foo found!"
                          ;;
                          baz)
                              echo "--bar baz found!"
                          ;;
                          *)
                              echo "$key $nextArg found!"
                          ;;
                      esac
                      if ! [[ "$2" =~ -.* ]]; then
                          shift
                          nextArg="$2"
                      else
                          shift
                          break
                      fi
                  done
              ;;
              -z|--baz)
                  nextArg="$2"
                  while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
      
                      echo "Doing some random task with $key $nextArg"
      
                      if ! [[ "$2" =~ -.* ]]; then
                          shift
                          nextArg="$2"
                      else
                          shift
                          break
                      fi
                  done
              ;;
              *)
                  echo "Unknown flag $key"
              ;;
          esac
          shift
      done
      

      在本例中,我们循环遍历所有命令行选项,寻找与我们接受的命令行标志(例如 -f 或 --foo)相匹配的参数。一旦我们找到一个标志,我们就会遍历每个参数,直到我们用完参数或遇到另一个标志。这让我们回到了只处理标志的外循环。

      使用此设置,以下命令是等效的:

      script -f foo bar baz
      script -f foo -f bar -f baz
      script --foo foo -f bar baz
      script --foo foo bar -f baz
      

      您还可以解析令人难以置信的杂乱无章的参数集,例如:

      script -f baz derp --baz herp -z derp -b foo --foo bar -q llama --bar fight
      

      获取输出:

      --foo baz found!
      -f derp found!
      Doing some random task with --baz herp
      Doing some random task with -z derp
      --bar foo found!
      --foo bar found!
      Unknown flag -q
      Unknown flag llama
      --bar fight found!
      

      【讨论】:

      • 谢谢你 - 我一直想知道是否有一种避免 getopts 的巧妙方法,虽然你的回答告诉我没有,但它告诉我我的自制替代品已经开启正确的线。
      【解决方案8】:
      #!/bin/bash
      myname=$(basename "$0")
      
      # help function
      help () { cat <<EOP
         $myname: -c cluster [...] -a action [...] -i instance [...]
      EOP
      }
      
      # parse sub options
      get_opts () {
        rs='' && rc=0 # return string and return code
        while [[ $# -gt 0 ]]; do
          shift
          [[ "$1" =~ -.* ]] && break ||  rs="$rs $1" && rc=$((rc + 1))
        done
        echo "$rs"
      }
      
      #parse entire command-line
      while [[ $# -gt 0 ]]; do
          case $1 in
              "-a") ACTS="$(get_opts $@)"
                 ;;
              "-i") INSTS=$(get_opts $@)
                 ;;
              "-c") CLUSTERS=$(get_opts $@)
                 ;;
              "-h") help
                 ;;
              ?) echo "sorry, I dont do $1"
                 exit
                 ;;
          esac
          shift
      done
      

      【讨论】:

        【解决方案9】:

        以下链接应该是此要求的通用解决方案。

        易于注入,清晰易懂,对原始代码的影响降到最低。

        Multiple option arguments using getopts (bash)

        function getopts-extra () {
            declare i=1
            # if the next argument is not an option, then append it to array OPTARG
            while [[ ${OPTIND} -le $# && ${!OPTIND:0:1} != '-' ]]; do
                OPTARG[i]=${!OPTIND}
                let i++ OPTIND++
            done
        }
        
        # Use it within the context of `getopts`:
        while getopts s: opt; do
            case $opt in
               s) getopts-extra "$@"
                  args=( "${OPTARG[@]}" )
            esac
        done
        

        【讨论】:

          【解决方案10】:

          因为你没有展示你希望如何构建你的列表

          /test/directory/subdirectory/file1
          . . .
          test/directory/subdirectory2/file3
          

          有点不清楚如何继续,但基本上你需要不断将任何新值附加到适当的变量,即

           case $opt in
              d ) dirList="${dirList} $OPTARG" ;;
           esac
          

          请注意,在第一次通过时,dir 将为空,您最终会在 ${dirList} 的最终值的起始处出现一个空格。 (如果你真的需要前面或后面不包含任何额外空格的代码,我可以给你看一个命令,但它会很难理解,而且这里似乎不需要它,但请告诉我)

          然后您可以将列表变量包装在 for 循环中以发出所有值,即

          for dir in ${dirList} do
             for f in ${fileList} ; do
                echo $dir/$f
             done
          done
          

          最后,将任何未知输入“捕获”到您的案例陈述中被认为是一种很好的做法,即

           case $opt in
              i ) initial=$OPTARG;;
              d ) dir=$OPTARG;;
              s ) sub=$OPTARG;;
              f ) files=$OPTARG;;
              * ) 
                 printf "unknown flag supplied "${OPTARG}\nUsageMessageGoesHere\n"
                 exit 1
              ;;
          
           esac 
          

          我希望这会有所帮助。

          【讨论】:

          • 这更有意义,但还不是 100%。我的文件列表将从标志中提取值以构建文件的目录路径。如果构建了第二个,那么我需要重建第二个目录/文件的新路径
          【解决方案11】:

          这是为单个选项传递多个参数的简单方法。

          #!/bin/bash
          
          #test.sh -i 'input1 input2'
          #OR
          #test.sh -i 'input*'
          
          while getopts "i:" opt; do
              case $opt in
                  i ) input=$OPTARG;;
              esac
          done
          inputs=( $input )
          
          echo "First input is "$inputs""
          echo "Second input is "${inputs[1]}""
          echo "All inputs: "${inputs[@]}""
          

          【讨论】:

          • 感谢您提供此代码 sn-p,它可能会提供一些有限的即时帮助。 proper explanation 将通过展示为什么这是解决问题的好方法,并使其对有其他类似问题的未来读者更有用,从而大大提高其长期价值。请edit您的回答添加一些解释,包括您所做的假设。
          猜你喜欢
          • 2013-04-17
          • 1970-01-01
          • 2014-05-23
          • 1970-01-01
          • 2021-04-29
          • 1970-01-01
          • 2012-07-02
          • 2021-06-23
          相关资源
          最近更新 更多