0

Once a bash program is executed while processing options in getops, the loop exits.

As a short example, I have the following bash script:

#!/usr/bin/env bash

while getopts ":a:l:" opt; do
  case ${opt} in
    a)
      ls -a $2
      ;;
    l)
      ls -l $2
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument" >&2
      exit 1
      ;;
  esac
done

echo -e "\nTerminated"

If the script is called test.sh, when I execute the script with this command, I get the following output, where only the -a flag is processed, and -l is ignored:

$ ./test.sh -al .
.  ..  file1.txt  file2.txt  test.sh

Terminated

However, if I remove the colons after each argument, indicating that operands are not required for each argument, then the script does as intended. If the while loop is changed to:

while getopts ":al" opt; do

Then, running my script gives the following output (with both -a and -l processed):

$ ./test.sh -al .
.  ..  file1.txt  file2.txt  test.sh
total 161
-rwxrwxrwx 1 root root   0 Nov 24 22:31 file1.txt
-rwxrwxrwx 1 root root   0 Nov 24 22:32 file2.txt
-rwxrwxrwx 1 root root 318 Nov 24 22:36 test.sh

Terminated

Additionally, adding something like OPTIND=1 to the end of my loop only causes an infinite loop of the script executing the first argument.

How can I get getopts to parse multiple arguments with option arguments (: after each argument)?

homersimpson
  • 4,124
  • 4
  • 29
  • 39
  • 3
    `getopts` is correct. You declared option `a` to take an argument; when you say `-al` `getopt` sees option `a` with argument `l`. – AlexP Nov 25 '17 at 04:47
  • 2
    You should not be directly accessing `$2`, you defeat the object of using `getopts`. The argument to `-a` is in `$OPTARG` which should follow `-a` or `-l`. – cdarke Nov 25 '17 at 11:47
  • 1
    Consider not using 'silent mode' (omit the leading colon from `:a:l:`) so you get to see what `getopts` thinks is going wrong, if anything. Only use the leading colon when you're confident that your code is working. Generally, make sure the computer tells you about errors as soon as possible. – Jonathan Leffler Nov 26 '17 at 00:04

1 Answers1

6

Speaking about short options only, there is no need for a space between an option and its argument, so -o something equals to -osomething. Although it's very common to separate them, there are some exceptions like: cut -d: -f1.

Just like @AlexP said, if you use while getopts ":a:l:" opt, then options -a and -l are expected to have an argument. When you pass -al to your script and you make the option -a to require an argument, getopts looks for it and basically sees this: -a l which is why it ignores the -l option, because -a "ate it".

Your code is a bit messy and as @cdarke suggested, it doesn't use the means provided by getopts, such as $OPTARG. You might want to check this getopts tutorial.

If I understand correctly, your main goal is to check that a file/folder has been passed to the script for ls. You will achieve this not by making the options require an argument, but by checking whether there is a file/folder after all the options. You can do that using this:

#!/usr/bin/env bash

while getopts ":al" opt; do
  case ${opt} in
    a) a=1 ;;
    l) l=1 ;;
    \?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
    :) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
  esac
done

shift $(( OPTIND - 1 ));

[[ "$#" == 0 ]] && { echo "No input" >&2; exit 2; }

input=("$@")

[[ "$a" == 1 ]] && ls -a "${input[@]}"
[[ "$l" == 1 ]] && ls -l "${input[@]}"

echo Done

This solution saves your choices triggered by options to variables (you can use an array instead) and later on decide based on those variables. Saving to variables/array gives you more flexibility as you can use them anywhere within the script.

After all the options are processed, shift $(( OPTIND - 1 )); discards all options and associated arguments and leaves only arguments that do not belong to any options = your files/folders. If there aren't any files/folders, you detect that with [[ "$#" == 0 ]] and exit. If there are, you save them to an array input=("$@") and use this array later when deciding upon your variables:

[[ "$a" == 1 ]] && ls -a "${input[@]}"
[[ "$l" == 1 ]] && ls -l "${input[@]}"

Also, unlike ls -a $2, using an array ls -a "${input[@]}" gives you the possibility to pass more than just one file/folder: ./test.sh -la . "$HOME".

gronostaj
  • 2,231
  • 2
  • 23
  • 43
PesaThe
  • 7,259
  • 1
  • 19
  • 43