Retrieving multiple arguments for a single option using getopts in Bash

112,575

Solution 1

You can use the same option multiple times and add all values to an array.

For the very specific original question here, Ryan's mkdir -p solution is obviously the best.

However, for the more general question of getting multiple values from the same option with getopts, here it is:

#!/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

The output would be:

$ /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

Solution 2

I know this question is old, but I wanted to throw this answer on here in case someone comes looking for an answer.

Shells like BASH support making directories recursively like this already, so a script isn't really needed. For instance, the original poster wanted something like:

$ 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

This is easily done with this command line:

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’

Or even a bit shorter:

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’

Or shorter, with more conformity:

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’

Or lastly, using sequences:

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’

Solution 3

getopts options can only take zero or one argument. You might want to change your interface to remove the -f option, and just iterate over the remaining non-option arguments

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

So,

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

Solution 4

I fixed the same problem you had like this:

Instead of:

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

Do this:

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

With the space separator you can just run through it with a basic loop. Here's the code:

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

Here's a sample output:

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

Solution 5

If you want to specify any number of values for an option, you can use a simple loop to find them and stuff them into an array. For example, let's modify the OP's example to allow any number of -s parameters:

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

This takes the first argument ($OPTARG) and puts it into the array $sub. Then it will continue searching through the remaining parameters until it either hits another dashed parameter OR there are no more arguments to evaluate. If it finds more parameters that aren't a dashed parameter, it adds it to the $sub array and bumps up the $OPTIND variable.

So in the OP's example, the following could be run:

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

If we added these lines to the script to demonstrate:

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

The output would be:

subdirectory1 subdirectory2
subdirectory2
file1
Share:
112,575
vegasbrianc
Author by

vegasbrianc

Updated on July 05, 2022

Comments

  • vegasbrianc
    vegasbrianc almost 2 years

    I need help with getopts.

    I created a Bash script which looks like this when run:

    $ foo.sh -i env -d directory -s subdirectory -f file

    It works correctly when handling one argument from each flag. But when I invoke several arguments from each flag I am not sure how to pull the multiple variable information out of the variables in 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
    

    After grabbing the options I then want to build directory structures from the variables

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

    Then the directory structure would be

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

    Any ideas?

  • vegasbrianc
    vegasbrianc almost 13 years
    This makes more sense but not 100% yet. My file list will pull the values from the flags to build a directory path to the file. if a second one is built then I need to rebuild a new path to the second directory/file
  • vegasbrianc
    vegasbrianc almost 13 years
    would getopt be a better option for this or would you use somehting different?
  • glenn jackman
    glenn jackman almost 13 years
    I would use just what I wrote above. If you want to be able to provide the -f option with multiple arguments, or to be able to provide -f with one argument multiple times, I know you can do that in Perl with the Getopt::Long module.
  • frankc
    frankc almost 13 years
    I agree with Glenn, this is normally what I use. However, another option is to just use another delimiter e.g. commas to separate the multiple arguments instead of spaces and then split $OPTARG on comma. For example -f file1,file2,file3. I tend to only do this on commands i plan to keep to myself as I don't trust others to realize they must not put spaces after the commas
  • choroba
    choroba about 11 years
    Also note that in "$@" can be deleted without changing the semantics.
  • Zac Thompson
    Zac Thompson over 9 years
    Yah, except it's making directories instead of files as the leaf nodes.
  • cchamberlain
    cchamberlain about 9 years
    getopts supports multiple for the same option per @mivk solution below - usage: foo.sh -i end -d dir -s subdir1 -s subdir2 -s subdir3 file [...]
  • glenn jackman
    glenn jackman about 9 years
    @cole, yes, and in the case branch you would append to an array: subdirs+=( "$OPTARG" )
  • chrBrd
    chrBrd over 7 years
    Thanks for this - I've been wondering if there was a neat way of avoiding getopts and while your answer's shown me that there isn't, it has shown me that my home-baked alternative is on the right lines.
  • jimh
    jimh about 7 years
    I appreciate this thorough explanation. One addendum is that using "${multi[@]}" instead of ${multi[@]} prevents issues from arguments containing spaces for anyone who didn't know and was curious.
  • laur
    laur over 3 years
    This is the way to do what OP asked in bash.
  • KoZm0kNoT
    KoZm0kNoT over 3 years
    I also used this approach initally but soon ran into issues when one of my parameters had spaces requiring quotes within quotes. If you have multiple parameters and some of them need quotes it can get messy fast.
  • divinelemon
    divinelemon over 2 years
    Thank you for this code snippet, which might provide some limited, immediate help. A proper explanation would greatly improve its long-term value by showing why this is a good solution to the problem and would make it more useful to future readers with other, similar questions. Please edit your answer to add some explanation, including the assumptions you’ve made.
  • WhoAmI
    WhoAmI over 2 years
    Is there a way to do that without quotes?