2

I am trying to do something that involves scripts calling other scripts in subshells to capture their output.

One of the scripts needs to have a side effect of starting a background process. This all works when executed directly, but it blocks when called within a subshell.

As a self-contained example consider the following 2 scripts:

test1.sh

#!/bin/bash
echo $(./test2.sh)

test2.sh

#!/bin/bash
(yes > /dev/null ; echo 'yes killed') &
echo success

When I run test2.sh by itself, I get the expected result of "success" on the terminal, and yes running in the background. Killing yes prints "yes killed" to the terminal as expected.
When I run test1.sh I expect to get the same behavior, but what actually happens is that the terminal hangs until I kill yes after which "success yes killed" is printed to the terminal.

What do I change to these scripts so that I can get the same behavior from calling either one?

The premise here is that the subshell evaluation in test1.sh will actually be stored in a variable for later use. The background process started in test2.sh should live past the execution of either script.

4 Answers 4

4

As @choroba and @GordonDavisson suggested, command substitution $( ... ) will not return until all of its stdout has disconnected [1].

The trick here is that even if you redirect all stdout of the commands, the subshell itself eg. ( ... ) still has its stdout attached. This means that the following will not work:

#!/bin/bash
(yes > /dev/null ; echo 'yes killed' > /dev/null) &
echo success

But this will:

#!/bin/bash
(yes ; echo 'yes killed' ) > /dev/null &
echo success

Note: You will no longer get any output to the terminal or stdout like this, but for my purpose this is not an issue. If necessary you could always redirect to a file or something if the output is needed


[1] See also https://stackoverflow.com/questions/16874043/bash-command-substitution-forcing-process-to-foreground

1
  • That second code block worked perfectly for me! I used your trick to run something like the following: (mkfifo pipe && gzip -d file.gz > pipe &) > /dev/null & Nov 15, 2018 at 18:02
3

Command substitution $(...) turns output into arguments. It first needs the whole output, because it's not possibly to supply arguments dynamically one by one.

3
  • The thing is, I don't care about the output of the background process (in reality it will have none, this is just an example)
    – vehystrix
    May 25, 2016 at 13:43
  • @vehystrix By running the script in $( ... ), you're telling the shell you do care about its output. If you don't need the output, don't capture it; just discard it with >/dev/null (and maybe >/dev/null 2>&1 to discard stderr as well). Also, while the second script might not output anything, the parent shell can't tell that until it closes its stdout... which doesn't happen until either the background command exits, or you explicitly close it in the background process. May 26, 2016 at 2:11
  • @GordonDavisson Looks like this is indeed part of the solution, after some prodding around I seem to have it working now. I will post an answer with my exact solution.
    – vehystrix
    May 26, 2016 at 5:52
2

I think bash's disown is the answer. See help disown.

Use it like this:

yes &>/dev/null </dev/zero &
disown -h $!

Redirect the output or closing terminal would result in SIGHUP signal or a broken pipe for output (SIGPIPE) eventually blocking or killing the program.

If you care about output redirect it to a file. Or use nohup like this:

nohup yes &
disown $!

A good explanation of nohup and disown.

0
0

You don't need echo to run another shell script

#!/bin/bash
#echo $(./test2.sh)
./test2.sh

You must log in to answer this question.

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