2

In man bash it says:

wait [-fn] [id ...]

Wait for each specified child process and return its termination status. Each id may be a process ID or a job specification; if a job spec is given, all processes in that job's pipeline are waited for. If id is not given, all currently active child processes are waited for, and the return status is zero. If the -n option is supplied, wait waits for any job to terminate and returns its exit status.

And typical examples like

 command1 & command2 & command3 & wait

will mean the three commands run in parallel and the next step is only done once ALL of them are finished.

My problem lies in the results of these two bash scripts:

#!/bin/bash
for i in 1 2 3 ; do
  a=$i
  echo "$a is $i"
done 2>/dev/null

Result:

1 is 1
2 is 2
3 is 3

Pretty much what I expected. Now I assume assigning the variable is a long process, so I wait for it:

#!/bin/bash
  for i in 1 2 3 ; do
  a=$i & wait
  echo "$a is $i"
done 2>/dev/null

Result is:

3 is 1
3 is 2
3 is 3

and I am quite baffled for the following reasons:

  1. The only process wait should be waiting to finish is assigning the variable, then the next step in the script should be run (echo). a=3 should only happen in the last iteration of the loop.
  2. As far as I know, for-loops are run in subshells and wait has a scope only for the shell it was launched in. So it should not even be waiting for the for-loop to finish (as this is the parent).
  3. At no point have I specified the echo to run in parallel to some other process, so I did not expect a racing condition.

So why is the a-variable set with the last loop iteration when the variable $i is not? What part of the wait command did I misunderstand? The behavior is completely off my expectations.

GNU bash, version 5.0.3(1) on a 5.7.0-0.bpo.2-amd64 Linux kernel.

Unsetting a beforehand makes the second script return this

 is 1
 is 2
 is 3

i.e. the variable is never set and was dragged from my previous run.

2
  • 2
    You are not posting the correct output of your second script. Try it again, but unset the variable a first (with unset a). You can't assign a variable in a background task and then expect it to be set in the parent shell.
    – Kusalananda
    Commented Sep 15, 2021 at 20:16
  • @Kusalananda That makes sense.
    – FelixJN
    Commented Sep 15, 2021 at 20:24

1 Answer 1

6

for loops don't run in a subshell, e.g. you can have for i in 1 2 3; do a=foo; done; echo $a and it'll print foo, even though the variable is used outside the loop. The value of the loop variable is also whatever it was last assigned to by the loop (which might not be the last value, if the loop was break'ed out of).

But using & does put that command in a subshell, and doing a=$i & makes the assignment happen in the subshell only. You could of course use the assigned value in the subshell too:

E.g.:

a=1;
{ a=2; echo "subshell: $a"; } &
echo "main: $a";
wait;
echo "after: $a"

would print

main: 1
subshell: 2
after: 1

do something like { a=$i; echo "$a"; } & to verify.

Having the backgrounded commands access (and modify!) variables in the main shell would require some syncronization between the processes, and that would bring complexity to the shell, for probably relatively minor gain.

If you do need it though, e.g. if you have a long-running command you want to run in the background, but need the output of, you'll have to do something like storing the output temporarily in a file, e.g.

tmp=$(mktemp -d)
some long process > "$tmp/a" &
another long process > "$tmp/b" &
wait
a=$(< "$tmp/a")
b=$(< "$tmp/b")
...
rm -rf "$tmp"

Or use something like GNU Parallel that's purpose-built for running commands in parallel. It also has the parset command which should be able to assign values to a shell variable.

As for why you got 3 from $a on each iteration of the loop, that's probably because you'd run the first loop before, and it'd left a to 3. With all the subsequent assignments shoved in the subshell, that's the value that remained in a for the main shell, for the whole loop.

1
  • With parset it looks like this: parset a,b ::: 'some long process' 'another long process'
    – Ole Tange
    Commented Sep 19, 2021 at 21:08

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .