1

I have the following script, which should read the output from another command and then wait for changes, syncing these to a target directory. I replaced the actual commands with sleep and cat to make it easier to reproduce

cat .dirs.txt | while read dir
do
    echo "Watching files in $dir"
    while true
    do
      sleep 10
      echo "Detected change in $dir"
    done &
done
jobs
wait

I see the expected "Watching files in XY" output, but the jobs command shows no jobs, and wait immediately continues. I do see the "Detected change in XY" every 10 seconds, so the jobs are actually executing in the background. I suspect it has something to do with the read command and the pipe, because it does work with a redirect instead of a pipe. So why is this not working as I expected?

1

2 Answers 2

3

Here's a simpler reproduction of the same behavior:

( sleep 5 & )
jobs      # prints nothing
wait      # waits for nothing

This happens because there are three separate processes:

  1. The process running the script.
  2. The subshell created by ( ... ).
    • In your case this subshell is created by ... | .... Bash runs each command in a pipeline in its own subshell.
  3. The sleep command in the background (&).
    • In your case there's a much more complicated command in the background, but that doesn't make a difference.

Process #2 is a child of process #1, and process #3 starts out as a child of process #2.

The important observation is that process #3 is not a child of process #1. So process #1 can't wait for it.


In your case, as you've observed, you can fix this by using ... <.dirs.txt instead of cat .dirs.txt | ..., so that ... isn't running in a subshell, so the background process is directly a child of the process running the script.

In a more complicated case where the input isn't just straight from a file, you can typically achieve the same thing via process substitution, e.g. ... < <(cat .dirs.txt).

Alternatively, you can make process #2 wait for process #3. Since process #2 isn't running in the background, process #1 automatically waits for it, which is basically the same as if process #1 were directly waiting for process #3:

cat .dirs.txt | {
    while read dir
    do
        echo "Watching files in $dir"
        while true
        do
          sleep 10
          echo "Detected change in $dir"
        done &
    done
    wait
}
Sign up to request clarification or add additional context in comments.

Comments

1

Another way to represent the problem and solution -

$: cat file | while read l; do date +"$l %c"; sleep 5 & done; wait; date +"Finished %c"
1 Fri Nov 28 13:40:06 2025
2 Fri Nov 28 13:40:06 2025
3 Fri Nov 28 13:40:06 2025
Finished Fri Nov 28 13:40:07 2025

Notice all the timestamps are the same. It didn't wait because of the pipe, which is a UUoC.

Remove the pointless cat pipe and it works.

$: while read l; do date +"$l %c"; sleep 5 & done < file; wait; date +"Finished %c"
1 Fri Nov 28 13:39:38 2025
[1] 1606
2 Fri Nov 28 13:39:38 2025
[2] 1608
3 Fri Nov 28 13:39:38 2025
[3] 1610
[1]   Done                    sleep 5
[2]-  Done                    sleep 5
[3]+  Done                    sleep 5
Finished Fri Nov 28 13:39:43 2025

1 Comment

Sorry if I'm missing something, but this answer doesn't seem to provide any information or explanation beyond what's already in the question itself?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.