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. forf3
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.