Bash, Linux: Interacting with multiple scripts running in background from another script

In this post, I would like to share an idea of a solution of an interesting task concerning manipulating multiple Bash scripts running in background via prompts from another Bash script. I will show you an example of a script implementing such solution. The example is simplified, but it fully illustrates the idea.

A script with prompt

First of all, we will prepare a script constantly prompting for a user input, outputting it and exiting the script execution in case the user enters “q”:

#!/bin/bash
  
stop=0

while [ ${stop} -eq 0 ]; do
  read -p "Enter input: " i
  echo "Your input: ${i}"
  if [[ ${i} = "q" ]]; then
    echo "Exiting script"
    stop=1
  fi
done

The script implementation uses a variable controlling the while-loop. In case user enters “q”, the loop-controlling variable value changes, the loop stops, and the script exits.

Manual test – one script with prompt

Execute the script. Enter “k” and then “q”:

./prompt1.sh 

Output:
Enter input: k
Your input: k
Enter input: q
Your input: q
Exiting script

Manual test with Here document input – one script with prompt

Now, we will automate the described above actions. We will use a Here document (Here document documentation). As above, we will enter “k” and then “q”:

./prompt1.sh <<END
> k
> q
> END

Output:
Your input: k
Your input: q
Exiting script

The results are as expected. You may notice that the results are printed out almost instantly. In order to have a better view at the script execution, we will slow it down a bit. We will do it by adding a 5 seconds sleep between the prompt line and the input printing out line:

read -p "Enter input: " i
sleep 5
echo "Your input: ${i}"

Re-execute the script with the same input in the Here document format. It is easier to watch the script execution now.

Automated test – one script with prompt

Next, we will automate our test by creating main.sh Bash script with the following code:

#!/bin/bash
  
./prompt1.sh <<END
k
q
END

Make the main.sh executable and execute it. As you can see, the result is the same as of the manual test:

chmod +x main.sh
./main.sh

Output:
Your input: k
Your input: q
Exiting script

Requirements – two scripts with prompts executed from another script

Let us increase the complexity of the task. Now we need to launch the script, enter the first input, execute another script, and enter the second input to the first script.

In order to meet the new requirements, we need to modify the script. The solution implementation can be the following:

  • launch the prompt1.sh script
  • enter “k” into prompt1.sh script prompt
  • send prompt1.sh script to the background
  • execute prompt2.sh script
  • get prompt1.sh script back to the foreground
  • enter “q” into prompt1.sh script prompt

Given these new requirements and implementation logic, Here document does not satisfy out needs anymore. If you search for a solution in web, you may come across the recommendations to use expect. Whereas expect will certainly allow us to build a solution we need, below I will show a mush easier approach.

Solution – two scripts with prompts executed from another script

The solution idea is the following. We will use an intermediary file to communicate between the main script and the prompt1.sh script. The implementation is easy: the output of the intermediary file is constantly read by the prompt1.sh and the main script writes to the intermediary file. This approach allows us to grant full control over the prompt1.sh script execution to the main script. As an additional benefit, the approach also allows us simplify the solution implementation logic. Now, we can omit the steps of sending the prompt1.sh script to the background and getting it back to the foreground. Nice.

The solution modified implementation logic:

  • launch the prompt1.sh script in the background
  • enter “k” into prompt1.sh script prompt
  • send prompt1.sh script to the background
  • execute prompt2.sh script
  • get prompt1.sh script back to the foreground
  • enter “q” into prompt1.sh script prompt

Implementation – two scripts with prompts executed from another script

Let us proceed to coding. First of all, we will create a prompt2.sh script. In order to make our test more interesting, the prompt2.sh script will be a duplicate of the prompt1.sh script. In this way, we will interact with two scripts out of the main.sh. We will make several modifications to the prompt scripts output messages, so that we can see which script writes them.

prompt1.sh script:

#!/bin/bash
  
stop=0

while [ ${stop} -eq 0 ]; do
  read -p "Enter prompt1 input: " i
  sleep 5
  echo "Your prompt1 input: ${i}"
  if [[ ${i} = "q" ]]; then
    echo "Exiting prompt1 script"
    stop=1
  fi
done

prompt2.sh script:

#!/bin/bash
  
stop=0

while [ ${stop} -eq 0 ]; do
  read -p "Enter prompt2 input: " i
  sleep 5
  echo "Your prompt2 input: ${i}"
  if [[ ${i} = "q" ]]; then
    echo "Exiting prompt2 script"
    stop=1
  fi
