196

Is there any easy way to pass (receive) named parameters to a shell script?

For example,

my_script -p_out '/some/path' -arg_1 '5'

And inside my_script.sh receive them as:

# I believe this notation does not work, but is there anything close to it?
p_out=$ARGUMENTS['p_out']
arg1=$ARGUMENTS['arg_1']

printf "The Argument p_out is %s" "$p_out"
printf "The Argument arg_1 is %s" "$arg1"

Is this possible in Bash or Zsh?

2

13 Answers 13

231

If you don't mind being limited to single-letter argument names i.e. my_script -p '/some/path' -a5, then in bash you could use the built-in getopts, e.g.

#!/bin/bash

while getopts ":a:p:" opt; do
  case $opt in
    a) arg_1="$OPTARG"
    ;;
    p) p_out="$OPTARG"
    ;;
    \?) echo "Invalid option -$OPTARG" >&2
    exit 1
    ;;
  esac

  case $OPTARG in
    -*) echo "Option $opt needs a valid argument"
    exit 1
    ;;
  esac
done

printf "Argument p_out is %s\n" "$p_out"
printf "Argument arg_1 is %s\n" "$arg_1"

Then you can do

$ ./my_script -p '/some/path' -a5
Argument p_out is /some/path
Argument arg_1 is 5

There is a helpful Small getopts tutorial or you can type help getopts at the shell prompt.

Edit: The second case statement in while loop triggers if the -p option has no arguments and is followed by another option, e.g. my_script -p -a5, and exits the program.

5
  • 8
    I know this is a bit old, but why only 1 letter for the arguments?
    – Kevin
    Apr 28 '17 at 15:54
  • 2
    I implemented this (but with i and d). When I run it with my_script -i asd -d asd I get an empty string for the d argument. When I run it my_script -d asd -i asd I get empty string for both arguments. Feb 14 '18 at 22:48
  • 4
    @Milkncookiez -- I had a similar problem -- I didn't include a ':' after the last argument (a 'w' in my case). Once I added the ':' it started working as expected
    – Derek
    May 24 '18 at 15:30
  • 2
    @KaushikGhose Just fixed it. I guess better late than never. I wish SO notified you when an answer other than the one you accepted got more accepted votes. Jan 9 '20 at 12:47
  • Arguably there's a second answer using getopt (no s) for long args. I'm not experienced to write it though.
    – davolfman
    Aug 17 at 17:30
61

I use this script and works like a charm:

for ARGUMENT in "$@"
do

    KEY=$(echo $ARGUMENT | cut -f1 -d=)
    VALUE=$(echo $ARGUMENT | cut -f2 -d=)   

    case "$KEY" in
            STEPS)              STEPS=${VALUE} ;;
            REPOSITORY_NAME)    REPOSITORY_NAME=${VALUE} ;;     
            *)   
    esac    


done

echo "STEPS = $STEPS"
echo "REPOSITORY_NAME = $REPOSITORY_NAME"

Usage

bash my_scripts.sh  STEPS="ABC" REPOSITORY_NAME="stackexchange"

Console result :

STEPS = ABC
REPOSITORY_NAME = stackexchange

STEPS and REPOSITORY_NAME are ready to use in the script.

It does not matter what order the arguments are in.

5
  • This is a neat solution, but it seems to take a long time to parse, like 5 to 10 seconds on my PC, is this normal behavior?
    – Astronaut
    Dec 13 '19 at 14:01
  • mmm in my case it is instantaneous.
    – JRichardsz
    Dec 13 '19 at 17:22
  • This is neat but assumes you have = delimiter between the key and the value. I.e. you can't pass a value with a space like so: -p 3000 or --port 3000 Nov 4 '20 at 10:32
  • Unfortunately this breaks if the value has an equals in it like this MYVAR="PROPERTIES_LIST":"\"apim=true&whichvalue=345\""
    – PaulNUK
    Apr 12 at 13:43
  • several systems will have problems with a variable like that. An equivalent could be PROPERTIES_LIST=apim:true&whichvalue:345
    – JRichardsz
    Apr 14 at 14:12
58

