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:
- 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 theread
, and so would never timeout if the FIFO was never written to. while [ true ]
means test if the string "true" is non-empty and continue the while loop if it is non-empty. Howeverwhile true
means run thetrue
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. Considerwhile [ false ]
vswhile false
...$((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.