28

I'm confused how to include optional arguments/flags when writing a bash script for the following program:

The program requires two arguments:

run_program --flag1 <value> --flag2 <value>

However, there are several optional flags:

run_program --flag1 <value> --flag2 <value> --optflag1 <value> --optflag2 <value> --optflag3 <value> --optflag4 <value> --optflag5 <value> 

I would like to run the bash script such that it takes user arguments. If users only input two arguments in order, then it would be:

#!/bin/sh

run_program --flag1 $1 --flag2 $2

But what if any of the optional arguments are included? I would think it would be

if [ --optflag1 "$3" ]; then
    run_program --flag1 $1 --flag2 $2 --optflag1 $3
fi

But what if $4 is given but not $3?

7
  • getopts is what you want. Without that, you could use a loop with a switch statement to detect each flag, optional or not.
    – orion
    Dec 19, 2016 at 23:02
  • 1
    Duplicate of stackoverflow.com/questions/16483119/…
    – steve
    Dec 19, 2016 at 23:06
  • @orion With getopts, I would need to specify each argument combination? 3 & 4, 3 & 5, 3 & 4 & 5, etc. ? Dec 19, 2016 at 23:08
  • No, you just set them if you get them, otherwise it reports it wasn't found, so basically you just "get" each option, in whatever order, and specify them in any order, if at all. But just read the bash man page, it's all there.
    – orion
    Dec 19, 2016 at 23:27
  • @orion I'm sorry, but I still don't quite understand getopts. Let's say I force users to run the script with all arguments: run_program.sh VAL VAL FALSE FALSE FALSE FALSE FALSE which runs the program as program --flag1 VAL --flag2 VAL. If you ran run_program.sh VAL VAL FALSE 10 FALSE FALSE FALSE, the program would run as program --flag1 VAL --flag2 VAL --optflag2 10. How can you get such behavior with getopts? Dec 20, 2016 at 3:53

4 Answers 4

44

This article shows two different ways - shift and getopts (and discusses the advantages and disadvantages of the two approaches).

With shift your script looks at $1, decides what action to take, and then executes shift, moving $2 to $1, $3 to $2, etc.

For example:

while :; do
    case $1 in
        -a|--flag1) flag1="SET"            
        ;;
        -b|--flag2) flag2="SET"            
        ;;
        -c|--optflag1) optflag1="SET"            
        ;;
        -d|--optflag2) optflag2="SET"            
        ;;
        -e|--optflag3) optflag3="SET"            
        ;;
        *) break
    esac
    shift
done

With getopts you define the (short) options in the while expression:

while getopts abcde opt; do
    case $opt in
        a) flag1="SET"
        ;;
        b) flag2="SET"
        ;;
        c) optflag1="SET"
        ;;
        d) optflag2="SET"
        ;;
        e) optflag3="SET"
        ;;
    esac
done

Obviously, these are just code-snippets, and I've left out validation - checking that the mandatory args flag1 and flag2 are set, etc.

Which approach you use is to some extent a matter of taste - how portable you want your script to be, whether you can live with short (POSIX) options only or whether you want long (GNU) options, etc.

6
  • 1
    I usually do while (( $# )) instead of while :; and often quit with an error in the * case
    – Jasen
    Dec 20, 2016 at 20:30
  • this answers the title but not the question.
    – Jasen
    Dec 20, 2016 at 20:49
  • @Jasen I looked at this last night and couldn't for the life of me see why. This morning it's much clearer (and I've seen orion's answer now as well). I'll delete this answer later today (I wanted to acknowledge your comment first, and this seemed the easiest way to do it).
    – John N
    Dec 21, 2016 at 6:13
  • no problem, it's still a good answer, just the question dodged it.
    – Jasen
    Dec 21, 2016 at 8:10
  • 1
    NB: In case of set -o nounset the first solution will error out if no parameter is given. Fix: case ${1:-} in
    – Raphael
    Nov 27, 2018 at 17:53
4

use an array.

#!/bin/bash

args=( --flag1 "$1" --flag2 "$2" )
[  "x$3" = xFALSE ] ||  args+=( --optflag1 "$3" )
[  "x$4" = xFALSE ] ||  args+=( --optflag2 "$4" )
[  "x$5" = xFALSE ] ||  args+=( --optflag3 "$5" )
[  "x$6" = xFALSE ] ||  args+=( --optflag4 "$6" )
[  "x$7" = xFALSE ] ||  args+=( --optflag5 "$7" )

program_name "${args[@]}"

this will handle arguments with spaces in them correctly.

[edit] I was using the roughly eqivalent syntax args=( "${args[@]}" --optflag1 "$3" ) but G-Man suggested a better way.

2
1

In a shell script, arguments are "$1", "$2", "$3", etc. The number of arguments is $#.
If your script doesn't recognize options you can leave out option detection and treat all arguments as operands.
To recognize options use the getopts builtin

0

If your input options are positional (you know at which places they are), and not specified with flags, then what you want is just to build the command line. Just prepare the command arguments for all of them:

FLAG1="--flag1 $1"
FLAG2="--flag2 $2"
OPTFLAG1=""
OPTFLAG2=""
OPTFLAG3=""
if [ xFALSE != x"$3" ]; then
   OPTFLAG1="--optflag1 $3"
fi
if [ xFALSE != x"$4" ]; then
   OPTFLAG2="--optflag2 $4"
fi
#the same for other flags

#now just run the program
runprogram $FLAG1 $FLAG2 $OPTFLAG1 $OPTFLAG2 $OPTFLAG3

If the parameters are not specified, then the corresonding strings are empty and expand into nothing. Note that in the last line, there are no quotes. That's because you want the shell to split the parameters into words (to give --flag1 and $1 as separate arguments to your program). Of course, this will go wrong if your original parameters contain spaces. If you're the one running this, then you can leave it, but if that's a general script, it can have unexpected behaviour if the user inputs something with spaces. To handle that, you'll need to make the code a bit uglier.

The x prefix in the [] test is there in case $3 or $4 is empty. In that case, bash would expand [ FALSE != $3 ] into [ FALSE != ] which is a syntax error, so another arbitrary character is there to guard against this. This is a very common way, you'll see it in many scripts.

I set OPTFLAG1 and the rest of them to "" at the beginning just to be sure (in case they were set to something before), but if they weren't actually declared in the environment, then you don't strictly need to do that.

A couple of additional remarks:

  • You could actually just receive the parameters the same way as runprogram does: with flags. That's what John N is talking about. That's where getopts becomes useful.
  • Using FALSE for that purpose is a bit unstandard and quite long. Usually, one would use a single character (possibly -) to signal an empty value, or just pass an empty string, like "", if that's not otherwise a valid value of the parameter. Empty string also makes the test shorter, just use if [ -n "$3" ].
  • If there is only one optional flag, or if they are dependent so you can never have OPTFLAG2, but not OPTFLAG1, then you can simply skip the ones you don't want to set. If you use "" for empty parameters, like suggested above, you can skip all the trailing empties anyway.

This method is pretty much the way options are passed to compilers in Makefiles.

And again - if inputs may contain spaces, it gets ugly.

1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .