397

I know that I can easily get positioned parameters like this in bash:

$0 or $1

I want to be able to use flag options like this to specify for what each parameter is used:

mysql -u user -h host

What is the best way to get -u param value and -h param value by flag instead of by position?

4
  • 2
    It might be a good idea to ask/check over at unix.stackexchange.com as well
    – MRR0GERS
    Commented Aug 15, 2011 at 19:30
  • 13
    google for "bash getopts" -- lots of tutorials. Commented Aug 15, 2011 at 19:33
  • 141
    @glenn-jackman: I will definately google it now that I know the name. The thing about google is - to ask a question - you should already know 50% of the answer.
    – Stann
    Commented Aug 15, 2011 at 19:41
  • Have a look at BashFAQ#035
    – kvantour
    Commented Aug 10, 2021 at 13:47

11 Answers 11

678

This example uses Bash's built-in getopts command and is from the Google Shell Style Guide:

a_flag=''
b_flag=''
files=''
verbose='false'

print_usage() {
  printf "Usage: ..."
}

while getopts 'abf:v' flag; do
  case "${flag}" in
    a) a_flag='true' ;;
    b) b_flag='true' ;;
    f) files="${OPTARG}" ;;
    v) verbose='true' ;;
    *) print_usage
       exit 1 ;;
  esac
done

Note: If a character is followed by a colon (e.g. f:), that option is expected to have an argument.

Example usage: ./script -v -a -b -f filename

Using getopts has several advantages over the accepted answer:

  • the while condition is a lot more readable and shows what the accepted options are
  • cleaner code; no counting the number of parameters and shifting
  • you can join options (e.g. -a -b -c-abc)

However, a big disadvantage is that it doesn't support long options, only single-character options.

16
  • 50
    For posterity: the colon after in 'abf:v' denotes that -f takes an additional argument (the filename in this case).
    – zahbaz
    Commented Sep 7, 2016 at 20:15
  • 1
    I had to change the error line to this: ?) printf '\nUsage: %s: [-a] aflag [-b] bflag\n' $0; exit 2 ;;
    – Andrew
    Commented Nov 16, 2016 at 22:03
  • 12
    Could you add a note about the colons? In that after each letter, no colon means no arg, one colon means an arg, and two colons means optional arg? Commented Jan 20, 2017 at 21:22
  • 2
    @nachtigall that would be a function named error declared before its usage. It could, for example, print the error message passed to it and then exit with a non-zero status. Sorry for the confusion, I'll change the example to be more clear.
    – Dennis
    Commented Jun 10, 2018 at 18:46
  • 4
    @WillBarnwell one should note that it was added 3 years after the question was asked, whereas the top answer was added on the same day.
    – rbennell
    Commented Dec 19, 2018 at 9:52
394

This is the idiom I usually use:

while test $# -gt 0; do
  case "$1" in
    -h|--help)
      echo "$package - attempt to capture frames"
      echo " "
      echo "$package [options] application [arguments]"
      echo " "
      echo "options:"
      echo "-h, --help                show brief help"
      echo "-a, --action=ACTION       specify an action to use"
      echo "-o, --output-dir=DIR      specify a directory to store output in"
      exit 0
      ;;
    -a)
      shift
      if test $# -gt 0; then
        export PROCESS=$1
      else
        echo "no process specified"
        exit 1
      fi
      shift
      ;;
    --action*)
      export PROCESS=`echo $1 | sed -e 's/^[^=]*=//g'`
      shift
      ;;
    -o)
      shift
      if test $# -gt 0; then
        export OUTPUT=$1
      else
        echo "no output dir specified"
        exit 1
      fi
      shift
      ;;
    --output-dir*)
      export OUTPUT=`echo $1 | sed -e 's/^[^=]*=//g'`
      shift
      ;;
    *)
      break
      ;;
  esac
done

Key points are:

  • $# is the number of arguments
  • while loop looks at all the arguments supplied, matching on their values inside a case statement
  • shift takes the first one away. You can shift multiple times inside of a case statement to take multiple values.
15
  • 4
    What does the --action* and --output-dir* cases do?
    – Lucio
    Commented Jul 23, 2014 at 0:22
  • 29
    @Lucio Super old comment, but adding it in case someone else ever visits this page. The * (wildcard) is for the case where someone types --action=[ACTION] as well as the case where someone types --action [ACTION]
    – cooper
    Commented Mar 31, 2016 at 18:14
  • 3
    Why for *) do you break there, shouldn't you exit or ignore the bad option? In other words -bad -o dir the -o dir part is never processed.
    – newguy
    Commented Mar 21, 2017 at 2:04
  • 2
    You can replace export PROCESS='echo $1 | sed -e 's/^[^=]*=//g' with export PROCESS="${1/*"="/}" Commented Feb 13, 2020 at 0:07
  • 3
    @cooper Super old comment, but how the * would catch a space in $1? Commented Apr 5, 2020 at 1:56
53

getopt is your friend.. a simple example:

function f () {
TEMP=`getopt --long -o "u:h:" "$@"`
eval set -- "$TEMP"
while true ; do
    case "$1" in
        -u )
            user=$2
            shift 2
        ;;
        -h )
            host=$2
            shift 2
        ;;
        *)
            break
        ;;
    esac 
done;

echo "user = $user, host = $host"
}

f -u myself -h some_host

There should be various examples in your /usr/bin directory.

3
  • 4
    A more extensive example can be found in the directory /usr/share/doc/util-linux/examples, at the very least on Ubuntu machines. Commented Jan 22, 2015 at 22:36
  • @Shizzmo Can you explain the syntax of set -- ? I have seen this idiom a few times now. Would be grateful. Thanks alot.
    – von spotz
    Commented May 30, 2021 at 12:13
  • @vonspotz unix.stackexchange.com/a/308263/192430 Commented Apr 14, 2022 at 2:57
34

I propose a simple TLDR:; example for the un-initiated.

Create a bash script called greeter.sh

#!/bin/bash

while getopts "n:" arg; do
  case $arg in
    n) Name=$OPTARG;;
  esac
done

echo "Hello $Name!"

You can then pass an optional parameter -n when executing the script.

Execute the script as such:

$ bash greeter.sh -n 'Bob'

Output

$ Hello Bob!

Notes

If you'd like to use multiple parameters:

  1. extend while getops "n:" arg: do with more paramaters such as while getops "n:o:p:" arg: do
  2. extend the case switch with extra variable assignments. Such as o) Option=$OPTARG and p) Parameter=$OPTARG

To make the script executable:

chmod u+x greeter.sh
0
18

I think this would serve as a simpler example of what you want to achieve. There is no need to use external tools. Bash built in tools can do the job for you.

function DOSOMETHING {

   while test $# -gt 0; do
           case "$1" in
                -first)
                    shift
                    first_argument=$1
                    shift
                    ;;
                -last)
                    shift
                    last_argument=$1
                    shift
                    ;;
                *)
                   echo "$1 is not a recognized flag!"
                   return 1;
                   ;;
          esac
  done  

  echo "First argument : $first_argument";
  echo "Last argument : $last_argument";
 }

This will allow you to use flags so no matter which order you are passing the parameters you will get the proper behavior.

Example :

 DOSOMETHING -last "Adios" -first "Hola"

Output :

 First argument : Hola
 Last argument : Adios

You can add this function to your profile or put it inside of a script.

Thanks!

Edit : Save this as a a file and then execute it as yourfile.sh -last "Adios" -first "Hola"

#!/bin/bash
while test $# -gt 0; do
           case "$1" in
                -first)
                    shift
                    first_argument=$1
                    shift
                    ;;
                -last)
                    shift
                    last_argument=$1
                    shift
                    ;;
                *)
                   echo "$1 is not a recognized flag!"
                   return 1;
                   ;;
          esac
  done  

  echo "First argument : $first_argument";
  echo "Last argument : $last_argument";
6
  • I uses above code & when run it is not printing anything. ./hello.sh DOSOMETHING -last "Adios" -first "Hola"
    – dinu0101
    Commented Dec 24, 2017 at 17:08
  • @dinu0101 This is a function. Not a script. You should use it as DOSOMETHING -last "Adios" -first "Hola" Commented Dec 25, 2017 at 17:48
  • Thanks @Matias. Understood. how to run inside script.
    – dinu0101
    Commented Dec 29, 2017 at 13:33
  • 1
    Thanks you very much @Matias
    – dinu0101
    Commented Dec 29, 2017 at 16:10
  • 3
    Using return 1; with the last example outputs can only 'return' from a function or sourced script on macOS. Switching to exit 1; works as expected though.
    – Mattias
    Commented Mar 16, 2018 at 9:53
10

Another alternative would be to use something like the below example which would allow you to use long --image or short -i tags and also allow compiled -i="example.jpg" or separate -i example.jpg methods of passing in arguments.

# declaring a couple of associative arrays
declare -A arguments=();  
declare -A variables=();

# declaring an index integer
declare -i index=1;

# any variables you want to use here
# on the left left side is argument label or key (entered at the command line along with it's value) 
# on the right side is the variable name the value of these arguments should be mapped to.
# (the examples above show how these are being passed into this script)
variables["-gu"]="git_user";  
variables["--git-user"]="git_user";  
variables["-gb"]="git_branch";  
variables["--git-branch"]="git_branch";  
variables["-dbr"]="db_fqdn";  
variables["--db-redirect"]="db_fqdn";  
variables["-e"]="environment";  
variables["--environment"]="environment";

