albertma789
albertma789

Reputation: 383

Executing bash shell script accepting argument and running in background

I have written a shell script run.sh to trigger a few tasks based on the user's input

#!/bin/bash

echo "Please choose mode [1-3]: "
read MODE

case $MODE in

  1)
    echo -n "Enter iteration: "
    read TIME
    echo "Start Updating ..."
    task 1 && task 2 && task 3  
    ;;

  2)
    echo -n "Enter Seed Value: "
    read SEED
    echo "Start Updating ..."
    task 4
    task 5
    ;;

  3)
    echo -n "Enter regression minimum value: "
    read MIN
    echo "Start Updating ..."
    task 6
    ;;

  *)
    echo -n "Unknown option - Exit"
    ;;
esac

The tasks 1,2 ... 6 are php scripts that are run like /usr/bin/php task1.php $TIME with $TIME as an argument for the php script etc...

The script runs fine when I type bash run.sh but since tasks 1-6 takes a long time to complete I would like an option to run the script in background while I disconnect from the terminal. However if I run the script using bash run.sh & I encountered an error like this:

Please choose mode [1-3]: 2
-bash: 2: command not found

[5]+  Stopped                 bash run.sh

It seems like bash interpreted my input 2 as an argument not corresponding to read MODE but instead of bash run.sh 2 which causes an error. I cannot change the script such that tasks 1-6 are run in the background like task 1 & task 2 & etc. because task 2 can only start running after task 1 is completed.

How can I accomplish what I want to do?

Upvotes: 3

Views: 3170

Answers (2)

Gordon Davisson
Gordon Davisson

Reputation: 126038

Short answer: running interactive programs (/scripts) in the background doesn't really work; it'll work even less well if you disconnect from the terminal. You should rewrite the script so it doesn't need user input as it runs.

Long answer: when you run the script in the background, something like this happens. Note that the exact order of events may vary, as both the background script and foreground interactive shell are running at the same time, "racing" each other to get things done.

  1. You start the script in the background with bash run.sh &
  2. Your interactive shell is immediately ready for your next command, so it prints your usual command prompt to the terminal.
  3. Your script prints its prompt ("Please choose mode [1-3]: ") to the terminal.
  4. Your interactive shell reads its next command from the terminal. Because the script's prompt printed second, it looks like you're sending input to it, but there's actually no connection between the most recent prompt and which program is receiving your input.
  5. Your interactive shell attempts to run your input ("2") as a command, and it fails.
  6. Your shell script finally gets its chance to read from the terminal... but it's in the background, so it's not allowed to. Instead, it is suspended, and a "Stopped" message is printed. From the bash man page, "Job Control" section:

Background processes which attempt to read from (write to when stty tostop is in effect) the terminal are sent a SIGTTIN (SIGTTOU) signal by the kernel's terminal driver, which, unless caught, suspends the process.

At this point, if you want to continue the script and tell it what to do, you'd need to move it to the foreground (e.g. with the fg) command. Which would kind of negate the point here. Also, its prompt ("Please choose mode [1-3]: ") will not be repeated, as that echo command successfully finished while it was in the background.

The solution: basically, write the script to non-interactively run the tasks in the necessary order. @Lenna has given examples; follow her recommendations.

Upvotes: 1

Lenna
Lenna

Reputation: 1290

You could run all of those tasks sequentially in a background subshell

( task 1; task 2; task 3 ) &

Try this out with:

( echo "One"; sleep 1; echo "Two"; sleep 2; echo "Three"; sleep 3; echo "Done" ) &

You could also make this more script-looking:

(
echo "One"
sleep 1
echo "Two"
sleep 2
echo "Three"
sleep 3
echo "Done"
) &

Feel free to make use of the useful envar $BASH_SUBSHELL

( echo $BASH_SUBSHELL; ( echo $BASH_SUBSHELL ) )

Upvotes: 8

Related Questions