10

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?

  • 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 '16 at 23:02
  • 1
    Duplicate of stackoverflow.com/questions/16483119/… – steve Dec 19 '16 at 23:06
  • @orion With getopts, I would need to specify each argument combination? 3 & 4, 3 & 5, 3 & 4 & 5, etc. ? – ShanZhengYang Dec 19 '16 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 '16 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? – ShanZhengYang Dec 20 '16 at 3:53
17

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.

  • I usually do while (( $# )) instead of while :; and often quit with an error in the * case – Jasen Dec 20 '16 at 20:30
  • this answers the title but not the question. – Jasen Dec 20 '16 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 '16 at 6:13
  • no problem, it's still a good answer, just the question dodged it. – Jasen Dec 21 '16 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 '18 at 17:53
3

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.

  • 1
    You can streamline this a little by saying args+=( --optflag1 "$3" ).  You might want to see my answer to our reference work, Security implications of failing to quote a variable in bash/POSIX shells, where I discuss this technique (of building a command line in an array by conditionally appending optional arguments). – G-Man May 8 '17 at 6:13
  • @G-Man Thanks, I had not seen that in the documentation, understanding [@] I had a sufficient tool to solve my problem. – Jasen May 11 '17 at 1:42
0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.