10

I want my script to read a file containing key/value pairs of environment variables to set, and then to set them.

So far, I have this:

#!/bin/bash

cat $1 | while read kv
do
    key=${kv%=*}
    val=`echo ${kv#*=} | sed 's/^"\|"$//g'`
    export $key="$val"
done

And I want to read a file like this:

XAUTHLOCALHOSTNAME="localhost"
DISPLAY=":0"
XAUTHORITY="/tmp/some-XAuthority"

I only need these variables in scope for the duration of the script, so I don't need to solve the problem of setting a variable in script for the parent scope.

From my testing, I think my problem lies with export $key="$val", so I think I just need a replacement for that line.

1
  • a bit off topic but since i got stuck while trying to mimic your script, i'm sharing ... when grabbing a key ... using key=${kv%%=*} is better than key=${kv%=*} because %% and ## differ from % and # for whether the removed part is the shortest or longest removable part Jun 20, 2013 at 14:53

5 Answers 5

11

export $key=$val should work just fine in bash. I suspect your issue is the pipe (|). All commands in a pipeline are executed in a subshell. In your example, the instance of bash that is running your script will fork off 2 subshells: one for cat $1 and another for the while read .... The variables are assigned and exported in the subshell running the while loop, and then all of them are promptly thrown out when the subshell exits. One way to fix this is to not spawn subshells at all. Instead of a useless use of cat, try redirection instead:

while read ...; do
   ...
done < "$1"

BashFAQ 24 explains this in much greater detail.

An alternate approach is to just source the file, with exports thrown in:

. <(sed '/^export/!s/^/export /' "$1")

The <( ) is process substitution.

As an additional note of caution, since you are reading environment variables from an argument, you must make sure that the argument file $1 is a trusted source so it can't do bad things to your script.

2
  • I used redirection previously; something else must have not worked when I changed the script to use cat. But, the script with redirection works now, thanks!
    – palswim
    Feb 15, 2012 at 23:44
  • Useless use of cat considered harmful! Jun 23, 2014 at 15:36
4

(jw013 has already given a correct answer, but I want to point out a few more issues.)

The right-hand side of a pipeline runs in its own subshell in bash (as well as most other shells, the exceptions being ATT ksh and zsh). So you're setting the environment variables in the subshell that executes the loop, but not in the main shell process.

Here there's a easy fix which is to replace the useless use of cat by a redirection:

while read …; do …; done <"$1"

In general, if you need to preprocess the input, put the loop and the remainder of the script (at least the part that needs the changed variables) inside a block.

grep '^foo_' "$1" | {
  while read …; do …; done
  do_something
}

Note that in general, while read line; do … does not quite iterate over the input lines. See Why is while IFS= read used so often, instead of IFS=; while read..? for an explanation. The correct idiom to iterate over input lines is

while IFS= read -r line; do …

Here, the removal of leading and trailing whitespace doesn't matter since there shouldn't be any given your sample input, but you may need -r to avoid backslash expansion. It's possible that while read line is in fact a correct way to read your input (but I suspect only if the variable values contain no newlines). It's also possible that your input format is ambiguous or more complex to parse. Check what happens if one of the variables' value contains a newline character, a double quote or a backslash.

One point where you are definitely mangling the input is when you extract the value:

val=`echo ${kv#*=} | sed 's/^"\|"$//g'`

First, you need to use double quotes around the variable substitution: echo "${kv#*=}"; otherwise the shell performs word splitting and filename generation (i.e. globbing) on it. Second, echo is not a reliable way to print a string, because some strings that begin with a - are treated as options, and some shells perform backslash expansion on the argument. A reliable and portable way to print a string is printf %s "${kv#*=}". Also, if the value spans multiple lines, the sed program will act on each separately, which is wrong. This is fixable, but there's an easier method, which is to use the shell's string manipulation facilities: val=${kv#*=}; val=${val#\"}; val=${val%\"}.

Assuming that your input is correctly parsed with a plain read, here's a simpler way to parse it, taking advantage of the IFS setting to split each line:

while read name value; do
  value=${value#\"}; value=${value%\"}
  export name="$value"
done <"$1"
0

Another good option might be to place the export in the file and source it:

export XAUTHLOCALHOSTNAME="localhost"
export DISPLAY=":0"
export XAUTHORITY="/tmp/some-XAuthority"

and then:

. "$1"
0

/env

EXPORT=value
#NOTEXPORT=notvalue

script

#!/bin/sh

for entry in $(cat /env)
do
  if [[ ! $entry == \#* ]]
  then
    export $entry
  fi
done
0

Using eval to the rescue (This works for me):

key=andre  # Set the key
val=3000   # Set the value

eval "export ${key}=${val}";

echo $andre # Prints 3000

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .