Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

How to save output of a command that modifies environment into a variable?

I'm using bash shell.

Assume that I have:

function f () { a=3; b=4 ; echo "`date`: $a $b"; }

And now, I can use commands to run f:

$ a=0; b=0; f; echo $a; echo $b; echo $c
Sat Jun 28 21:27:08 CEST 2014: 3 4
3
4

but I would like to save output of f to variable c, so I tried:

a=0; b=0; c=""; c=$(f); echo $a; echo $b; echo $c

but unfortunatelly, I've got:

0
0
Sat Jun 28 21:28:03 CEST 2014: 3 4

so I don't have any environment change here.

How to save output of command (not only function) to variable and save environmental changes?

I know that $(...) opens new subshell and that is the problem, but is it possible to do some workaround?

share|improve this question
    
Right, so the issue is that $a and $b are local variables in your f function. You could export them, but that seems sketchy. –  HalosGhost Jun 28 at 20:00
1  
@HalosGhost: No, I don’t think so. Look at the first example: …; f; echo $a; … results in 3 being echoed, so f is modifying a shell variable (and not just its own local variable). –  Scott Jun 28 at 20:38

4 Answers 4

up vote 2 down vote accepted

If you're using Bash 4 or later, you can use coprocesses:

function f () { a=3; b=4 ; echo "`date`: $a $b"; }
coproc cat
f >&${COPROC[1]}
exec {COPROC[1]}>&-
read c <&${COPROC[0]}
echo a $a
echo b $b
echo c $c

will output

a 3
b 4
c Sun Jun 29 10:08:15 NZST 2014: 3 4

coproc creates a new process running a given command (here, cat). It saves the PID into COPROC_PID and standard output/input file descriptors into an array COPROC (just like pipe(2), or see here or here).

Here we run the function with standard output pointed at our coprocess running cat, and then read from it. Since cat just spits its input back out, we get the output of the function into our variable. exec {COPROC[1]}>&- just closes the file descriptor so that cat doesn't keep waiting forever.


Note that read takes only one line at a time. You can use mapfile to get an array of lines, or just use the file descriptor however you want to use it in a different way.

exec {COPROC[1]}>&- works in current versions of Bash, but earlier 4-series versions require you to save the file descriptor into a simple variable first: fd=${COPROC[1]}; exec {fd}>&-. If your variable is unset it will close standard output.


If you're using a 3-series version of Bash, you can get the same effect with mkfifo, but it's not much better than using an actual file at that point.

share|improve this answer
    
I can use only bash 3.1. The pipes are hard to use. Does mktemp and is the only option? Aren't here special syntax or option to run command and get output but with environment changes? –  faramir Jun 29 at 17:04
    
You can use mkfifo to make a named pipe as a substitute for coproc, which doesn't involve actually writing the data to disk. With that you'd redirect output to the fifo and input from it instead of the coprocess. Otherwise no. –  Michael Homer Jun 29 at 22:10
    
+1 for adapting my answer not to use a file. But, in my tests, the exec {COPROC[1]}>&- command (at least sometimes) terminates the coprocess immediately, so its output is no longer available to be read. coproc { cat; sleep 1; } seems to work better. (You may need to increase the 1 if you’re reading more than one line.) –  Scott Jun 30 at 18:10
1  
There is a conventional, well-understood meaning of "actual file" in use here that you're wilfully disregarding. –  Michael Homer Jul 1 at 23:42
1  
"Actual file" meant a file on disk, as understood by everyone here except, allegedly, you. Nobody has ever implied or permitted a reasonable reader to infer that either of them was the same as arguments or environment variables. I am not interested in continuing this discussion. –  Michael Homer Jul 2 at 2:27

If you're already counting on the body of f executing in the same shell as the caller, and thus being able to modify variables like a and b, why not make the function just set c as well? In other words:

$ function f () { a=3; b=4 ; c=$(echo "$(date): $a $b"); }
$ a=0; b=0; f; echo $a; echo $b; echo $c
3
4
Mon Jun 30 14:32:00 EDT 2014: 3 4

One possible reason may be that the output variable needs to have different names (c1, c2, etc) when called in different places, but you could address that by having the function set c_TEMP and having the callers do c1=$c_TEMP etc.

