可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I want my bash script to sleep until a specific time. So, I want a command like "sleep" which takes no interval but an end time and sleeps until then.
The "at"-daemon is not a solution, as I need to block a running script until a certain date/time.
Is there such a command?
回答1:
As mentioned by Outlaw Programmer, I think the solution is just to sleep for the correct number of seconds.
To do this in bash, do the following:
current_epoch=$(date +%s)
target_epoch=$(date -d '01/01/2010 12:00' +%s)
sleep_seconds=$(( $target_epoch - $current_epoch ))
sleep $sleep_seconds
To add precision down to nanoseconds (effectively more around milliseconds) use e.g. this syntax:
current_epoch=$(date +%s.%N)
target_epoch=$(date -d "20:25:00.12345" +%s.%N)
sleep_seconds=$(echo "$target_epoch - $current_epoch"|bc)
sleep $sleep_seconds
Note that macOS / OS X does not support precision below seconds, you would need to use coreutils
from brew
instead → see these instructions
回答2:
Use sleep
, but compute the time using date
. You'll want to use date -d
for this. For example, let's say you wanted to wait until next week:
expr `date -d "next week" +%s` - `date -d "now" +%s`
Just substitute "next week" with whatever date you'd like to wait for, then assign this expression to a value, and sleep for that many seconds:
startTime=$(date +%s)
endTime=$(date -d "next week" +%s)
timeToWait=$(($endTime- $startTime))
sleep $timeToWait
All done!
回答3:
As this question was asked 4 years ago, this first part concerns old bash versions:
General method
In order to reduce forks
, instead of running date
two times, I prefer to use this:
sleep $(($(date -f - +%s- <<< $'tomorrow 21:30\nnow')0))
where tomorrow 21:30
could be replaced by any kind of date and format recognized by date
, in the future.
or better, for reaching next HH:MM
meaning today if possible, tomorrow if too late:
sleep $((($(date -f - +%s- <<<$'21:30 tomorrow\nnow')0)%86400))
This works under bash, ksh and other modern shells, but you have to use:
sleep $(( ( $(printf 'tomorrow 21:30\nnow\n' | date -f - +%s-)0 )%86400 ))
under lighter shells like ash or dash.
New bash way (no fork)
As my need for this kind of tool has never exceded 24 hours, the following will only concern HH
, HH:MM
or HH:MM:SS
syntax meaning in next 24 hours. (Anyway if you need more, you could even return to old method using a fork to date
. Trying to eliminate one fork in a script running over many days is overkill.)
As new versions of bash do offer a printf
option to retrieve date, for this new way to sleep until HH:MM whithout using date
or any other fork, I've build a little bash function. Here it is:
sleepUntil() {
local slp tzoff now quiet=false
[ "$1" = "-q" ] && shift && quiet=true
local hms=(${1//:/ })
printf -v now '%(%s)T' -1
printf -v tzoff '%(%z)T\n' $now
tzoff=$((0${tzoff:0:1}(3600*${tzoff:1:2}+60*${tzoff:3:2})))
slp=$(((86400+(now-now%86400)+10#$hms*3600+10#${hms[1]}*60+${hms[2]}-tzoff-now)%86400))
$quiet || printf 'sleep %ss, -> %(%c)T\n' $slp $((now+slp))
sleep $slp
}
Then:
sleepUntil 11:11 ; date +"Now, it is: %T"
sleep 3s, -> sam 28 sep 2013 11:11:00 CEST
Now, it is: 11:11:00
sleepUntil -q 11:11:5 ; date +"Now, it is: %T"
Now, it is: 11:11:05
HiRes time with bash under GNU/Linux
Under recent Linux kernel, you will find a variables file named /proc/timer_list
where you could read an offset
and a now
variable, in nanoseconds. So we may compute sleep time to reach the very top desired time.
(I wrote this to generate and track specific events on very big log files, containing thousand line for one second).
mapfile </proc/timer_list _timer_list
for ((_i=0;_i<${#_timer_list[@]};_i++));do
[[ ${_timer_list[_i]} =~ ^now ]] && TIMER_LIST_SKIP=$_i
[[ ${_timer_list[_i]} =~ offset:.*[1-9] ]] && \
TIMER_LIST_OFFSET=${_timer_list[_i]//[a-z.: ]} && \
break
done
unset _i _timer_list
readonly TIMER_LIST_OFFSET TIMER_LIST_SKIP
sleepUntilHires() {
local slp tzoff now quiet=false nsnow nsslp
[ "$1" = "-q" ] && shift && quiet=true
local hms=(${1//:/ })
mapfile -n 1 -s $TIMER_LIST_SKIP nsnow </proc/timer_list
printf -v now '%(%s)T' -1
printf -v tzoff '%(%z)T\n' $now
nsnow=$((${nsnow//[a-z ]}+TIMER_LIST_OFFSET))
nsslp=$((2000000000-10#${nsnow:${#nsnow}-9}))
tzoff=$((0${tzoff:0:1}(3600*${tzoff:1:2}+60*${tzoff:3:2})))
slp=$(( ( 86400 + ( now - now%86400 ) +
10#$hms*3600+10#${hms[1]}*60+${hms[2]} -
tzoff - now - 1
) % 86400)).${nsslp:1}
$quiet || printf 'sleep %ss, -> %(%c)T\n' $slp $((now+${slp%.*}+1))
sleep $slp
}
After defining two read-only variables, TIMER_LIST_OFFSET
and TIMER_LIST_SKIP
, the function will access very quickly the variable file /proc/timer_list
for computing sleep time:
sleepUntilHires 15:03 ;date +%F-%T.%N ;sleep .97;date +%F-%T.%N
sleep 19.632345552s, -> sam 28 sep 2013 15:03:00 CEST
2013-09-28-15:03:00.003471143
2013-09-28-15:03:00.976100517
sleepUntilHires -q 15:04;date -f - +%F-%T.%N < <(echo now;sleep .97;echo now)
2013-09-28-15:04:00.003608002
2013-09-28-15:04:00.974066555
And finally
Little test function
tstSleepUntilHires () {
local now next last
printf -v now "%(%s)T"
printf -v next "%(%H:%M:%S)T" $((now+1))
printf -v last "%(%H:%M:%S)T" $((now+2))
sleepUntilHires $next
date -f - +%F-%T.%N < <(echo now;sleep .92;echo now)
sleepUntilHires $last
date +%F-%T.%N
}
May render something like:
sleep 0.155579469s, -> Mon Aug 20 20:42:51 2018
2018-08-20-20:42:51.005743047
2018-08-20-20:42:51.927112981
sleep 0.071764300s, -> Mon Aug 20 20:42:52 2018
2018-08-20-20:42:52.003165816
- At begin of next second,
- print time, then
- wait 0.92 seccond, then
- print time, then
- compute 0.07 seconds left, to next second
- sleep 0.07 seconds, then
- print time.
Nota: .92 + 0.071 = .991
(on my desktop)
回答4:
You can stop a process from executing, by sending it a SIGSTOP signal, and then get it to resume executing by sending it a SIGCONT signal.
So you could stop your script by sending is a SIGSTOP:
kill -SIGSTOP <pid>
And then use the at deamon to wake it up by sending it a SIGCONT in the same way.
Presumably, your script will inform at of when it wanted to be woken up before putting itself to sleep.
回答5:
To follow on SpoonMeiser's answer, here's a specific example:
$cat ./reviveself
#!/bin/bash
# save my process ID
rspid=$$
# schedule my own resuscitation
# /bin/sh seems to dislike the SIGCONT form, so I use CONT
# at can accept specific dates and times as well as relative ones
# you can even do something like "at thursday" which would occur on a
# multiple of 24 hours rather than the beginning of the day
echo "kill -CONT $rspid"|at now + 2 minutes
# knock myself unconscious
# bash is happy with symbolic signals
kill -SIGSTOP $rspid
# do something to prove I'm alive
date>>reviveself.out
$
回答6:
I wanted an script that only checked the hours and minutes so I could run the script with the same parameters every day. I don't want to worry about which day will be tomorrow. So I used a different approach.
target="$1.$2"
cur=$(date '+%H.%M')
while test $target != $cur; do
sleep 59
cur=$(date '+%H.%M')
done
the parameters to the script are the hours and minutes, so I can write something like:
til 7 45 && mplayer song.ogg
(til is the name of the script)
no more days late at work cause you mistyped the day. cheers!
回答7:
On Ubuntu 12.04.4 LTS here is the simple bash input which works :
sleep $(expr `date -d "03/21/2014 12:30" +%s` - `date +%s`)
回答8:
Here is a solution that does the job AND informs the user about how much time is remaining.
I use it almost everyday to run scripts during the night (using cygwin, as I couldn't get cron
to work on windows)
Features
- Precise down to the second
- Detects system time changes and adapts
- Intelligent output telling how much time is left
- 24-hour input format
- returns true to be able to chain with
&&
Sample run
$ til 13:00 && date
1 hour and 18 minutes and 26 seconds left...
1 hour and 18 minutes left...
1 hour and 17 minutes left...
1 hour and 16 minutes left...
1 hour and 15 minutes left...
1 hour and 14 minutes left...
1 hour and 10 minutes left...
1 hour and 5 minutes left...
1 hour and 0 minutes left...
55 minutes left...
50 minutes left...
45 minutes left...
40 minutes left...
35 minutes left...
30 minutes left...
25 minutes left...
20 minutes left...
15 minutes left...
10 minutes left...
5 minutes left...
4 minutes left...
3 minutes left...
2 minutes left...
1 minute left...
Mon, May 18, 2015 1:00:00 PM
(The date at the end is not part of the function, but due to the && date
)
Code
til(){
local hour mins target now left initial sleft correction m sec h hm hs ms ss showSeconds toSleep
showSeconds=true
[[ $1 =~ ([0-9][0-9]):([0-9][0-9]) ]] || { echo >&2 "USAGE: til HH:MM"; return 1; }
hour=${BASH_REMATCH[1]} mins=${BASH_REMATCH[2]}
target=$(date +%s -d "$hour:$mins") || return 1
now=$(date +%s)
(( target > now )) || target=$(date +%s -d "tomorrow $hour:$mins")
left=$((target - now))
initial=$left
while (( left > 0 )); do
if (( initial - left < 300 )) || (( left < 300 )) || [[ ${left: -2} == 00 ]]; then
# We enter this condition:
# - once every 5 minutes
# - every minute for 5 minutes after the start
# - every minute for 5 minutes before the end
# Here, we will print how much time is left, and re-synchronize the clock
hs= ms= ss=
m=$((left/60)) sec=$((left%60)) # minutes and seconds left
h=$((m/60)) hm=$((m%60)) # hours and minutes left
# Re-synchronise
now=$(date +%s) sleft=$((target - now)) # recalculate time left, multiple 60s sleeps and date calls have some overhead.
correction=$((sleft-left))
if (( ${correction#-} > 59 )); then
echo "System time change detected..."
(( sleft <= 0 )) && return # terminating as the desired time passed already
til "$1" && return # resuming the timer anew with the new time
fi
# plural calculations
(( sec > 1 )) && ss=s
(( hm != 1 )) && ms=s
(( h > 1 )) && hs=s
(( h > 0 )) && printf %s "$h hour$hs and "
(( h > 0 || hm > 0 )) && printf '%2d %s' "$hm" "minute$ms"
if [[ $showSeconds ]]; then
showSeconds=
(( h > 0 || hm > 0 )) && (( sec > 0 )) && printf %s " and "
(( sec > 0 )) && printf %s "$sec second$ss"
echo " left..."
(( sec > 0 )) && sleep "$sec" && left=$((left-sec)) && continue
else
echo " left..."
fi
fi
left=$((left-60))
sleep "$((60+correction))"
correction=0
done
}
回答9:
timeToWait = $(( $end - $start ))
Beware that "timeToWait" could be a negative number! (for example, if you specify to sleep until "15:57" and now it's "15:58"). So you have to check it to avoid strange message errors:
#!/bin/bash
set -o nounset
### // Sleep until some date/time.
# // Example: sleepuntil 15:57; kdialog --msgbox "Backup needs to be done."
error() {
echo "$@" >&2
exit 1;
}
NAME_PROGRAM=$(basename "$0")
if [[ $# != 1 ]]; then
error "ERROR: program \"$NAME_PROGRAM\" needs 1 parameter and it has received: $#."
fi
current=$(date +%s.%N)
target=$(date -d "$1" +%s.%N)
seconds=$(echo "scale=9; $target - $current" | bc)
signchar=${seconds:0:1}
if [ "$signchar" = "-" ]; then
error "You need to specify in a different way the moment in which this program has to finish, probably indicating the day and the hour like in this example: $NAME_PROGRAM \"2009/12/30 10:57\"."
fi
sleep "$seconds"
# // End of file
回答10:
You can calculate the number of seconds between now and the wake-up time and use the existing 'sleep' command.
回答11:
You could perhaps use 'at' to send a signal to your script, which sat waiting for that signal.
回答12:
I put together a small utility called Hypnos to do this. It's configured using the crontab syntax and blocks until that time.
#!/bin/bash
while [ 1 ]; do
hypnos "0 * * * *"
echo "running some tasks..."
# ...
done
回答13:
Here's something I wrote just now to synchronise multiple test clients:
#!/usr/bin/python
import time
import sys
now = time.time()
mod = float(sys.argv[1])
until = now - now % mod + mod
print "sleeping until", until
while True:
delta = until - time.time()
if delta <= 0:
print "done sleeping ", time.time()
break
time.sleep(delta / 2)
This script sleeps until next "rounded" or "sharp" time.
A simple use case is to run ./sleep.py 10; ./test_client1.py
in one terminal and ./sleep.py 10; ./test_client2.py
in another.
回答14:
On OpenBSD, the following could be used to compact a */5
5-minute crontab(5)
job into an 00
hourly one (to make sure fewer emails are generated, all whilst performing the same task at exact intervals):
#!/bin/sh -x
for k in $(jot 12 00 55)
do
echo $(date) doing stuff
sleep $(expr $(date -j +%s $(printf %02d $(expr $k + 5))) - $(date -j +%s))
done
Note that the date(1)
would also break the sleep(1)
by design on the final iteration, as 60
minutes is not a valid time (unless it is!), thus we won't have to wait any extra time prior to getting our email report.
Also note that should one of the iterations take more than 5 minutes allotted to it, the sleep
would likewise graciously fail by design by not sleeping at all (due to what is a negative number interpreted as a command-line option, instead of wrapping around to the next hour or even eternity), thus making sure your job could still complete within the hour allotted (e.g., if only one of the iterations takes a little bit more than 5 minutes, then we would still have the time to catch up, without anything wrapping around to the next hour).
The printf(1)
is needed because date
expects exactly two digits for the minute specification.
回答15:
function sleepuntil() {
local target_time="$1"
today=$(date +"%m/%d/%Y")
current_epoch=$(date +%s)
target_epoch=$(date -d "$today $target_time" +%s)
sleep_seconds=$(( $target_epoch - $current_epoch ))
sleep $sleep_seconds
}
target_time="11:59"; sleepuntil $target_time
回答16:
I actually wrote https://tamentis.com/projects/sleepuntil/ for this exact purpose. It's a bit over-kill most of the code comes from BSD 'at' so it's fairly standard-compliant:
$ sleepuntil noon && sendmail something