31

I want to build a bash script that executes a command and in the meanwhile performs other stuff, with the possibility of killing the command if the script is killed. Say, executes a cp of a large file and in the meanwhile prints the elapsed time since copy started, but if the script is killed it kills also the copy. I don't want to use rsync, for 2 reasons: 1) is slow and 2) I want to learn how to do it, it could be useful. I tried this:

until cp SOURCE DEST
do
#evaluates time, stuff, commands, file dimensions, not important now
#and echoes something
done

but it doesn't execute the do - done block, as it is waiting that the copy ends. Could you please suggest something?

Santhucool
  • 1,656
  • 2
  • 36
  • 92
marco
  • 569
  • 1
  • 4
  • 19
  • Start a background job with `&` and use [wait](http://stackoverflow.com/questions/356100/how-to-wait-in-bash-for-several-subprocesses-to-finish-and-return-exit-code-0). – ceving Nov 23 '13 at 16:52
  • until is synchronous, not asynchronous. For asynchronous jobs, use the background operator (`&`) – Jo So Nov 23 '13 at 16:52
  • sorry, i don' get "synchronous" and "asynchronous".. – marco Nov 23 '13 at 16:54

4 Answers4

54

until is the opposite of while. It's nothing to do with doing stuff while another command runs. For that you need to run your task in the background with &.

cp SOURCE DEST &
pid=$!

# If this script is killed, kill the `cp'.
trap "kill $pid 2> /dev/null" EXIT

# While copy is running...
while kill -0 $pid 2> /dev/null; do
    # Do stuff
    ...
    sleep 1
done

# Disable the trap on a normal exit.
trap - EXIT

kill -0 checks if a process is running. Note that it doesn't actually signal the process and kill it, as the name might suggest. Not with signal 0, at least.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • 3
    Great answer - didn't know about `kill -0`. Append `2>/dev/null` to `kill -0 $pid` to suppress the error message that is issued once the process no longer exists. – mklement0 Nov 23 '13 at 18:23
  • Great answer! Used here to create an activity indicator for an internet speed test sccript: http://stackoverflow.com/questions/12498304/using-bash-to-display-a-progress-working-indicator/42318974#42318974 – Victoria Stuart Feb 18 '17 at 18:35
15

There are three steps involved in solving your problem:

  1. Execute a command in the background, so it will keep running while your script does something else. You can do this by following the command with &. See the section on Job Control in the Bash Reference Manual for more details.

  2. Keep track of that command's status, so you'll know if it is still running. You can do this with the special variable $!, which is set to the PID (process identifier) of the last command you ran in the background, or empty if no background command was started. Linux creates a directory /proc/$PID for every process that is running and deletes it when the process exits, so you can check for the existence of that directory to find out if the background command is still running. You can learn more than you ever wanted to know about /proc from the Linux Documentation Project's File System Hierarchy page or Advanced Bash-Scripting Guide.

  3. Kill the background command if your script is killed. You can do this with the trap command, which is a bash builtin command.

Putting the pieces together:

# Look for the 4 common signals that indicate this script was killed.
# If the background command was started, kill it, too.
trap '[ -z $! ] || kill $!' SIGHUP SIGINT SIGQUIT SIGTERM
cp $SOURCE $DEST &  # Copy the file in the background.
# The /proc directory exists while the command runs.
while [ -e /proc/$! ]; do
    echo -n "."  # Do something while the background command runs.
    sleep 1  # Optional: slow the loop so we don't use up all the dots.
done

Note that we check the /proc directory to find out if the background command is still running, because kill -0 will generate an error if it's called when the process no longer exists.


Update to explain the use of trap:

The syntax is trap [arg] [sigspec …], where sigspec … is a list of signals to catch, and arg is a command to execute when any of those signals is raised. In this case, the command is a list:

'[ -z $! ] || kill $!'

This is a common bash idiom that takes advantage of the way || is processed. An expression of the form cmd1 || cmd2 will evaluate as successful if either cmd1 OR cmd2 succeeds. But bash is clever: if cmd1 succeeds, bash knows that the complete expression must also succeed, so it doesn't bother to evaluate cmd2. On the other hand, if cmd1 fails, the result of cmd2 determines the overall result of the expression. So an important feature of || is that it will execute cmd2 only if cmd1 fails. That means it's a shortcut for the (invalid) sequence:

if cmd1; then
   # do nothing
else
   cmd2
fi

With that in mind, we can see that

trap '[ -z $! ] || kill $!' SIGHUP SIGINT SIGQUIT SIGTERM

will test whether $! is empty (which means the background task was never executed). If that fails, which means the task was executed, it kills the task.

Adam Liss
  • 47,594
  • 12
  • 108
  • 150
  • 1
    Nice, but the `-e /proc/$!` approach doesn't work on OS X (which may not be a concern, but the question is not Linux-specific). If suppressing the error message from `kill -0` is the only concern, simply append `2>/dev/null`; i.e.: `while kill -0 ${!} 2>/dev/null; do` – mklement0 Nov 23 '13 at 18:27
  • thank you for replying. could you please explain the syntax of the trap command you suggest? i don't get the logical OR.. – marco Nov 23 '13 at 19:37
  • Ah, sorry. That's a common bash idiom, but it's confusing the first time you see it. Updated the answer with an explanation. – Adam Liss Nov 23 '13 at 21:15
  • 1
    If you move the `trap` statement after the one launching the background task, there'll be no need for `[ -z $! ] ||`. Also, since you don't `exit` the script from inside the trap handler, the `while` loop may run another iteration, which is probably undesired. Finally, the `kill` statement may fail even when `$!` is non-empty (as the process may have terminated); again, `2>/dev/null` is our friend. To put it all together, I suggest defining the `trap` handler as follows: `trap "kill ${!} 2>/dev/null; exit 3" SIGHUP SIGINT SIGQUIT SIGTERM`. And, just to be clear: I learned from your post. – mklement0 Nov 23 '13 at 21:40
  • I have loop like this: `while [ -e "/proc/$1" ]; do` - it doesn't' work without the quotation for some reason. – unfa Jul 19 '18 at 15:18
4

here is the simplest way to do that using ps -p :

[command_1_to_execute] &
pid=$!

while ps -p $pid &>/dev/null; do
    [command_2_to_be_executed meanwhile command_1 is running]
    sleep 10 
done

This will run every 10 seconds the command_2 if the command_1 is still running in background .

hope this will help you :)

2

What you want is to do two things at once in shell. The usual way to do that is with a job. You can start a background job by ending the command with an ampersand.

copy $SOURCE $DEST & 

You can then use the jobs command to check its status.

Read more:

C. Ross
  • 31,137
  • 42
  • 147
  • 238