Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I would like to pass params to a bash script, dd-style. Basically, I want

./script a=1 b=43

to have the same effect as

a=1 b=43 ./script

I thought I could achieve this with:

for arg in "$@"; do
   eval "$arg";
done

What's a good way of ensuring that the eval is safe, i.e. that "$arg" matches a static (no code execution), variable assignment?

Or is there a better way to do this? (I would like to keep this simple).

share|improve this question
    
This is tagged with bash. Do you want a Posix compliant solution, or will you accept bash solutions? –  rici 17 hours ago
    
What the tag says is what I mean :) –  PSkocik 17 hours ago

5 Answers 5

up vote 12 down vote accepted

You can do this in bash without eval (and without artificial escaping):

for arg in "$@"; do
  if [[ $arg =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
    declare +i +a +A "$arg"
  fi
done

Edit: Based on a comment by Stéphane Chazelas, I added flags to the declare to avoid having the variable assigned being already declared as an array or integer variable, which will avoid a number of cases in which declare will evaluate the value part of the key=val argument. (The +a will cause an error if the variable to be set is already declared as an array variable, for example.) All of these vulnerabilities relate to using this syntax to reassign existing (array or integer) variables, which would typically be well-known shell variables.

In fact, this is just an instance of a class of injection attacks which will equally affect eval-based solutions: it would really be much better to only allow known argument names than to blindly set whichever variable happened to be present in the command-line. (Consider what happens if the command line sets PATH, for example. Or resets PS1 to include some evaluation which will happen at the next prompt display.)

Rather than use bash variables, I'd prefer to use an associative array of named arguments, which is both easier to set, and much safer. Alternatively, it could set actual bash variables, but only if their names are in an associative array of legitimate arguments.

As an example of the latter approach:

# Could use this array for default values, too.
declare -A options=([bs]= [if]= [of]=)
for arg in "$@"; do
  # Make sure that it is an assignment.
  # -v is not an option for many bash versions
  if [[ $arg =~ ^[[:alpha:]_][[:alnum:]_]*= &&
        ${options[${arg%%=*}]+ok} == ok ]]; then
    declare "$arg"
    # or, to put it into the options array
    # options[${arg%%=*}]=${arg#*=}
  fi
done
share|improve this answer
1  
The regex seems to have the brackets wrong. Perhaps use this instead: ^[[:alpha:]_][[:alnum:]_]*=? –  lcd047 17 hours ago
1  
@lcd047: foo= is the only way to set foo to the empty string, so it should be allowed (IMHO). I fixed the brackets, thanks. –  rici 17 hours ago
1  
declare is about as dangerous as eval (one may even say worse as it's not as apparent that it is as dangerous). Try for instance to call that with 'DIRSTACK=($(echo rm -rf ~))' as argument. –  Stéphane Chazelas 17 hours ago
1  
@PSkocik: +x is "not -x". -a = indexed array, -A = associative array, -i = integer variable. Thus: not indexed array, not associative array, not integer. –  lcd047 16 hours ago
1  
Note that with the next version of bash, you may need to add +c to disable compound variables or +F to disable floating ones. I'd still use eval where you know where you stand. –  Stéphane Chazelas 16 hours ago

This seems to work as expected:

regexp='^[a-zA-Z][a-zA-Z0-9]*=[a-zA-Z0-9]*$'
for arg in "$@"; do
  [[ "$arg" =~ $regexp ]] &&\
  eval "$arg"
done

If you can improve it (make it terser or more general), I will delete this answer and accept the best improvement on this.

share|improve this answer

My attempt:

#! /usr/bin/env bash
name='^[a-zA-Z][a-zA-Z0-9_]*$'
count=0
for arg in "$@"; do
    case "$arg" in
        *=*)
            key=${arg%%=*}
            val=${arg#*=}

            [[ "$key" =~ $name ]] && { let count++; eval "$key"=\$val; } || break

            # show time
            if [[ "$key" =~ $name ]]; then
                eval "out=\${$key}"
                printf '|%s| <-- |%s|\n' "$key" "$out"
            fi
            ;;
        *)
            break
            ;;
    esac
done
shift $count

# show time again   
printf 'arg: |%s|\n' "$@"

It works with (almost) arbitrary garbage on the RHS:

$ ./assign.sh Foo_Bar33='1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0' '1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0=33'
|Foo_Bar33| <-- |1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0|
arg: |1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0=33|

$ ./assign.sh a=1 b=2 c d=4
|a| <-- |1|
|b| <-- |2|
arg: |c|
arg: |d=4|
share|improve this answer
    
shift will kill the wrong things if you don't break the loop at the first non-x=y parameter –  frostschutz 18 hours ago
    
@frostschutz Good point, edited. –  lcd047 18 hours ago
    
Nice job at generalizing it. I think it can be simplified a little. –  PSkocik 17 hours ago
    
Did you get the chance to take a look at my edit? –  PSkocik 17 hours ago
    
Please take a look at my edit. That's the way I like it (+maybe just shift instead of shift 1). Otherwise thanks! –  PSkocik 17 hours ago

lcd047's solution refactored:

 while [[ $1 =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; do
      key=${1%%=*}
      val=${1#*=}
      shift
      eval "$key"=\$val
 done

frostschutz for deserves the credit for most of the refactoring. Adding a prefix to protect against tampering with system variables (thanks, Stéphane Chazelas) is easy with this method too.

share|improve this answer
    
I like this too, cuz it's explicit (unlike the magical, but better, declare). This removes one nesting level. The shifting inside the loop seems to work, I don't know if it's guaranteed to or not. –  PSkocik 17 hours ago
1  
but you can get rid of *=* and stop substituting key/val where there is no =. (since you were refactoring) :P –  frostschutz 17 hours ago
1  
in fact you can get rid of the for loop and the if and use while $1 instead, since you're shifting and all... –  frostschutz 17 hours ago
1  
Heh, the proof that brainstorming works. :) –  lcd047 16 hours ago
1  
Harvesting morning ideas: you can even get rid of key and val, and just write eval "${1%%=*}"=\${1#*=}. But that's pretty much as far as it goes, eval "$1" as in @rici's declare "$arg" won't work, obviously. Also beware of setting things like PATH or PS1. –  lcd047 7 hours ago

A POSIX one (sets $<prefix>var instead of $var to avoid problems with special variables like IFS/PATH...):

prefix=my_prefix_
for var do
  case $var in
    (*=*)
       case ${var%%=*} in
         "" | *[!abcdefghijiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_]*) ;;
         (*) eval "$prefix${var%%=*}="'${var#*=}'
       esac
  esac
done

Called as myscript x=1 PATH=/tmp/evil %=3 blah '=foo' 1=2, it would assign:

my_prefix_x <= 1
my_prefix_PATH <= /tmp/evil
my_prefix_1 <= 2
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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