Sample edited: Wed Apr 22 2020, something between 10:30 and 10h:55
(Important for reading samples)
General method (Avoid useless forks!)
As this question was asked 4 years ago, this first part concerns old bash versions:
(Nota: this method use date -f
wich is no POSIX and don't work under MacOS! If under Mac, goto my pure bash function)
In order to reduce forks
, instead of running date
two times, I prefer to use this:
Simple starting sample
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 .
if read -rp "Sleep until: " targetTime ;then
sleep $(( $(date -f - +%s- <<< "$targetTime"$'\nnow') 0 ))
fi
With high precision (nanosec)
Nearly same:
sleep $(bc <<<s$(date -f - +'t=%s.%N;' <<<$'07:00 tomorrow\nnow')'st-t')
Reaching next time
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 %s\nnow\n' 21:30 | date -f - +%s-)0 )%86400 ))
under lighter shells like busybox, ash or dash (one pipe/fork more).
Pure bash way, no fork!!
Tested under MacOS!
I wrote one two little functions: sleepUntil
and sleepUntilHires
Syntax:
sleepUntil [-q] <HH[:MM[:SS]]> [more days]
-q Quiet: don't print sleep computed argument
HH Hours (minimal required argument)
MM Minutes (00 if not set)
SS Seconds (00 if not set)
more days multiplied by 86400 (0 by default)
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() { # args: [-q] <HH[:MM[:SS]]> [more days]
local slp tzoff now quiet=false
[ "$1" = "-q" ] && shift && quiet=true
local -a hms=(${1//:/ })
printf -v now '%(%s)T' -1
printf -v tzoff '%(%z)T' $now
tzoff=$((0${tzoff:0:1}(3600*10#${tzoff:1:2}+60*10#${tzoff: -2})))
slp=$(((( 86400 + ( now - now%86400 ) +
10#$hms*3600+10#${hms[1]:-0}*60+10#${hms[2]:-0} -
tzoff - now
) % 86400 ) + 10#${2:-0} * 86400
))
$quiet || printf 'sleep %ss, -> %(%c)T\n' $slp $((now+slp))
read -t $slp _
}
Then:
sleepUntil 10:37 ; date +"Now, it is: %T"
sleep 49s, -> Wed Apr 22 10:37:00 2020
Now, it is: 10:37:00
sleepUntil -q 10:37:44 ; date +"Now, it is: %T"
Now, it is: 10:37:44
sleepUntil 10:50 1 ; date +"Now, it is: %T"
sleep 86675s, -> Thu Apr 23 10:50:00 2020
^C
If target is before this will sleep until tomorrow:
sleepUntil 10:30 ; date +"Now, it is: %T"
sleep 85417s, -> Thu Apr 23 10:30:00 2020
^C
sleepUntil 10:30 1 ; date +"Now, it is: %T"
sleep 171825s, -> Fri Apr 24 10:30:00 2020
^C
HiRes time with bash under GNU/Linux
Recent bash, from version 5.0 add new $EPOCHREALTIME
variable with microseconds. From this there is a sleepUntilHires
function.
sleepUntilHires() { # args: [-q] <HH[:MM[:SS[.xxx]]]> [more days]
local slp tzoff now quiet=false musec musleep tmu
[ "$1" = "-q" ] && shift && quiet=true
local -a hms
IFS=: read -a hms <<<${1}
printf -v tmu %.06f ${hms[2]:-0}
printf -v hms[2] %.0f ${tmu%.*}
tmu=${tmu#*.}
printf -v now '%(%s)T' -1
IFS=. read now musec <<<$EPOCHREALTIME
musleep=$((2000000+10#$tmu-10#$musec))
printf -v tzoff '%(%z)T\n' $now
tzoff=$((0${tzoff:0:1}(3600*10#${tzoff:1:2}+60*10#${tzoff:3:2})))
slp=$(((( 86400 + ( now - now%86400 ) +
10#$hms*3600+10#0${hms[1]:-0}*60+10#${hms[2]:-0} -
tzoff - now - 1
) % 86400 ) + 10#${2:-0} * 86400
)).${musleep:1}
$quiet ||
printf 'sleep %ss, -> %(%c)T.%s\n' $slp $((now+${slp%.*}+1)) $tmu
read -t $slp _
}
Please note: this use read -t
wich is built-in, instead of sleep
. Unfortunely, this won't work when running in background, without real TTY. Feel free to replace read -t
by sleep
if you
plan to run this in background scripts... (But for background process, consider using cron
and/or at
instead of all this)
HiRes sleep until next period
About RonJohn's comment, if the goal is to start every 15 minutes, sleep
command could become:
sleepUntilNext() { # args: [-q] <float period in seconds>
local slp sec mus quiet=0 res=0 chr
[[ $1 == -q ]] && quiet=1 && shift
printf -v slp %.6f $1
slp=00000$(( 10#${slp/.} - ${EPOCHREALTIME/.} % 10#${slp/.} ))
printf -v slp %.6f ${slp::-6}.${slp: -6}
((quiet)) || printf 'Sleep %s...' $slp >&2
IFS= read -d '' -rsn1 -t $slp chr &&
printf -v res %d \'"$chr"
IFS=. read sec mus <<<$EPOCHREALTIME
((quiet)) || {
IFS=. read sec mus <<<$EPOCHREALTIME
printf '\rDone: %(%a %d %T)T.%s\e[K\n' $sec $mus >&2
}
return $res
}
Then for a period of 15':
sleepUntilNext 900
Sleep 662.334231...
Done: Wed 26 16:15:00.000171
for 20':
sleepUntilNext 1200
Sleep 11.509587...
Done: Wed 26 16:20:00.000168
But this accept float values as period:
while sleepUntilNext -q 1.5;do date +%T.%N;done
16:32:49.502416682
16:32:51.001073971
16:32:52.502804292
16:32:54.002940518
16:32:55.503174242
(Replace read -t
by sleep
or use pseudo FD... see About sleep and/or read -t further)
Skip next paragraph for tests and warning about $ËPOCHSECONDS
!
Older method using /proc/timer_list
, avoided to normal user, by recent Kernel!!
Under 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:
Little test function
tstSleepUntilHires () {
local now next last
printf -v next "%(%H:%M:%S)T" $((${EPOCHREALTIME%.*}+1))
sleepUntilHires $next
date -f - +%F-%T.%N < <(echo now;sleep .92;echo now)
printf -v next "%(%H:%M:%S)T" $((${EPOCHREALTIME%.*}+1))
sleepUntilHires $next
date +%F-%T.%N
}
May render something like:
sleep 0.244040s, -> Wed Apr 22 10:34:39 2020.000000
2020-04-22-10:34:39.001685312
2020-04-22-10:34:39.922291769
sleep 0.077012s, -> Wed Apr 22 10:34:40 2020.000000
2020-04-22-10:34:40.004264869
- 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.
Jan 2023: New chapter about GregD's comment and hibernation
GregD's comment warned about too long sleep if initiated before computer going into deep sleep mode...
For this I've rewrited sleepUntilHires
: Of course, they won't wake computer up, but end immediately (max 20 ms) when waked and show time gap.
sleepUntilHires() { # args: [-q] <HH[:MM[:SS[.xxx]]]> [more days]
local slp tzoff now quiet=false musec musleep tmu start=${EPOCHREALTIME} tgt
[[ $_dummySleepFd ]] && [[ -e /dev/fd/$_dummySleepFd ]] ||
exec {_dummySleepFd}<> <(:)
[ "$1" = "-q" ] && shift && quiet=true
local -a hms; IFS=: read -a hms <<<${1}
printf -v tmu %.06f ${hms[2]:-0}
printf -v hms[2] %.0f ${tmu%.*}
tmu=${tmu#*.}; printf -v now '%(%s)T' -1
IFS=. read now musec <<<$start
musleep=$((2000000+10#$tmu-10#$musec))
printf -v tzoff '%(%z)T\n' $now
tzoff=$((0${tzoff:0:1}(3600*10#${tzoff:1:2}+60*10#${tzoff:3:2})))
slp=$(((( 86400 + ( now - now%86400 ) +
10#$hms*3600+10#0${hms[1]:-0}*60+10#${hms[2]:-0} -
tzoff - now - 1
) % 86400 ) + 10#${2:-0} * 86400
)).${musleep:1}
tgt=$((${start/.}+10#${slp/.}))
printf -v tmu '%(%c)T.%s' $(((${start/.}+10#${slp/.})/1000000)) $tmu
while ((slp=tgt-${EPOCHREALTIME/.},slp>0)) ;do
slp=00000$slp
printf -v slp %.6f ${slp::-6}.${slp: -6}
$quiet || printf '\rsleep %ss, -> %s ...' "$slp" "$tmu"
if (( 10#${slp/.} > 20000));then # If left more than 20 ms
read -u $_dummySleepFd -t .02 _ # then sleep 20 ms
else read -u $_dummySleepFd -t $slp _ # else sleep left time
fi
done
if ! $quiet;then now=00000$((${EPOCHREALTIME/.}-tgt))
printf '\nScript done %.6f seconds late.\n' ${now::-6}.${now: -6}
fi
}
Well I have to start my laptop now...
sleepUntilHires 15:13:18.1234
sleep 0.002148s, -> lun 16 jan 2023 15:13:18 CET.123400 ...
Script done 0.000746 seconds late.
That was a normal run. Now I will put my laptop in hibernation:
sleepUntilHires 15:15:18.1234
sleep 86.256328s, -> lun 16 jan 2023 15:15:18 CET.123400 ...
Script done 111.469551 seconds late.
Then sleep finish immediately when waked up, and show his final gap.
Care to not mix $EPOCHSECOND
and $EPOCHREALTIME
!
Read my warning about difference between $EPOCHSECOND
and $EPOCHREALTIME
This function use $EPOCHREALTIME
so don't use $EPOCHSECOND
for establishing next second:
Sample issue: Trying to print time next rounded by 2 seconds:
for i in 1 2;do
printf -v nextEvenSecond "%(%T)T" $(((EPOCHSECONDS/2)*2+2))
echo $nextEvenSecond
sleepUntilHires $nextEvenSecond
IFS=. read now musec <<<$EPOCHREALTIME
printf "%(%c)T.%s\n" $now $musec
done
May produce:
11:44:32
sleep 1.485212s, -> Wed Nov 9 11:44:32 2022.000000
Wed Nov 9 11:44:32 2022.000311
11:44:32
sleep 86399.999143s, -> Thu Nov 10 11:44:32 2022.000000
You are going to wait 1 day instead of 2 seconds!!!
You have to use $EPOCHREALTIME
:
printf -v nextEvenSecond "%(%T)T" $(((${EPOCHREALTIME%.*}/2)*2+2))
11:48:12
sleep 0.300672s, -> Wed Nov 9 11:48:12 2022.000000
Wed Nov 9 11:48:12 2022.000345
11:48:14
sleep 1.998397s, -> Wed Nov 9 11:48:14 2022.000000
Wed Nov 9 11:48:14 2022.000536
11:48:16
sleep 1.998916s, -> Wed Nov 9 11:48:16 2022.000000
Wed Nov 9 11:48:16 2022.000325
About sleep
and/or read -t
In this functions, I use read -t
(timeout) instead of sleep
, for two reasons:
- this permit user to interrupt sleep by hitting Return key.
(Variant: IFS= read -sn 1 -t $slp _
, to permit user to interrupt by hitting Any key).
sleep
implie a call (fork) to /bin/sleep
as it's not a buitin.
For avoiding user interrupt by keyboard interaction aka for running this non interactively, you could
- create a pseudo tty (file descriptor) for redirecting
read
's input at begin of sleepUntil.source
script:
exec {_dummySleepFd}<> <(:)
then replace
read -t $slp _
by
read -u $_dummySleepFd -t $slp _
- Load
sleep
loadable builtin if your bash implementation permit this (see sudo apt install bash-builtins
):
enable -f sleep sleep
if your $BASH_LOADABLES_PATH
is set, else:
enable -f /usr/lib/bash/sleep sleep
to unload/disable:
enable -d sleep
some tests (on my raspberry):
exec {_dummySleepFd}<> <(:)
s=${EPOCHREALTIME/.};read -u $_dummySleepFd -t .01;echo $((${EPOCHREALTIME/.}-s))
10975
s=${EPOCHREALTIME/.};read -u $_dummySleepFd -t .01;echo $((${EPOCHREALTIME/.}-s))
10977
With /bin/sleep
:
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
60676
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
47910
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
44585
First time, /bin/sleep
was loaded from filesystem. this is longer. But anyway, even in cache, running a fork to sleep
is ressource killer...
enable -f /usr/lib/bash/sleep sleep
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
10803
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
10760
Seem a little better than using read -u $_dummySleepFd -t ...