Bash – Process substitution inside a subshell to set a variable

bashprocess-substitutionsubshellvariable

I'm trying to run a script remotely and use its standard output to populate a variable. I'm doing this to avoid temporary files.

Here's the pattern I'm trying:

var=$(bash <(curl -fsSkL http://remote/file.sh))
echo "var=${var}"

I'm testing this pattern without curl using cat:

var=$(bash <(cat ./local/file.sh))
echo "var=${var}"

This should be the same as far as syntax is concerned. ./local/file.sh contains echo hello, so I would expect var to contain the value hello, but alas, executing the above results in the following:

test.sh: command substitution: line 4: syntax error near unexpected token `('
test.sh: command substitution: line 4: `bash <(cat ./local/file.sh)'
var=

How can I accomplish my goal without using temporary files?

Best Answer

Those are the errors you get when trying to perform a process substitution in bash when the shell is running in POSIX mode. The bash shell does not support process substitutions in POSIX mode.

bash will run in POSIX mode when either

  1. set -o posix has been used, or
  2. the shell is being invoked as sh.

My hunch is that you have a script, test.sh, that you are running with sh test.sh or that has a #!/bin/sh hashbang line, and that your sh happens to be bash. Another possibility is that the script does not have #!-line at all, and it is being invoked by bash-as-sh in some other way.

Instead, see to that your test.sh script is being invoked by bash.

Example:

$ cat script.sh
echo hello
$ cat test.sh
var=$(bash <( cat script.sh ))
printf 'var="%s"\n' "$var"
$ bash -o posix test.sh
test.sh: command substitution: line 2: syntax error near unexpected token `('
test.sh: command substitution: line 2: `bash <( cat script.sh ))'
var=""
$ bash test.sh
var="hello"

If you want to do this in a portable way in your test.sh script using POSIX sh:

var=$( cat script.sh | bash )
printf 'var="%s"\n' "$var"

This would have the effect of the bash process reading the output of cat on its standard input, which is not quite the same as your process substitution variant (which leaves standard input alone). This matters if the script that the internal bash shell executes does any form of reading of data from its standard input.

If you need to leave the bash shell's standard input alone (because you need to read from it), you may possibly assume that mktemp is available and use

var=$( tmpfile=$(mktemp); cat script.sh >"$tmpfile" && bash "$tmpfile"; rm -f "$tmpfile" )
printf 'var="%s"\n' "$var"

Where cat script.sh is understood to later be replaced by your curl command.

If you're comfortable with juggling file descriptors, you could also make file descriptor 3 a copy of the standard input descriptor before calling the bash shell, and then let the fetched script read from that:

var=$( exec 3<&0; cat script.sh | bash )
printf 'var="%s"\n' "$var"

The script.sh script would then use read -u 3 to read from the original standard input stream, or utility <&3 to redirect that input stream into another utility.