8

I'd like to assign the contents of variables of another bash script to variables in the calling script.

Specifically, I source this file: https://projects.archlinux.org/svntogit/packages.git/plain/trunk/PKGBUILD?h=packages/firefox (and other alike files).

The file contains variables named depends, makedepends, etc.

So in my script I have multiple statements like these:

depends="$(source "/path/to/file" ; printf '%s' "${depends[@]}")"
makedepends="$(source "/path/to/file" ; printf '%s' "${makedepends[@]}")"
...

So basically, each statement starts it's own subshell which sources the file and prints the contents of just ONE variable to a variable in the parent shell.

Is there another way which involves to start just a SINGLE subshell, source the file and get the contents of specified variables of the file assigned to specified variables in the calling shell without polluting the environment of the calling shell?

------------------------------------------------------------------------

Since Mark Mann pointed out the dangers of source-ing foreign scripts, I ended up with another solution. Instead of using source to get variables of another script, one can use a multi line grep with a perl regex to grep all needed variables from the file (varName=(...),varName2="...",varname3='...',varName4=...) and eval the result:

$ grepvars='(license)|(depends)|(makedepends)|(url)|(pkgdesc)|(pkgver)'
$
$ eval $(grep -Pzo "^(${grepvars})=\([^\)\(\`]*\)|^(${grepvars})=\"[^\"\(\`]*\"|^(${grepvars})='\''[^'\'']*'\''|^(${grepvars})=[^\s;\(\`]*" /tmp/above_mentioned_file)
$ echo $url
https://www.mozilla.org/firefox/
$
$ echo ${depends[@]}
gtk3 gtk2 mozilla-common libxt startup-notification mime-types dbus-glib alsa-lib ffmpeg2.8 desktop-file-utils hicolor-icon-theme libvpx icu libevent nss hunspell sqlite ttf-font
7
  • 1
    You could source it in a subshell that does all the commands that require those variables. Is there a big problem with having those variables in the calling shell though? Will the mess up future steps or something? Feb 29, 2016 at 23:08
  • This is a general question to improve my bash code - I treat it like this: I assume, each file I source directly in the parent shell will possibly overwrite variables in the parent shell. Also, basically all follow up code in the specific script depends on the information retrieved from the file.
    – MCH
    Feb 29, 2016 at 23:11
  • That's true that they will overwrite existing variables, but I don't think your proposed approach is going to make for better scripting. If you have that much data to manage, perhaps it's time to move to a more traditional programming language instead of a shell Feb 29, 2016 at 23:12
  • You may be right that this is better to be handled by python, perl, etc. :)
    – MCH
    Feb 29, 2016 at 23:16
  • @MCH: No, the grep solution doesn't mitigate the problem. Imagine a line that reads url=http://example.com; rm -rf $HOME. If you want to be safe we should probably parse the file directly.
    – unpythonic
    Mar 1, 2016 at 19:17

5 Answers 5

12

Use eval.

If you have your source (in /tmp/other.sh):

a=1
b=2
c=3

And you want only a portion, you can use eval to get just those items (here in /tmp/main.sh):

eval $(source /tmp/other.sh;
       echo a="$a";
       echo b="$b";)

echo a is $a "(expect 1)"
echo b is $b "(expect 2)"
echo c is $c "(expect nothing)"

And running it:

$ bash /tmp/main.sh
a is 1 (expect 1)
b is 2 (expect 2)
c is (expect nothing)

WARNING: Performing an eval or source on an untrusted script is very dangerous. You're executing a shell script, and that script can perform anything you could do yourself. WARNING

8
  • 3
    This only works if the values don't contain any shell special character. You can make this robust, but it requires some effort. Feb 29, 2016 at 23:29
  • Well now I'm interested! How can you make this robust --- why isn't this robust?
    – MCH
    Feb 29, 2016 at 23:31
  • Are you saying, I need to escape all shell special chars before eval?
    – MCH
    Feb 29, 2016 at 23:38
  • 1
    As soon as you're executing source against a foreign script, you're in very dangerous territory. Escaping the actual values matters little if the script has a rm -rf $HOME. Updating answer to reflect this.
    – unpythonic
    Feb 29, 2016 at 23:45
  • You're obviously right - tbh I didn't even think of rm -rf $HOME :) So eval cat script in subshell ?!? :)
    – MCH
    Feb 29, 2016 at 23:51
3

Without using eval you can still do this via process substitution if you're using bash or similar:

source <(source "/path/to/file" ; printf %s\\n "depends=${depends[*]}" "makedepends=${makedepends[*]}")

