mattmilten
mattmilten

Reputation: 6706

Bash: Pass variable containing multiple variables

This is my (not working) setup:

one.sh:

#!/bin/bash -x
MYFLAGS="FLAG=foo"
echo ${MYFLAGS} | ${MYFLAGS} ./two.sh

two.sh:

#!/bin/bash -x
echo $FLAG

output:

>> one.sh
+ MYFLAGS=FLAG=foo
+ echo FLAG=foo
+ FLAG=foo ./two.sh
./one.sh: line 5: FLAG=foo: command not found

How do I pass the list (in this casse only containing one element) of variables to the second script two.sh? I know, I could use export FLAG to make it available but I would like to avoid that.

Upvotes: 0

Views: 897

Answers (1)

Charles Duffy
Charles Duffy

Reputation: 295403

Explaining The Problem

Shell variables can contain arbitrary C strings, and consequently can contain any character except NUL. Thus, any character you might put inside a variable that contained multiple other variables... could also be a valid value, and is thus unsafe to use as a generic delimiter.

There are some fairly straightforward approaches available to circumvent this:

  • Pass explicit lengths alongside the values, and split the string according to them.
  • Encode the values in a manner that restricts the characters that can be present in the data, such as to create open space in the character set to place delimiters.

Some of the more obvious means of encoding require using eval at decode time. This incurs security risks, and is thus frowned on. Workarounds to read eval-style data formats exist, but some have significant bugs/restrictions (xargs parses a subset of bash's string-literal formats, but not the full set).

The below approaches are selected to allow all possible values to be represented, and to avoid any security concerns.


Passing Arbitrary Variables Over A Pipe

To emit a NUL-delimited stream of key=value pairs (note that you can't save this in a single variable -- as the values are terminated by NUL and a literal NUL can't be stored in a string -- but you can pass it over a file descriptor):

emit_variables() {
  local var
  for var; do
    printf '%q=%s\0' "${var}" "${!var}"
  done
}

To consume it:

read_variables() {
  local varspec
  while IFS= read -r -d '' varspec; do
    name=${varspec%%=*}
    value=${varspec#*=}
    printf -v "$name" '%s' "$value"
  done
}

Thus:

# make both these functions available through the environment
export -f emit_variables read_variables

foo=$'bar\nbaz' bar='baz=qux'
emit_variables foo bar | bash -c 'read_variables; declare -p foo bar'

...will properly emit:

declare -- foo="bar
baz"
declare -- bar="baz=qux"

Extending The Above To Use The Environment

Storing that content in a variable requires escaping the data in a way that doesn't disturb the NULs. For example:

export variable_state=$(emit_variables foo bar | openssl enc -base64)

...and, it your consumer:

read_variables < <(openssl enc -base64 -d <<<"$variable_state")

Upvotes: 3

Related Questions