I stole this from drupal.org, but you could do something like this:

while [ $# -gt 0 ]; do
  case "$1" in
    --p_out=*)
      p_out="${1#*=}"
      ;;
    --arg_1=*)
      arg_1="${1#*=}"
      ;;
    *)
      printf "***************************\n"
      printf "* Error: Invalid argument.*\n"
      printf "***************************\n"
      exit 1
  esac
  shift
done

The only caveat is that you have to use the syntax my_script --p_out=/some/path --arg_1=5.

2
  • 13
    The caveat is not necessary. :) You can have the conditions as follows: -c|--condition) May 30 '17 at 14:27
  • Docs to truncate string in variable (and to continue the trend in this thread, this should be the accepted answer).
    – toraritte
    Aug 8 '20 at 16:31
40

The probably closest syntax to that is:

p_out='/some/path' arg_1='5' my_script
3
  • 8
    Related to this, if the -k option is set in the calling shell, then my_script p_out='/some/path' arg_1='5' has the same effect. (All arguments in the form of an assignment are added to the environment, not just those assignments preceding the command.)
    – chepner
    May 14 '14 at 22:55
  • @chepner, Hi, I like your solution, but I don't know where to put the '-k' option as you mentioned, can you show me the code? thanks Mar 19 at 2:56
  • @StayFoolish One way would be to run set -k, then run my_script p_out='/some/path' arg_1='5'.
    – chepner
    Mar 19 at 12:21
18

With zsh, you'd use zparseopts:

#! /bin/zsh -
zmodload zsh/zutil
zparseopts -A ARGUMENTS -p_out: -arg_1:

p_out=$ARGUMENTS[--p_out]
arg1=$ARGUMENTS[--arg_1]

printf 'Argument p_out is "%s"\n' "$p_out"
printf 'Argument arg_1 is "%s"\n' "$arg_1"

But you'd call the script with myscript --p_out foo.

Note that zparseopts doesn't support abbreviating long options or the --p_out=foo syntax like GNU getopt(3) does.

2
  • Do you know why the zparseopts uses just one dash for the arguments whereas in the [] it is 2 dashes? Does not make sense!
    – Timo
    Nov 7 '17 at 14:59
  • @Timo, see info zsh zparseopts for details Nov 7 '17 at 16:20
15

I just came up with this script

while [ $# -gt 0 ]; do

   if [[ $1 == *"--"* ]]; then
        v="${1/--/}"
        declare $v="$2"
   fi

  shift
done

pass it like my_script --p_out /some/path --arg_1 5 and then in the script you can use $arg_1 and $p_out.

1
  • I like this solution in KSH88 I had to v=``echo ${1} | awk '{print substr($1,3)}'`` typeset $v="$2" (Remove one backtick each side)
    – hol
    Sep 24 '18 at 9:45
10

I found the solution from cdmo the best, because it is not only limited to a single letter. With a slight adjustment it will consume either whitespace separated arguments like --url www.example.com and those with an assignment like, for example the synonym parameter -u=www.example.com. Even flag arguments can be parsed:

while [ $# -gt 0 ]; do
  case "$1" in
    --url*|-u*)
      if [[ "$1" != *=* ]]; then shift; fi # Value is next arg if no `=`
      URL="${1#*=}"
      ;;
    --file*|-f*)
      if [[ "$1" != *=* ]]; then shift; fi
      FILE="${1#*=}"
      ;;
    --help|-h)
      printf "Meaningful help message" # Flag argument
      exit 0
      ;;
    *)
      >&2 printf "Error: Invalid argument\n"
      exit 1
      ;;
  esac
  shift
done
0
7

This answer was initially an edit of @cdmo's answer (thanks to @Milkncookiez's comment also!), that got rejected as expected, so here it goes:

#!/usr/bin/env bash

while [ $# -gt 0 ]; do
  case "$1" in
    -p|-p_out|--p_out)
      p_out="$2"
      ;;
    -a|-arg_1|--arg_1)
      arg_1="$2"
      ;;
    *)
      printf "***************************\n"
      printf "* Error: Invalid argument.*\n"
      printf "***************************\n"
      exit 1
  esac
  shift
  shift
done

The below invocations are all equivalent:

$ ./my-script.sh -a "lofa" -p "miez"
$ ./my-script.sh -arg_1 "lofa" --p_out "miez"
$ ./my-script.sh --arg_1 "lofa" -p "miez"

When using the variables, one can make sure that they have a default value set by using "${p_out:-"default value"}" for example.

0
1

I made a combination of some approaches recommended here. In my case, I need a script that could handle arguments in this format: --arg1=test So I used the following:

args=()

for argument in "$@"
do

    key=$(echo $argument | cut -f1 -d=)
    value=$(echo $argument | cut -f2 -d=)   

    if [[ $key == *"--"* ]]; then
        v="${key/--/}"
        declare $v="${value}" 
   fi
done

args+=( '--local_dir' ${local_dir})
args+=( '--format' ${format})

python3 ./test.py "${args[@]}"

You can see that I added the arguments together to call a python script with those arguments: test_script.sh --local_dir=test --format=other

Thanks to Shahzad Malik and JRichardsz, for the guide to join the two approaches.

0

I have just made this one. It doesn't require "=" and supports non-valued parameters. Mind the need of additional shift commands for valued parameters.

#!/bin/bash

if [ $# -eq 0 ]; then
    printf "Utilizacao:\n"
    printf "$0 [[--imagem_inicial|-i] <tag_inicial>] [[--imagem_final|-f] <tag_final>] [--verbose|-v]\n"
    exit 1
fi

while [ $# -gt 0 ]; do
  case "$1" in
    --imagem_inicial|-i)
      export tag_imagem_inicial="${2}"
      shift
      ;;
    --imagem_final|-f)
      export tag_imagem_final="${2}"
      shift
      ;;
    --verbose|-v)
      export verbose_option=1
      ;;
    *)
      printf "ERRO: Parametros invalidos\n"
      printf "Execute sem parametros para a sintaxe.\n"
      exit 1
  esac
  shift
done

echo tag_imagem_inicial=${tag_imagem_inicial}
echo tag_imagem_final=${tag_imagem_final}
0

My solution is based on that of JRichardsz, but fixing a few issues and adding more transparency about what's actually going on − I'm not a fan of "copy this random code that you don't understand, and run it on your personal computer. I swear it's safe" :P

for argument in "$@"
do
  key=$(echo $argument | cut --fields 1 --delimiter='=')
  value=$(echo $argument | cut --fields 2 --delimiter='=')

  case "$key" in
    "wallet")        wallet="$value" ;;
    "network")       network="$value" ;;
    *)
  esac
done

echo "We'll be using wallet $wallet and network $network"

Invoking this script can be done as:

myScript.sh wallet=myWallet.json network=https://granadanet.smartpy.io
-2

If a function or an application has more than zero arguments, it always has a last argument.

If you want to read option flag and value pairs, as in: $ ./t.sh -o output -i input -l last

And you want to accept a variable number of option/value pairs,

And do not want a huge "if .. then .. else .. fi" tree,

Then after checking for an argument count of non-zero and even,

Write a while loop with these four eval statements as the body, followed by a case statement using the two values determined in each pass through the loop.

The tricky part of the scripting is demonstrated here:

#!/bin/sh    

# For each pair - this chunk is hard coded for the last pair.
eval TMP="'$'$#"
eval "PICK=$TMP"
eval TMP="'$'$(($#-1))"
eval "OPT=$TMP"

# process as required - usually a case statement on $OPT
echo "$OPT \n $PICK"

# Then decrement the indices (as in third eval statement) 

:<< EoF_test
$ ./t.sh -o output -i input -l last
-l 
last
$ ./t.sh -o output -l last
-l 
last
$ ./t.sh  -l last
-l 
last
EoF_test
-3
mitsos@redhat24$ my_script "a=1;b=mitsos;c=karamitsos"
#!/bin/sh
eval "$1"

you've just injected command line parameters inside script scope !!

1
  • 3
    This doesn't work with the syntax the OP specified; they want -a 1 -b mitsos -c karamitsos Feb 18 '15 at 14:48

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.