share|improve this answer

It’s a kluge, but try

f > c.file
c=$(cat c.file)

(and, optionally, rm c.file).

share|improve this answer
    
Thank you. That is simple answer, and works, but have some drawbacks. I know that I can use mktemp, but I would like not to create additional files. –  faramir Jun 29 at 17:06

Just assign all of the variables and write the output at the same time.

f() { c= ; echo "${c:=$(date): $((a=3)) $((b=4))}" ; }

Now if you do:

f ; echo "$a $b $c"

Your output is:

Tue Jul  1 04:58:17 PDT 2014: 3 4
3 4 Tue Jul  1 04:58:17 PDT 2014: 3 4

Note that this is fully POSIX portable code. I initially set c= to the '' null string because parameter expansion limits concurrent variable assignment + evaluation to either only numeric (like $((var=num))) or from null or nonexistent values - or, in other words, you can't concurrently set and evaluate a variable to an arbitrary string if that variable is already assigned a value. So I just ensure that it is empty before trying. If I did not empty c before attempting to assign it the expansion would only return the old value.

Just to demonstrate:

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2

newval is not assigned to $c inline because oldval is expanded into ${word}, whereas the inline $((arithmetic=assignment)) always occurs. But if $c has no oldval and is empty or unset...

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a)) 
    c= a=$((a+a))
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2
newval 8

...then newval is at once assigned to and expanded into $c.

All other ways to do this involve some form of secondary evaluation. For example, let's say I wished to assign the output of f() to a variable named name at one point and var at another. As currently written, this will not work without setting the var in the caller's scope. A different way could look like this, though:

f(){ fout_name= fout= set -- "${1##[0-9]*}" "${1%%*[![:alnum:]]*}"
    (: ${2:?invalid or unspecified param - name set to fout}) || set --
    export "${fout_name:=${1:-fout}}=${fout:=$(date): $((a=${a:-50}+1)) $((b=${b:-100}-4))}"
    printf %s\\n "$fout"
}

f &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$a" "$b"

I've provided a better formatted example below, but, called as above the output is:

sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
51
96

Or with different $ENV or arguments:

b=9 f myvar &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$myvar" \
        "$a" "$b"

###OUTPUT###

Tue Jul  1 19:56:42 PDT 2014: 52 5
myvar
Tue Jul  1 19:56:42 PDT 2014: 52 5
Tue Jul  1 19:56:42 PDT 2014: 52 5
52
5

Probably the trickiest thing to get right when it comes to twice evaluating is to ensure that variables don't break quotes and execute random code. The more times a variable is evaluated the harder it gets. Parameter expansion helps a great deal here, and using export as opposed to eval is far safer.

In the above example f() first assigns $fout the '' null string and then sets positional params to test for valid variable names. If both tests do not pass a message is emitted to stderr and the default value of fout is assigned to $fout_name. Regardless of the tests, however, $fout_name is always assigned to either fout or the name you specify and $fout and, optionally, your specified name are always assigned the output value of the function. To demonstrate this I wrote this little for loop:

for v in var '' "wr;\' ong"
    do sleep 10 &&
        a=${a:+$((a*2))} f "$v" || break
    echo "${v:-'' #null}" 
    printf '#\t"$%s" = '"'%s'\n" \
        a "$a" b "$b" \
        fout_name "$fout_name" \
        fout "$fout" \
        '(eval '\''echo "$'\''"$fout_name"\")' \
            "$(eval 'echo "$'"$fout_name"\")"
 done

It plays around some with variable names and parameter expansions. If you have a question just ask. That only runs the same few lines in the function already represented here. It's worth mentioning at least though that the $a and $b variables behave differently depending on whether they are defined at invocation, or set already. Still, the for does almost nothing but format the data set and provided by f(). Have a look:

###OUTPUT###

Wed Jul  2 02:50:17 PDT 2014: 51 96
var
#       "$a" = '51'
#       "$b" = '96'
#       "$fout_name" = 'var'
#       "$fout" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:27 PDT 2014: 103 92
'' #null
#       "$a" = '103'
#       "$b" = '92'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:37 PDT 2014: 207 88
wr;\' ong
#       "$a" = '207'
#       "$b" = '88'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.