283

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?

3
  • 2
    have a look at docopt – it helps with named parameters and does input validation, too
    – Beat
    Commented May 14, 2014 at 21:55
  • Relevant: stackoverflow.com/questions/5499472/…
    – Kaz
    Commented Aug 24, 2017 at 4:47
  • Just wondering, why is this preferable to using env vars? P_OUT='/some/path' ARG_1=5 my_script works with no extra steps. You can reference the env vars directly in the script, with defaults, error handling for missing values etc. I'm trying to imagine why the extra effort for command-line params is worthwhile. Commented Sep 5, 2023 at 14:51

15 Answers 15

280

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.

9
  • 17
    I know this is a bit old, but why only 1 letter for the arguments?
    – Kevin
    Commented Apr 28, 2017 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. Commented Feb 14, 2018 at 22:48
  • 6
    @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
    Commented May 24, 2018 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. Commented Jan 9, 2020 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
    Commented Aug 17, 2021 at 17:30
101

This is not a parser for positioned arguments, is for key=value aa=bb arguments;

for ARGUMENT in "$@"
do
   KEY=$(echo $ARGUMENT | cut -f1 -d=)

   KEY_LENGTH=${#KEY}
   VALUE="${ARGUMENT:$KEY_LENGTH+1}"

   export "$KEY"="$VALUE"
done

# use here your expected variables
echo "STEPS = $STEPS"
echo "REPOSITORY_NAME = $REPOSITORY_NAME"
echo "EXTRA_VALUES = $EXTRA_VALUES"

Usage

bash args_shell_parser.sh STEPS="ABC" REPOSITORY_NAME="stackexchange" \
           EXTRA_VALUES="KEY1=VALUE1 KEY2=VALUE2"

Console result:

STEPS = ABC
REPOSITORY_NAME = stackexchange
EXTRA_VALUES = KEY1=VALUE1 KEY2=VALUE2

Features:

  • It does not matter what order the arguments are in
  • Explicit declaration of all variables are not required
  • values could have spaces.
  • It handles complex cases, when the argument value contains "=" sign

Put this snippet at the start of your script.

Prior version of this script:

13
  • 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
    Commented Dec 13, 2019 at 14:01
  • 1
    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 Commented Nov 4, 2020 at 10:32
  • 1
    Thanks for your suggestion #BohdanShulha It had an error. I fixed: gist.github.com/jrichardsz/…
    – JRichardsz
    Commented Jan 23, 2022 at 21:28
  • 2
    You can also support passing argument names with an optional -- prefix (e.g. --profile=test) if you add [[ $KEY == --* ]] && KEY=${KEY:2} right before the export "$KEY"="$VALUE" line. Commented Jun 20, 2023 at 16:48
  • 1
    support passing argument names with an optional -- prefix could use for ARGUMENT in "$@";do export $(echo $ARGUMENT | cut -f1 -d=|cut -d '-' -f 3)=${ARGUMENT:$(echo $ARGUMENT | cut -f1 -d=|wc -L)+1}; done
    – steven
    Commented Oct 19, 2023 at 8:46
78

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.

4
  • 19
    The caveat is not necessary. :) You can have the conditions as follows: -c|--condition) Commented May 30, 2017 at 14:27
  • Docs to truncate string in variable (and to continue the trend in this thread, this should be the accepted answer).
    – toraritte
    Commented Aug 8, 2020 at 16:31
  • @Milkncookiez: I am a noob, can you make the full code? Thanks! Commented Jul 3, 2023 at 2:17
  • 3
    @LeQuangNam In the example above, change --p_out to -p|--p_out Commented Jul 7, 2023 at 10:37
48

The probably closest syntax to that is:

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

Update 1

Nearly a decade later I realize that it gets much closer than that (not sure whether it already was back then).

man -P 'less -p "^\s+set \["' bash
set [-abefhkmnptuvxBCEHPT] [-o option-name] [--] [-] [arg ...]

-k      

All arguments in the form of assignment statements are placed in the environment for a command, not just those that precede the command name.

So you can do this, too:

set -k
my_script p_out='/some/path' arg_1='5'

A bit more complicated but safer:

if [[ $- =~ k ]]; then
    # set -k has already been set so no need to reverse it after the command
    my_script p_out='/some/path' arg_1='5'
else
    set -k
    my_script p_out='/some/path' arg_1='5'
    set +k
fi

You can test this with:

set -k
echo "\$foo: '$foo'"; bash -c 'echo $foo' foo=bar
8
  • 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
    Commented May 14, 2014 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 Commented Mar 19, 2021 at 2:56
  • @StayFoolish One way would be to run set -k, then run my_script p_out='/some/path' arg_1='5'.
    – chepner
    Commented Mar 19, 2021 at 12:21
  • This is by far the easiest (and imho best) solution. It's idiomatic bash, and you don't need any parser logic which is just needless complexity (and hard to reuse). Commented May 12, 2023 at 17:17
  • 1
    Remember to call set +k when finish calling the command, otherwise, you can face the problem when using Bash completion. Commented Jul 2, 2023 at 9:33
24

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
23

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.

3
  • 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
    Commented Sep 24, 2018 at 9:45
  • NB shift removes the 1st argument from the start of the args list
    – mirekphd
    Commented Jul 24, 2022 at 13:18
  • Should also shift within the if since you've consumed 2 args, right?
    – Josh M.
    Commented Aug 22, 2022 at 13:22