This starts a subshell and sources the file, just like your initial example, but then instead of directly printing the value and using command substitution, it prints the value formatted as an assignment and sources the output using process substitution.

This exactly answers the following part of your question:

Is there another way which involves to start just a SINGLE subshell, source the file and get the contents of specified variables of the file assigned to specified variables in the calling shell without polluting the environment of the calling shell?

But of course the dangers of sourcing foreign scripts still apply. Don't source an untrusted script, ever.

If you wrote the script yourself and you are absolutely sure it has no side effects (and produces no output) then you could use the above for your own personal computer. It's not something that should ever be in a production script. Very very big security hole.

3
  • Running source "/path/to/file" will still execute any commands found in that file. This is the root of the insecurity. I've amended my comments above.
    – unpythonic
    Mar 1, 2016 at 21:22
  • @MarkMann, absolutely correct. I will edit my answer to emphasize that point. I just wanted to provide the alternate answer; I think source with process substitution makes more sense than eval with command substitution. Either one involves sourcing a foreign script, though.
    – Wildcard
    Mar 1, 2016 at 21:29
  • Also your solution works if there is a variable with multiple words. You just need to escape " character like this: printf %s\\n "var1=\"$var1\"" Nov 16, 2017 at 15:12
0

source <(cat "/path/to/file" | grep "depends\|makedepends")

The /path/to/file is not sourced at all (vs. the approach of Wildcard’s) . See discussion about insecurity above. Also if it is, for example .env file of docker-compose or similar, it could be used in a such a way to draw variables by other scripts without executing unquoted variables, those which can cause execution errors. For example - traefik frontend rule like, WEB_FRONTEND_RULE=Host:my.host.name;PathPrefix:/my-path-prefix which is valid in docker-compose .env is not valid to be sourced by bash. And WEB_FRONTEND_RULE='Host:my.host.name;PathPrefix:/my-path-prefix' is valid to be sourced by bash, but is not valid in docker-compose .env.

0
0

Source or env both process the whole script, the danger lies in where and who has access to execute it. When you are using it internally it's fine.

On the topic, if you source a script it kind of runs like it was in the same script so your variables will just be set. So if script 1 is /tmp/script1.sh:

a=1
b=2
c=3

and script 2 like /tmp/script2.sh

source /tmp/script1.sh
echo $a
echo $b
echo $c

it will print when run:

1 2 3

For me that is the most common use case, for example when delegating common functionality between scripts (like os detection).

This is perhaps what you mean with pollution, but perhaps you can define the source script with their own vars and this is fine? It depends a bit on what you really want to achieve. The eval based answer also 'pollutes' in a similar way but gives you more control. If you want to reuse data, maybe it is easier to let the scripts you call just write to a text file and use that, or indeed use perl, grep, sed or any other great unixy tool to do what you want.

0

Expanding on the answer from Wildcard, and after sourcing the file (with all the same caveats around only doing this only if you trust the file), we can identify exported variable names by scanning for them, get their declaration and re-export them.

source-export() {
  local _RC=0
  local FILENAME=$1
  shift
  source <(
    source "${FILENAME}" "$@"
    _RC=$?
    if (( _RC )); then
      typeset -p _RC
      exit
    fi
    # show the exported names
    PATTERN="\b(?<=export)(\s+\w+)+"
    while read -r NAME; do
      [[ -n ${NAME} ]] && typeset -p "${NAME}"
    done < <(grep -oP "${PATTERN}" "${FILENAME}" | tr ' ' '\n')
  )
  return ${_RC}
}

Note: the pattern we use to scan for exports should be reasonably robust (handling cases like export A B). Another potentially more robust approach would be to diff the variable table pre- and post-sourcing for exported variables.

Example:

$ cat foo.sh
export XXX="432"
YYY=111
export ZZZ="'adfa $*"
export A B
A=1
B=2
C=3

$ source-export foo.sh 3 2 1
$ echo -e "XXX=$XXX\nYYY=$YYY\nZZZ=$ZZZ\nA=$A\nB=$B\nC=$C"
XXX=432
YYY=
ZZZ='adfa 3 2 1
A=1
B=2
C=

versus:

$ source foo.sh 3 2 1
$ echo -e "XXX=$XXX\nYYY=$YYY\nZZZ=$ZZZ\nA=$A\nB=$B\nC=$C"
XXX=432
YYY=111
ZZZ='adfa 3 2 1
A=1
B=2
C=3

Tested on bash and zsh.

You must log in to answer this question.

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