# $@ here represents all arguments passed in
for i in "$@"  
do  
  arguments[$index]=$i;
  prev_index="$(expr $index - 1)";

  # this if block does something akin to "where $i contains ="
  # "%=*" here strips out everything from the = to the end of the argument leaving only the label
  if [[ $i == *"="* ]]
    then argument_label=${i%=*} 
    else argument_label=${arguments[$prev_index]}
  fi

  # this if block only evaluates to true if the argument label exists in the variables array
  if [[ -n ${variables[$argument_label]} ]]
    then
        # dynamically creating variables names using declare
        # "#$argument_label=" here strips out the label leaving only the value
        if [[ $i == *"="* ]]
            then declare ${variables[$argument_label]}=${i#$argument_label=} 
            else declare ${variables[$argument_label]}=${arguments[$index]}
        fi
  fi

  index=index+1;
done;

# then you could simply use the variables like so:
echo "$git_user";
6

I like Robert McMahan's answer the best here as it seems the easiest to make into sharable include files for any of your scripts to use. But it seems to have a flaw with the line if [[ -n ${variables[$argument_label]} ]] throwing the message, "variables: bad array subscript". I don't have the rep to comment, and I doubt this is the proper 'fix,' but wrapping that if in if [[ -n $argument_label ]] ; then cleans it up.

Here's the code I ended up with, if you know a better way please add a comment to Robert's answer.

Include File "flags-declares.sh"

# declaring a couple of associative arrays
declare -A arguments=();
declare -A variables=();

# declaring an index integer
declare -i index=1;

Include File "flags-arguments.sh"

# $@ here represents all arguments passed in
for i in "$@"
do
  arguments[$index]=$i;
  prev_index="$(expr $index - 1)";

  # this if block does something akin to "where $i contains ="
  # "%=*" here strips out everything from the = to the end of the argument leaving only the label
  if [[ $i == *"="* ]]
    then argument_label=${i%=*}
    else argument_label=${arguments[$prev_index]}
  fi

  if [[ -n $argument_label ]] ; then
    # this if block only evaluates to true if the argument label exists in the variables array
    if [[ -n ${variables[$argument_label]} ]] ; then
      # dynamically creating variables names using declare
      # "#$argument_label=" here strips out the label leaving only the value
      if [[ $i == *"="* ]]
        then declare ${variables[$argument_label]}=${i#$argument_label=} 
        else declare ${variables[$argument_label]}=${arguments[$index]}
      fi
    fi
  fi

  index=index+1;
done;

Your "script.sh"

. bin/includes/flags-declares.sh

# any variables you want to use here
# on the left left side is argument label or key (entered at the command line along with it's value) 
# on the right side is the variable name the value of these arguments should be mapped to.
# (the examples above show how these are being passed into this script)
variables["-gu"]="git_user";
variables["--git-user"]="git_user";
variables["-gb"]="git_branch";
variables["--git-branch"]="git_branch";
variables["-dbr"]="db_fqdn";
variables["--db-redirect"]="db_fqdn";
variables["-e"]="environment";
variables["--environment"]="environment";

. bin/includes/flags-arguments.sh

# then you could simply use the variables like so:
echo "$git_user";
echo "$git_branch";
echo "$db_fqdn";
echo "$environment";
1
  • the bad array thing happens if you don't use an "=" to declare the argument. so -u=git_user works, but -u git_user throws the warning (but still works). I think I'll keep the original just to remind me to always use = in arguments, seems tidyer
    – emilBeBri
    Commented Nov 12, 2023 at 20:31
5
#!/bin/bash

if getopts "n:" arg; then
  echo "Welcome $OPTARG"
fi

Save it as sample.sh and try running

sh sample.sh -n John

in your terminal.

3

If you're familiar with Python argparse, and don't mind calling python to parse bash arguments, there is a piece of code I found really helpful and super easy to use called argparse-bash https://github.com/nhoffman/argparse-bash

Example take from their example.sh script:

#!/bin/bash

source $(dirname $0)/argparse.bash || exit 1
argparse "$@" <<EOF || exit 1
parser.add_argument('infile')
parser.add_argument('outfile')
parser.add_argument('-a', '--the-answer', default=42, type=int,
                    help='Pick a number [default %(default)s]')
parser.add_argument('-d', '--do-the-thing', action='store_true',
                    default=False, help='store a boolean [default %(default)s]')
parser.add_argument('-m', '--multiple', nargs='+',
                    help='multiple values allowed')
EOF

echo required infile: "$INFILE"
echo required outfile: "$OUTFILE"
echo the answer: "$THE_ANSWER"
echo -n do the thing?
if [[ $DO_THE_THING ]]; then
    echo " yes, do it"
else
    echo " no, do not do it"
fi
echo -n "arg with multiple values: "
for a in "${MULTIPLE[@]}"; do
    echo -n "[$a] "
done
echo
3

I had trouble using getopts with multiple flags, so I wrote this code. It uses a modal variable to detect flags, and to use those flags to assign arguments to variables.

Note that, if a flag shouldn't have an argument, something other than setting CURRENTFLAG can be done.

    for MYFIELD in "$@"; do

        CHECKFIRST=`echo $MYFIELD | cut -c1`

        if [ "$CHECKFIRST" == "-" ]; then
            mode="flag"
        else
            mode="arg"
        fi

        if [ "$mode" == "flag" ]; then
            case $MYFIELD in
                -a)
                    CURRENTFLAG="VARIABLE_A"
                    ;;
                -b)
                    CURRENTFLAG="VARIABLE_B"
                    ;;
                -c)
                    CURRENTFLAG="VARIABLE_C"
                    ;;
            esac
        elif [ "$mode" == "arg" ]; then
            case $CURRENTFLAG in
                VARIABLE_A)
                    VARIABLE_A="$MYFIELD"
                    ;;
                VARIABLE_B)
                    VARIABLE_B="$MYFIELD"
                    ;;
                VARIABLE_C)
                    VARIABLE_C="$MYFIELD"
                    ;;
            esac
        fi
    done
1

So here it is my solution. I wanted to be able to handle boolean flags without hyphen, with one hyphen, and with two hyphen as well as parameter/value assignment with one and two hyphens.