21

CAVEAT
The solution below does not check for incomplete / malformed command invocation! For example, if --p_out requires an argument, but the command is called as my-script.sh --p_out --arg_1 27 then it will happily assign the string "--arg_1" to the p_out variable.

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

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

From 3.5.3 Shell Parameter Expansion of the GNU Bash manual:

${parameter:-word}
If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.

#!/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

# Example of using the saved command line arguments.
# WARNING: Any of these may be an empty string, if the corresponding
#          option was not supplied on the command line!

echo "Without default values:"
echo "p_out: ${p_out}"
echo "arg_1: ${arg_1}"; echo

# Example of using parsed command line arguments with DEFAULT VALUES.
# (E.g., to avoid later commands being blown up by "empty" variables.)

echo "With default values:"
echo "p_out: ${p_out:-\"27\"}"
echo "arg_1: ${arg_1:-\"smarties cereal\"}"

If the above script is saved into my-script.sh, then these 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"

(where . is Bash's source command.)

To demonstrate the default values part, here's the output without any arguments given:

$ . my-script.sh
Without default values:
p_out:
arg_1:

With default values:
p_out: "27"
arg_1: "smarties cereal" 
0
19

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
    Commented Nov 7, 2017 at 14:59
  • @Timo, see info zsh zparseopts for details Commented Nov 7, 2017 at 16:20
4

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}
2

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.

1

I want the script to function consistently with other utilities, providing the least surprise to the user. Here is what I came up with, combining several ideas here (especially the answer from cdmo) and some additional research. It has the following features:

Let's assume we have the following command, accepting three positional parameters and a named foo parameter:

munge [--foo=bar] x y z

#!/bin/bash
set -eu

usage() {
  echo 'Usage: munge [--foo=bar] <x> <y> <z>' >&2
}

args=()
foo="default value"
while [ $# -gt 0 ]; do
  case "$1" in
### start custom parameter checks
    --foo=*)
      foo="${1#*=}"
      shift
      ;;
    --foo*)
      foo="$2"
      shift
      shift
      ;;
### end custom parameter checks
    --help)
      usage
      exit 0
      ;;
    --)
      shift
      while [ $# -gt 0 ]; do args+=("$1"); shift; done;
      break;
      ;;
    --*)
      echo "**Bad parameter: $1**" >&2
      usage
      exit 1
      ;;
    *)
      args+=("$1");
      shift
      ;;
  esac
done

if [[ ${#args[@]} -ne 3 ]]; then
  echo "**Incorrect number of arguments: ${#args[@]}**" >&2
  usage
  exit 1
fi

# test
echo foo: $foo
echo arg 1: ${args[0]}
echo arg 2: ${args[1]}
echo arg 3: ${args[2]}

This accepts the following:

  • munge blah blah blah
  • munge --foo=bar blah blah blah
  • munge --foo bar blah blah blah
  • munge --foo=bar -- blah blah blah
  • munge --foo bar -- blah blah blah
  • munge blah --foo=bar blah blah
  • munge blah --foo bar blah blah
  • munge blah blah blah --foo=bar
  • munge blah blah blah --foo bar

Caveats:

  • The program must henceforth use the args array (e.g. {$args[0]}) rather than $* (e.g. $1) and the like.
  • Usage is always sent to stderr, when for --help it should more correctly be sent to stdout.
  • If a known long parameter comes at the end without the value, the error message will say $2: unbound variable instead of something useful. (The additional error handling would just be too verbose for the benefit.)

Yeah, it's verbose and clumsy, but it is most consistent with other CLI utilities. Bash really needs something better. It this area Bash is really pitiful in comparison to PowerShell (which of course has its own problems).

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
-2

function satunnaisluku () { let $1=$RANDOM ;}; satunnaisluku apu; satunnaisluku kipu; echo $apu; echo $kipu

  • there are several other methods to use nameparameters. Forexample method to find maximum from matrix:

function haemaksimi () { maksimi=$(echo $(sort -n <(declare -p $1 | tr ' ' '\n' | tr -dc '\n'[0-9])) | sed 's/.* //') ;} ; apu=""; for n in {1..10000}; do apu=$apu" "$RANDOM$RANDOM; done; unset maksimi; time haemaksimi matrix; echo $maksimi


I have made IDE for BASH-scripting to myself - some few thousand lines - and I have used a function like those for ten years - for reading items in different tasks in the same session - nowadays my days are full of scripting because a busted leg.

If you use something very heavily for ten years and it always operates impeccably without doing any harm to other programs isn't it quite the same how it is called?

I stumble constantly to warnings: BASH cannot do that ... Sour grapes I say.

2
  • 1) see the editing help about code formatting, 2) properly indenting the code would make it far, far easier to read for others, 3) echo $(foo) is usually unnecessary, it's almost the same as just foo, except for some shell oddities. I don't know if you're relying on those, since 4) there's really no description of the code here, which makes it harder to figure out what you're showing. 5) function foo() is non-standard, and usually better written as just foo()
    – ilkkachu
    Commented Aug 15, 2023 at 6:47
  • Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
    – Community Bot
    Commented Aug 15, 2023 at 15:09
-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 Commented Feb 18, 2015 at 14:48

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.