done

It is interesting to note that in order to execute the described above test manually, a tester has to open and work with two terminal windows, one for prompt1.sh script and one for prompt2.sh script. This fact helps emphasize the value of automated test scripts, like the one we are discussing here in this post.

In the main.sh script, we will add creation of a log file where we will keep track of the execution:

log=prompts.log
touch ${log}

Next, we will add creation of prompt1.sh input file:

p1_input=prompt1.input
touch ${p1_input}

Next comes the main line of the main.sh script:

tail -f ${p1_input} | ./prompt1.sh >> ${log} &

Using the tail command, we constantly output (line by line) the content of the prompt1.input file. This output is piped to the prompt1.sh script. The prompt1.sh script is executed in the background (&) and the prompt1.sh output is redirected to the log file (prompts.log).

Next, we add the first line to the prompt1.input file:

echo "k" >> ${p1_input}

As we will see later in the log file, “k” is delivered to the prompt1.sh script which is reflected by this line in the log file:

Your prompt1 input: k

The interaction with the prompt1.sh is paused now. We focus our attention at the prompt2.sh now. The prompt2.sh script execution is the same as before, we provide the inputs in the Here document format. The only change we make is the output redirect to the log file:

./prompt2.sh >> ${log} <<END
k
q
END

The prompt2.sh script execution is reflected by these lines in the log file:

Your prompt2 input: k
Your prompt2 input: q
Exiting prompt2 script

prompt2.sh script execution is finished. Now, we come back to the prompt1.sh script. Like before, we add a line to the prompt1.input file:

echo "q" >> ${p1_input}

“q” is delivered to the prompt1.sh script. “q” triggers the modification of the while-loop controlling variable, which stops the loop and the script execution. In the log file, you can see two new lines:

Your prompt1 input: q
Exiting prompt1 script

The main.sh script complete listing. As you can see below, there is one additional line in the listing: rm -f ${log} ${p1_input}. This line is added to delete the log and prompt1.input files from the previous execution of the main.sh script:

#!/bin/bash
  
log=prompts.log
p1_input=prompt1.input
rm -f ${log} ${p1_input}
touch ${log} ${p1_input}

tail -f ${p1_input} | ./prompt1.sh >> ${log} &

echo "k" >> ${p1_input}

./prompt2.sh >> ${log} <<END
k
q
END

echo "q" >> ${p1_input}

Execute the main.sh script and wait for the end of the execution. Then, check the log file:

./main.sh
cat ./prompts.log

Output:
Your prompt1 input: k
Your prompt2 input: k
Your prompt2 input: q
Exiting prompt2 script
Your prompt1 input: q
Exiting prompt1 script

The obtained results are as expected. The final thing we have to do is to kill the tail -f prompt1.input process last hanging. Add the following lines to the end of the main.sh:

inp_ps=$( ps -edf | grep "tail -f prompt1.input" | grep -v grep | awk '{ print $2 }' )
if [[ ${#inp_ps} -gt 0 && $( echo ${inp_ps} ) -gt 0 ]]; then
  echo "Killing the process: ${inp_ps}" >> ${log}
  kill -9 ${inp_ps}
fi

He we get the id of the “tail -f prompt1.input” process. If such process id exists (the actual check is if the length of the variable holding id value is greater than zero and the value is greater than zero), the process is killed. We note this action in the log.

Executing main.sh script now produces the following log:

Your prompt2 input: k
Your prompt1 input: k
Your prompt2 input: q
Exiting prompt2 script
Your prompt1 input: q
Exiting prompt1 script
Killing the process: 10783

main.sh script final listing is the following:

#!/bin/bash
  
log=prompts.log
p1_input=prompt1.input
rm -f ${log} ${p1_input}
touch ${log} ${p1_input}

tail -f ${p1_input} | ./prompt1.sh >> ${log} &

echo "k" >> ${p1_input}

./prompt2.sh >> ${log} <<END
k
q
END

echo "q" >> ${p1_input}

sleep 5

inp_ps=$( ps -edf | grep "tail -f prompt1.input" | grep -v grep | awk '{ print $2 }' )
if [[ ${#inp_ps} -gt 0 && $( echo ${inp_ps} ) -gt 0 ]]; then
  echo "Killing the process: ${inp_ps}" >> ${log}
  kill -9 ${inp_ps}
fi

In case you have a question or a comment concerning this post, please send them to: grebnov@gmail.com