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")