Quantcast
Channel: User Kamil Maciorowski - Super User
Viewing all articles
Browse latest Browse all 651

Answer by Kamil Maciorowski for How to set a global variable from a bash function when its output is piped to another command

$
0
0

Analysis

When executing a pipeline, Bash runs each part of it in a subshell. Changes made to the subshell environment cannot affect the shell's execution environment. You can think of the execution environment as a shell-specific extension to the environment: it includes aliases, shell variables, shell options and more. And like the environment gets inherited by a child but the child cannot change the environment of the parent, execution environment gets inherited by a subshell but the subshell cannot change the execution environment of the main shell.

You can make Bash run the last command of a pipeline (not executed in the background) in the current shell environment, by setting the lastpipe option of shopt (shopt -s lastpipe), but this will only work if job control is turned off (set +m). Usually in an interactive shell you want job control to be on (and it is on by default).

Since you want to pipe from your function, the function cannot be the last command of a pipeline, so lastpipe cannot help you anyway.


Portable solution

You can make a named pipe, start a receiver asynchronously, run your function as a writer not in a pipeline, then wait for the receiver to finish:

#/bin/bashtestit() {a=4echo "Hello world"}fifo='/tmp/myfifo'mkfifo -- "$fifo"{ cat -n; sleep 2; } <"$fifo" &testit >"$fifo"waitecho "$a"rm -- "$fifo"

I used cat -n so it's clear the output comes through cat. I used sleep 2 so it's clear the shell waits for the receiver. In an interactive shell (where job control is on by default) you will get messages about the job in the background; in a script (where job control is off by default) there will be no such messages. The shebang only matters if the code is executed as a script. Note if you execute the code as a script then testit will set the variable in the shell interpreting the script, not in the shell you invoke the script from (see What is the difference between executing a Bash script vs sourcing it?).

The above solution is portable (well, cat -n is not portable, but it's not a part of the solution itself, just an example receiver).


Neater solution

In Bash you can use process substitution:

#/bin/bashtestit() {a=4echo "Hello world"}testit > >(cat -n; sleep 2)waitecho "$a"

Notes

  • In each of the above solutions the receiver cannot affect the execution environment of the shell. This is true even if you use a shell function instead of cat -n; sleep 2.

  • In general you can pipe from function to function (e.g. f1 | f2 | f3 | f4 | f5) but at most one of them can be executed in the execution environment of the main shell. E.g. for f3 the solution is < <(f1 | f2) f3 > >(f4 | f5); wait.

  • If you want more than one function to affect the execution environment of the shell then you must not run them concurrently. Example:

    f1 >regular_file<regular_file f2

Conclusion

A shell function can affect the execution environment of the shell; and a shell function can be used in a pipeline. The problem is the two functionalities cannot work at the same time. In my opinion it is reasonable to design your functions (and consequently maybe even the whole workflow), so each function is supposed to do at most one of these things.


Viewing all articles
Browse latest Browse all 651

Trending Articles