# Handle multiple types of arguments and prints some variables
#
# Boolean flags
# 1) No hyphen
#    create   Assigns `true` to the variable `CREATE`.
#             Default is `CREATE_DEFAULT`.
#    delete   Assigns true to the variable `DELETE`.
#             Default is `DELETE_DEFAULT`.
# 2) One hyphen
#      a      Assigns `true` to a. Default is `false`.
#      b      Assigns `true` to b. Default is `false`.
# 3) Two hyphens
#    cats     Assigns `true` to `cats`. By default is not set.
#    dogs     Assigns `true` to `cats`. By default is not set.
#
# Parameter - Value
# 1) One hyphen
#      c      Assign any value you want
#      d      Assign any value you want
#
# 2) Two hyphens
#   ... Anything really, whatever two-hyphen argument is given that is not
#       defined as flag, will be defined with the next argument after it.
#
# Example:
# ./parser_example.sh delete -a -c VA_1 --cats --dir /path/to/dir
parser() {
    # Define arguments with one hyphen that are boolean flags
    HYPHEN_FLAGS="a b"
    # Define arguments with two hyphens that are boolean flags
    DHYPHEN_FLAGS="cats dogs"

    # Iterate over all the arguments
    while [ $# -gt 0 ]; do
        # Handle the arguments with no hyphen
        if [[ $1 != "-"* ]]; then
            echo "Argument with no hyphen!"
            echo $1
            # Assign true to argument $1
            declare $1=true
            # Shift arguments by one to the left
            shift
        # Handle the arguments with one hyphen
        elif [[ $1 == "-"[A-Za-z0-9]* ]]; then
            # Handle the flags
            if [[ $HYPHEN_FLAGS == *"${1/-/}"* ]]; then
                echo "Argument with one hyphen flag!"
                echo $1
                # Remove the hyphen from $1
                local param="${1/-/}"
                # Assign true to $param
                declare $param=true
                # Shift by one
                shift
            # Handle the parameter-value cases
            else
                echo "Argument with one hyphen value!"
                echo $1 $2
                # Remove the hyphen from $1
                local param="${1/-/}"
                # Assign argument $2 to $param
                declare $param="$2"
                # Shift by two
                shift 2
            fi
        # Handle the arguments with two hyphens
        elif [[ $1 == "--"[A-Za-z0-9]* ]]; then
            # NOTE: For double hyphen I am using `declare -g $param`.
            #   This is the case because I am assuming that's going to be
            #   the final name of the variable
            echo "Argument with two hypens!"
            # Handle the flags
            if [[ $DHYPHEN_FLAGS == *"${1/--/}"* ]]; then
                echo $1 true
                # Remove the hyphens from $1
                local param="${1/--/}"
                # Assign argument $2 to $param
                declare -g $param=true
                # Shift by two
                shift
            # Handle the parameter-value cases
            else
                echo $1 $2
                # Remove the hyphens from $1
                local param="${1/--/}"
                # Assign argument $2 to $param
                declare -g $param="$2"
                # Shift by two
                shift 2
            fi
        fi

    done
    # Default value for arguments with no hypheb
    CREATE=${create:-'CREATE_DEFAULT'}
    DELETE=${delete:-'DELETE_DEFAULT'}
    # Default value for arguments with one hypen flag
    VAR1=${a:-false}
    VAR2=${b:-false}
    # Default value for arguments with value
    # NOTE1: This is just for illustration in one line. We can well create
    #   another function to handle this. Here I am handling the cases where
    #   we have a full named argument and a contraction of it.
    #   For example `--arg1` can be also set with `-c`.
    # NOTE2: What we are doing here is to check if $arg is defined. If not,
    #   check if $c was defined. If not, assign the default value "VD_"
    VAR3=$(if [[ $arg1 ]]; then echo $arg1; else echo ${c:-"VD_1"}; fi)
    VAR4=$(if [[ $arg2 ]]; then echo $arg2; else echo ${d:-"VD_2"}; fi)
}


# Pass all the arguments given to the script to the parser function
parser "$@"


echo $CREATE $DELETE $VAR1 $VAR2 $VAR3 $VAR4 $cats $dir

Some references

  • The main procedure was found here.
  • More about passing all the arguments to a function here.
  • More info regarding default values here.
  • More info about declare do $ bash -c "help declare".
  • More info about shift do $ bash -c "help shift".

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