How can I control a shell script from outside while it is sleeping?

Handling the timing is the script's responsibility. Even if that means using /bin/sleep today, it might not in the future, so killing that isn't actually guaranteed to work long-term. Well, I guess you can make that guarantee, but it's neater not to. My point is you shouldn't kill the sleep directly from outside the script, since the sleep is an implementation detail. Instead, have your script trap a different signal, like SIGUSR1 and send that to your script.

A simple example might look like

#!/usr/bin/env bash

kill_the_sleeper () {
    # This probably isn't really needed here
    # If we don't kill the sleep process, it'll just
    # hang around in the background until it finishes on its own
    # which isn't much of an issue for "sleep" in particular
    # But cleaning up after ourselves is good practice, so let's
    # Just in case we end up doing something more complicated in future
    if [ -v sleep_pid ]; then
        kill "$sleep_pid"
    fi
}

trap kill_the_sleeper USR1 EXIT

while true; do
    # Signals don't interrupt foreground jobs,
    # but they do interrupt "wait"
    # so we "sleep" as a background job
    sleep 5m &
    # We remember its PID so we can clean it up
    sleep_pid="$!"
    # Wait for the sleep to finish or someone to interrupt us
    wait
    # At this point, the sleep process is dead (either it finished, or we killed it)
    unset sleep_pid

    # PART X: code that executes every 5 mins
done

Then you can cause PART X to run by running kill -USR1 "$pid_of_the_script" or pkill -USR1 -f my_fancy_script

This script isn't perfect by any means, but it should be decent for simple cases at least.


In another terminal,
Kill the sleep process:

pkill -f "sleep 5m"

The loop will go on.


If pkill is not available in your OS, you can get the PID using:

$ ps aux | grep '[s]leep'
username 14628  0.0  0.0   8816   672 pts/19   S+   08:33   0:00 sleep 5m

Then run kill PID to kill the process (here: kill 14628).


This is good for manually killing your sleep, but it might not be a good solution for general purposes in scripting, as it will kill all processes with sleep 5m anywhere in the command.


Safer alternative if you can edit the script:

Change your script to write the PID to a file:

TMP_DIR="$HOME/.cache/myscript/"
mkdir -p "$TMP_DIR"
while [ true ]
do
  sleep 5m &
  printf '%s' $! > "$TMP_DIR"/sleep.PID
  wait
  # PART X: code that executes every 5 mins
done

Then you can run:

kill $(< ~/.cache/myscript/sleep.PID)

Please note, that your PID file could be overwritten, e.g. by another instance of the same script and might not work reliably enough for your requirements.


I like @sitaram's method of using ionotifywait. However, a similar thing can be done using more ubiquitous infra - the shell read command (which can take an optional timeout in Bash/ksh/Zsh) and a FIFO.

The wait-loop looks something like this:

mkfifo /tmp/myfifo
while true; do
    read -t $((5*60)) <> /tmp/myfifo
    date
    echo "Executed every 5 minutes"
done

and the read is interrupted early simply by writing a newline to the FIFO:

echo > /tmp/myfifo

A couple of notes:

  1. A non-blocking redirect <> is used instead of a regular <. A regular redirect would block in Bash (until the FIFO is written to), before Bash ever starts the read, and so would never timeout if the FIFO was never written to.
  2. while [ true ] means test if the string "true" is non-empty and continue the while loop if it is non-empty. However while true means run the true command (which as you probably guessed) always returns true, and continue the while loop if it is true. In this case the result is exactly the same. However I think it is clearer without the [ ] test construct. Consider while [ false ] vs while false ...
  3. $((5*60)) is an arithmetic expansion. 300 could have just as easily been used, but this helps demonstrate how arithmetic may easily be performed in command lines.