4

The group command { list; } should execute list in the current shell environment.

This allows things like variable assignments to be visible outside of the command group (http://mywiki.wooledge.org/BashGuide/CompoundCommands).

I use it to send output to a logfile as well as terminal:

{ { echo "Result is 13"; echo "ERROR: division by 0" 1>&2; } | tee -a stdout.txt; } 3>&1 1>&2 2>&3 | tee -a stderr.txt;

On the topic "pipe stdout and stderr to two different processes in shell script?" read here: pipe stdout and stderr to two different processes in shell script?.

{ echo "Result is 13"; echo "ERROR: division by 0" 1>&2; }

simulates a command with output to stdout and stderr.

I want to evaluate the exit status also. /bin/true and /bin/false simulate a command that may succeed or fail. So I try to save $? to a variable r:

~$ r=init; { /bin/true; r=$?; } | cat; echo $r;
init
~$ r=init; { /bin/true; r=$?; } 2>/dev/null; echo $r;
0

As you can see the above pipeline construct does not set variable r while the second command line leads to the expected result. Is it a bug or is it my fault? Thanks.

I tested Ubuntu 12.04.2 LTS (~$) and Debian GNU/Linux 7.0 (wheezy) (~#) with the following versions of bash:

~$ echo $BASH_VERSION
4.2.25(1)-release

~# echo $BASH_VERSION
4.2.37(1)-release
Community
  • 1
  • 1
  • While `{ ... }` does imply running a simple command in the current shell environment, `|` (and certain other constructs) implies running one or more commands in a subshell, regardless of whether it's enclosed by `{ ... }` or `( ... )` or not. – twalberg Aug 05 '13 at 18:36
  • I understand. Manpage states correctly: "Each command in a pipeline is executed as a separate process (i.e., in a subshell)." That's the answer. Thanks to Douglas Leeder who pointed out how to analyse confusion about sub shells. – Hagen Riedel Aug 06 '13 at 08:43

4 Answers4

1

I tried a test program:

x=0
{ x=$$ ; echo "$$ $BASHPID $x" ; }
echo $x

x=0
{ x=$$ ; echo "$$ $BASHPID $x" ; } | cat
echo $x

And indeed - it looks like the pipe forces the prior code into another process, but without reinitialising bash - so $BASHPID changes but $$ does.

See Difference between bash pid and $$ for more details of the different between $$ and $BASHPID.

Also outputting $BASH_SUBSHELL shows that the second bit is running in a subshell (level 1), and the first is at level 0.

Community
  • 1
  • 1
Douglas Leeder
  • 52,368
  • 9
  • 94
  • 137
0

I think, you miss that /bin/true returs 0 and /bin/false returns 1

$ r='res:'; { /bin/true; r+=$?; } 2>/dev/null; echo $r;

res:0

And

$ r='res:'; { /bin/false; r+=$?; } 2>/dev/null; echo $r;

res:1

vp_arth
  • 14,461
  • 4
  • 37
  • 66
0

bash executes all elements of a pipeline as subprocesses; if they're shell builtins or command groups, that means they execute in subshells and so any variables they set don't propagate to the parent shell. This can be tricky to work around in general, but if all you need is the exit status of the command group, you can use the $PIPESTATUS array to get it:

$ { false; } | cat; echo "${PIPESTATUS[@]}"
1 0
$ { false; } | cat; r=${PIPESTATUS[0]}; echo $r
1
$ { true; } | cat; r=${PIPESTATUS[0]}; echo $r
0

Note that this only works for getting the exit status of the last command in the group:

$ { false; true; false; uselessvar=$?; } | cat; r=${PIPESTATUS[0]}; echo $r
0

... because uselessvar=$? succeeded.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
0

Using a variable to hold the exit status is no appropriate method with pipelines:

~$ r=init; { /bin/true; r=$?; } | cat; echo $r;
init

The pipeline creates a subshell. In the pipe the exit status is assigned to a (local) copy of variable r whose value is dropped.

So I want to add my solution to the orginating challenge to send output to a logfile as well as terminal while keeping track of exit status. I decided to use another file descriptor. Formatting in a single line may be a bit confusing ...

{ { r=$( { { { echo "Result is 13"; echo "ERROR: division by 0" 1>&2; /bin/false; echo $? 1>&4; } | tee stdout.txt; } 3>&1 1>&2 2>&3 | tee stderr.txt; } 4>&1 1>&2 2>&3 ); } 3>&1; } 1>stdout.term 2>stderr.term; echo r=$r

... so I apply some indentation:

{
    {
        :   # no operation
        r=$( { 
            {
                {
                    echo "Result is 13"
                    echo "ERROR: division by 0" 1>&2
                    /bin/false; echo $? 1>&4
                } | tee stdout.txt;
            } 3>&1 1>&2 2>&3 | tee stderr.txt;
        } 4>&1 1>&2 2>&3 );
    } 3>&1;
} 1>stdout.term 2>stderr.term; echo r=$r

Do not mind the line "no operation". It showed up that the forum's formatting checker relies on it and otherwise would insist: "Your post appears to contain code that is not properly formatted as code. Please indent all code by 4 spaces using the code toolbar button or the CTRL+K keyboard shortcut. For more editing help, click the [?] toolbar icon."

If executed it yields the following output:

r=1

For demonstration purposes I redirected terminal output to the files stdout.term and stderr.term.

root@voipterm1:~# cat stdout.txt
Result is 13
root@voipterm1:~# cat stderr.txt
ERROR: division by 0
root@voipterm1:~# cat stdout.term
Result is 13
root@voipterm1:~# cat stderr.term
ERROR: division by 0

Let me explain:

  1. The following group command simulates some command that yields an error code of 1 along with some error message. File descriptor 4 is declared in step 3:

                {
                     echo "Result is 13"
                     echo "ERROR: division by 0" 1>&2
                     /bin/false; echo $? 1>&4
                 } | tee stdout.txt;
    
  1. By the following code stdout and stderr streams are swapped using file descriptor 3 as a dummy. This way error messages are sent to the file stderr.txt:

            {
                 ...
             } 3>&1 1>&2 2>&3 | tee stderr.txt;
    
  1. Exit status has been sent to file descriptor 4 in step 1. It is now redirected to file descriptor 1 which defines the value of variable r. Error messages are redirected to file descriptor 2 while normal output ("Result is 13") is attached to file descriptor 3:

        r=$( { 
             ...
         } 4>&1 1>&2 2>&3 );
    
  1. Finally file descriptor 3 is redirected to file descriptor 1. This controls the output "Result is 13":
     {
         ...
     } 3>&1;
    

The outermost curly brace just shows how the command behaves.

Gordon Davisson suggested to exploit the array variable PIPESTATUS containing a list of exit status values from the processes in the most-recently-executed foreground pipeline. This may be an promising approach but leads to the question how to hand over its value to the enclosing pipeline.

~# r=init; { { echo "Result is 13"; echo "ERROR: division by 0" 1>&2; } | tee -a stdout.txt; r=${PIPESTATUS[0]}; } 3>&1 1>&2 2>&3 | tee -a stderr.txt; echo "Can you tell me the exit status? $r"
ERROR: division by 0
Result is 13
Can you tell me the exit status? init
shaedrich
  • 5,457
  • 3
  • 26
  • 42