27

I have a script with a long list of OPTIONAL arguments. some have associated values.

Such as:

.script --first 2012-12-25 --last 2012-12-26 --copy --remove
.script --first 2012-12-25 

Thus the following case statement:

for arg in "$@"
do
    case $arg in
        "--first" )
           START_DATE=$arg;;
        "--last" )
           END_DATE=$arg;;
        "--copy" )
           COPY=true;;
        "--remove" )
           REMOVE=true;;

# ... and so on
   esac
done

My problem:

that needs a increment $arg+1 type statement to get the following arg (in some cases).

How is that possible?

I'm also happy to do a substring such .script --first2012-12-25 --last2012-12-26

and not sure how to proceed there.

7ochem
  • 2,183
  • 1
  • 34
  • 42
Gabe Rainbow
  • 3,658
  • 4
  • 32
  • 42

5 Answers5

34

You can allow both --a=arg or -a arg options with a little more work:

START_DATE="$(date '+%Y-%m-%d')";
LAST_DATE="$(date '+%Y-%m-%d')";
while [[ $# -gt 0 ]] && [[ "$1" == "--"* ]] ;
do
    opt="$1";
    shift;              #expose next argument
    case "$opt" in
        "--" ) break 2;;
        "--first" )
           START_DATE="$1"; shift;;
        "--first="* )     # alternate format: --first=date
           START_DATE="${opt#*=}";;
        "--last" )
           LAST_DATE="$1"; shift;;
        "--last="* )
           LAST_DATE="${opt#*=}";;
        "--copy" )
           COPY=true;;
        "--remove" )
           REMOVE=true;;
        "--optional" )
           OPTIONAL="$optional_default";;     #set to some default value
        "--optional=*" )
           OPTIONAL="${opt#*=}";;             #take argument
        *) echo >&2 "Invalid option: $@"; exit 1;;
   esac
done

Note the --optional argument uses a default value if "=" is not used, else it sets the value in the normal way.

user8819
  • 53
  • 6
Gilbert
  • 3,740
  • 17
  • 19
  • Better write `while [[ "$1" = --* ]]; do ` directly. – gniourf_gniourf Dec 28 '12 at 13:05
  • gniourf_gniourf: some configurations write errors if you use an undefined variable. The $# -gt avoids such. – Gilbert Dec 29 '12 at 01:23
  • FYI: I quote every variable expansion to avoid problems with command lines with --first="12 May 2012". As without quotes such change inside the script TO $1="--first=12" $2="May" $3=2012. Oops. – Gilbert Dec 29 '12 at 01:27
  • 1
    You're talking about the `unset` option? In this case, `set +u` will do. Then, if you really want to check `$#`, you could use bash arithmetic: `(($#>0))`. – gniourf_gniourf Dec 29 '12 at 09:05
2

Use shift in the end of each case statement.

Quote from a bash manual:

shift [n]

The positional parameters from n+1 ... are renamed to $1 .... Parameters represented by the numbers $# down to $#-n+1 are unset. n must be a non-negative number less than or equal to $#. If n is 0, no parameters are changed. If n is not given, it is assumed to be 1. If n is greater than $#, the positional parameters are not changed. The return status is greater than zero if n is greater than $# or less than zero; otherwise 0.

favoretti
  • 29,299
  • 4
  • 48
  • 61
  • 3
    Yes, `shift` can be a solution, but not without some more work. `shift` affects `"$@"`, but it won't affect the `for` loop, which is working on a *copy* of `"$@"` evaluated at the top of the loop. – Keith Thompson Dec 28 '12 at 02:43
2

$@ is an array, & not a simple variable.

You can capture it to a local variable as x=("$@") & then use array x with indices as 0 to ($# - 1).

To access individual elements, use ${x[$i]}. You can NOT directly use ${@[$i]}, however.

So instead of for arg in "$@" loop, you will have i=0; while [ $i -lt $# ]; do loop.

anishsane
  • 20,270
  • 5
  • 40
  • 73
  • 1
    `"$@"` is not an array. It is a special variable that expands to all the received arguments, correctly delimited (as opposed to `"$*"`, which joins all into one). – Luis Lavaire. Jan 06 '22 at 03:20
  • @LuisLavaire., yes it's an array. You can loop over it, you can slice it, reference 'n'th element from it. It's just that it is an immutable array. So, you cannot set any arbitrary entry like `a[5]=abcd`. You have to set the entire array for this `set -- v a l u e s`. – anishsane Jan 06 '22 at 13:36
  • You can't loop over it. You even mention it yourself: _"You can NOT directly use `${@[$i]}`, however."_ – Luis Lavaire. Jan 07 '22 at 23:39
  • You can loop. Just the syntax is different. Use `${!i}` instead of `${@[$i]}`. (I did not know this at the time of answering.) – anishsane Jan 11 '22 at 16:04
  • The `${!...}` notation is for performing referential substitution. Strictly speaking, you can't iterate over `"$@"` (because the shell expands it, so all the shell will see after expansion is just strings, not an array), even when the aforementioned substitution seems to be looping over it. – Luis Lavaire. Jan 12 '22 at 03:20
1

If you have more than one option, and especially options with values mixed with options without values, let getopts do the work for you.

potrzebie
  • 1,768
  • 1
  • 12
  • 25
1

getopts cannot have optional arguments it seems. otherwise great.

my solution

loop the $@ and setting a variable equal to x=$arg do the case switch on that variable (rather than arg)

that worked fine for arguments of the type --startdate 2012-12-25 --enddate 2012-12-29

but did not work for --remove that has no following argument.

therefore tack on stuff (unlikely argument) onto the arg string.

leaving the following

argc="$@ jabberwhocky" 
echo $argc
x=0
# x=0 for unset variable
for arg in $argc
do
   case $x in
        "--start" )
          STARTDATE=$arg ;;
        "--end" )
          ENDDATE=$arg ;;
        "--copy" )
          COPY=true;;
        "--remove" )
          REMOVE=true;;

... and so on....

    esac
    x=$arg
done
Gabe Rainbow
  • 3,658
  • 4
  • 32
  • 42