1

I want to assign a variable in the background while I'm showing a progress indicator like this

sleep_echo(){
  sleep $1
  echo $2
}
show_ellapsed_time () {
  #...
}

(mivar=$(sleep_echo 3 hello)) &
show_ellapsed_time $!
echo $mivar
echo "after asignment"

When I run this in terminal it outputs the progress indicator and afterwards echoes myvar

processing 00:00:03 [\]
hello
after asignment

However the assignation doesn't happen when I run it within a script

processing 00:00:03 [\]

after asignment

Why is this failing? Isn't te function invoked?

2 Answers 2

8

A child process cannot communicate with its parent process through variable assignment.

In Bash, a child process can be created in several ways :

  • Launching an external command
  • Launching a subshell by enclosing statements with ()
  • Launching a subshell when using redirection (such as pipes)
  • Launching anything in the background with &

External commands receive a copy of all shell variables that are marked for export (e.g. export VAR, local -x VAR)

Subshells receive a copy of all variables. This is useful, but makes it easy to not realize you are in a new context. Any assignment made in the child process will never be seen by the parent.

What you can do:

  • Use inter-process communication to capture the output
  • Put the information you need in a file, as files are accessible by all processes (assuming adequate permissions)
  • Use a named FIFO, which is a special kind of file allowing separate processes to communicate (see mkfifo).
  • Structure your script in a way that you keep the code blocks that most need it in the main context by using the right kind of construct to build pipes.

For that last one, using process substitution like <(COMMAND) and >(COMMAND) is often much better than the typical pipelines built with |, for instance allowing the body of a while loop (or, really, any other statement the substitution is fed to/from) to remain in the context of the script. This allows variable assignments inside loops to have the intended effect.

In your case, you have these lines:

(mivar=$(sleep_echo 3 hello)) &
show_ellapsed_time $!

Maybe you could replace them with something like :

show_ellapsed_time &
BG_PID=$!
mivar=$(sleep_echo 3 hello)
kill $BG_PID

I do not know enough about your show_ellapsed_time to know if that could work without the parameter you were passing to that function, but the assignment would work. You could also pass the PID of the main shell to the function, like this :

show_ellapsed_time $$ &
2
  • Brilliant. Thank you (and @Chris_Maes) for the quick reply.
    – dinigo
    Commented Jan 23, 2017 at 12:32
  • Note that the capacity of a FIFO is limited, so the writer might block until you actually start reading from the FIFO.
    – chepner
    Commented Jan 23, 2017 at 12:39
1

your variable is inside another scope. If you enclose some commands with round brackets ( and ), the variables you use within that scope are no longer accessible once outside that scope. An example:

(myvar=test)
echo "myvar=$myvar"

will give:

myvar=

since myvar is not known anymore. The only reason your command seems to work in the terminal; is that probably you have set your variable mivar during your tests; so the variable is still set. If you open a new terminal your script won't work anymore.

Sending the process to the background (using &); will also cause the scope to change; so even removing the brackets won't help. I think you should redesign your script (or consider using a higher level language), since it seems like you want to introduce multithreading into bash :)

1
  • Not just another scope; another process.
    – chepner
    Commented Jan 23, 2017 at 12:39

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