3

I have a written a bash script which uses number of parameters. I have assigned variables in the following format.

x=$1
y=$2
z=$3
k=$4

The arguments are optional, and it runs without them as well For example :

./myscript.sh
./myscript.sh x y ...

both cases are working fine.

I am looking for a better approach, from design point of view as I don't like the way the variables are getting the value.

it will not look nice if in the future my arguments increase till 9

Thanks.

Singh
  • 41
  • 7
  • I don't think there's anything much simpler than that. There's no one-liner to set multiple variables. – Barmar Oct 13 '16 at 00:08
  • See: http://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash/14203146#14203146. – agc Oct 13 '16 at 01:07
  • 4
    If your command takes 9 arguments, you probably have bigger issues than 9 assignment statements at the top of your script. – chepner Oct 13 '16 at 01:09
  • @Barmar Of course there is one: `read x y z k <<<"$@"`. Simple, clean. Read my answer. –  Oct 13 '16 at 02:20
  • Possible duplicate of [Is there a way to avoid positional arguments in bash?](http://stackoverflow.com/questions/12128296/is-there-a-way-to-avoid-positional-arguments-in-bash) – bishop Oct 13 '16 at 17:24

6 Answers6

2

You can use read combined with herestring and the quote reconstruction ability of printf:

read x y z k <<<$(printf " %q" "$@")

By example:

$ cat example.bash
#!/bin/bash
read x y z k <<<$(printf " %q" "$@")
echo "x=[$x]"
echo "y=[$y]"
echo "z=[$z]"
echo "k=[$k]"

$ ./example.bash a b "c d"
x=[a]
y=[b]
z=[c d]
k=[]

So, what's going on here? Let's work from the inside out.

printf " %q" "$@" quotes the arguments it's given in a way equivalent to the original command line arguments. Without this quoting reconstruction, command line arguments with spaces would be treated as separate arguments even if originally quoted. To see what I mean, try read x y z w <<<"$@": z is assigned "c" and k is assigned "d".

read receives the reconstituted command line, then assigns every non-escaped-space separated string into the given variables, left to right.

Back to our example:

  • "$@" is essentially a b "c d"
  • printf " %q" "$@" is a b c\ d
  • read x y z k <<<"a b c\ d" is a hard-coded representation of what you want.

While this is compact and extensible, it's also tricky. If your script takes arguments representing options (script behavior changes based on presence of absence of said arguments) then I'd suggest using getopts. If, however, your script takes many arguments representing values (like inputs into a matrix calculation) then reading into an array (read -a) might be easier to understand.


You might also want to handle the case where no command line arguments are provided. That requires a slight elaboration:

read x y z k <<<$([ 0 -eq $# ] && echo '' || printf " %q" "$@")

In this variant, the number of arguments are checked and if there are some, then the printf requoting business is performed.

Community
  • 1
  • 1
bishop
  • 37,830
  • 11
  • 104
  • 139
  • This doesn't work with new lines: `./script a b "c d" "*" LIST $'f\ng'`. –  Oct 13 '16 at 03:26
  • 1
    Nicely done; one thing to watch out for is that if there are more arguments than variables, the last variable (`k` in this case) will receive a space-separated list of _all remaining_ arguments. @sorontar: More generally, this won't work with strings containing control characters (`printf %q` represents them as `$'…'` strings); however, that's rarely a concern when passing arguments. – mklement0 Oct 13 '16 at 19:18
1

You can probably use this construct.

for i in x y z k 
do
    eval $i='$1'
    shift
done
matesc
  • 392
  • 4
  • 7
  • 1
    This could get tricky if any of the arguments contain whitespace or other metacharacters. – Keith Thompson Oct 13 '16 at 01:18
  • 1
    Nicely done. @KeithThompson: While being wary of `eval` in general is advisable, _this_ use seems to be safe, thanks to the single quotes: neither whitespace nor metacharacters in arguments break the assignment (though I'd recommend using `declare $i="$1"` instead so as not to get into the habit of using `eval`). – mklement0 Oct 13 '16 at 04:10
1

If the number of arguments is open-ended, you're better off using an array - possibly even "$@", the array of all arguments, directly (to copy all arguments to a custom array, use something like args=( "$@" )).

You can learn more about Bash arrays here.
Also, the solution below demonstrates array techniques by employing an auxiliary array to create the individual variables.

That said, if you do need distinct variable names, here's a robust approach:

This approach is probably more indirect than it needs to be, in the interest of parameterization and error handling.
See rené's helpful answer for the gist of this approach.

#!/usr/bin/env bash

# Declare the up to 9 variable names to assign to, using a helper array.
varNames=( a b c d e f g h i )

# Exit, if more arguments than available variables were specified.
(( $# > ${#varNames[@]} )) && { echo "Too many arguments." >&2; exit 2; }

# Assign to the variables in sequence, looping over the variable-names array.
for varName in "${varNames[@]}"; do
  (( $# )) || break   # Break, if there are no more arguments.
  declare "$varName"="$1"
  shift
done

To then print the resulting variables, using variable indirection:

# Print all variable values, using indirection.
for varName in "${varNames[@]}"; do  
  echo "Value of \$$varName: '${!varName}'"
done
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    @sorontar: I probably tried to show too much at once - simplified now. Still not simple, but it does have one distinct advantage: it works robustly, unlike your `<<<"$@"` solutions. If using an _array_ is an option, there is no need to resort to `read -a` or `readarray` - simply copy `"$@`" as follows: `arr=( "$@" )` (as you've just discovered) - which brings us back to my very first suggestions at the top. – mklement0 Oct 13 '16 at 03:25
1

Skip positional parameters altogether, by naming your parameters on the command line:

$ cat example.bash
#!/bin/bash
declare x=1013 y=242 z k
declare "$@" >/dev/null
echo "x=[$x]"
echo "y=[$y]"
echo "z=[$z]"
echo "k=[$k]"

$ ./example.bash x="b" z="c d" k=$'e\nf'
x=[b]
y=[242]
z=[c d]
k=[e
f]

The first declare acts as a safety net and initializes all your intended variables with default values. The second declare pulls in the named variables on the command line, treating them as variable assignment. Variables not supplied on the command line keep their initialized, default value (as in the example, y is not passed and therefore gets its default of 242). Variables other than those declared will be available in your script. This may or may not be desired.

bishop
  • 37,830
  • 11
  • 104
  • 139
0

Using "shift" you can get command line arguments as named options. See Handling positional parameters

while :
do
    case "$1" in
      -f | --file)
          file="$2"   
          shift 2
          ;;
      -h | --help)
          display_help  # Call your function
          # no shifting needed here, we're done.
          exit 0
          ;;
      -u | --user)
          username="$2" # You may want to check validity of $2
          shift 2
          ;;
      -v | --verbose)
          #  It's better to assign a string, than a number like "verbose=1"
          #  because if you're debugging the script with "bash -x" code like this:
          #
          #    if [ "$verbose" ] ...
          #
          #  You will see:
          #
          #    if [ "verbose" ] ...
          #
          #  Instead of cryptic
          #
          #    if [ "1" ] ...
          #
          verbose="verbose"
          shift
          ;;
      --) # End of all options
          shift
          break;
          ;;
      -*) # Wrong option
          echo "Error: Unknown option: $1" >&2
          exit 1
          ;;
      *)  # No more options
          break
          ;;
    esac
done

Or you can use getopts.

gile
  • 5,580
  • 1
  • 25
  • 31
0

In no case is it possible to assign a zero byte $'\0' to a variable.
A string in c ends in a zero byte, it could not contain a zero byte.
No further discussion of this issue follows.

Variables with No spaces, tabs or newlines

If you don't need to allow spaces or newlines, this will work robustly:

if (IFS=''; reg=$'[ \t\n]'; [[ "$*" =~ $reg ]] ); then
    echo "$0: error: input contains spaces, tabs or newlines" >&2
    exit 1
fi

if (( $# != 4 )); then
    echo "$0: error: we need 4 arguments"
    exit 2
fi

read x y z k <<<"$@"

printf 'x="%s" y="%s" z="%s" k="%s"\n' "$x" "$y" "$z" "$k"

If the input has either spaces, tabs or newlines the script will barf.

Use it as:

$ script one two t33 f44
x="one"   y="two"   z="t33"   k="f44"

Variables with no newlines

To make it accept spaces and tabs, we need to extend the read to:

IFS=$'\n' read -d '' x y z k < <(printf '%s\n' "$@")

$ ./script $'o\tne' 't wo' t33 f44
x="o    ne"   y="t wo"   z="t33"   k="f44"

One reasonable alternative is to use Associative Arrays (no newlines):

if (IFS=''; reg=$'[\n]'; [[ "$*" =~ $reg ]] ); then 
    echo "$0: error: input contains newlines" >&2
    exit 1
fi

varlist=(x y z k)
n=${#varlist[@]}

if (( $# != $n )); then
echo "$0: error: we need $n arguments"
exit 2
fi

declare -A arr
for i in ${varlist[@]}; do
IFS='' read "arr[$i]" 
done < <(printf '%s\n' "$@")

for i in ${!arr[@]}; do
printf '%s="%s"    ' "$i" "${arr[$i]}"
done
echo

Some changes allow to use directly variable names:

if (IFS=''; reg=$'[\n]'; [[ "$*" =~ $reg ]] ); then 
    echo "$0: error: input contains newlines" >&2
    exit 1
fi

varlist=(x y z k)
n=${#varlist[@]}

if (( $# != $n )); then
    echo "$0: error: we need $n arguments"
    exit 2
fi

for i in ${varlist[@]}; do
    IFS='' read "$i"
done < <(printf '%s\n' "$@")

for i in ${varlist[@]}; do
    printf '%s="%s"    ' "$i" "${!i}"
done
echo

And a final adaptation allow to finally accept also newlines:

varlist=(x y z k)
n=${#varlist[@]}

if (( $# != $n )); then
    echo "$0: error: we need $n arguments"
    exit 2
fi

for i in ${varlist[@]}; do
    IFS='' read -d '' "$i"
done < <(printf '%s\0' "$@")

for i in ${varlist[@]}; do
    printf '%s="%s"    ' "$i" "${!i}"
done
echo