123

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?

codeforester
  • 39,467
  • 16
  • 112
  • 140
theomega
  • 31,591
  • 21
  • 89
  • 127
  • 3
    Note that solution that simply uses a long sleep preceded by a calculation may sleep too long, maybe much too long especially if you have a machine that can go hibernate. The posix sleep command makes no promises about not sleeping too long. The solution by @Camusensei addresses the concern very nicely. – GregD May 24 '20 at 18:13
  • @GregD Please have a look at my [Jan 2023: New chapter about GregD's comment and hibernation](https://stackoverflow.com/a/19067658/1765658) – F. Hauri - Give Up GitHub Jan 16 '23 at 14:29

22 Answers22

124

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

Community
  • 1
  • 1
SpoonMeiser
  • 19,918
  • 8
  • 50
  • 68
  • 8
    Thanks for that, I wrote a small shell-script to get a "sleepuntil" command. – theomega Mar 14 '09 at 15:20
  • 1
    @enigmaticPhysicist: There is a similar command call "at", that runs async. – voidlogic Aug 19 '14 at 16:36
  • 10
    If you want to stop tomorrow at 3 AM, you can use `target_epoch=$(date -d 'tomorrow 03:00' +%s)` instead. – Yajo Jan 12 '18 at 08:52
  • You could do same by using one fork to `date` instead of two! See 1st part of [my answer](https://stackoverflow.com/a/19067658/1765658) – F. Hauri - Give Up GitHub May 10 '18 at 08:30
  • Just discovered that `time` accepts floats on my Linux ! – Gabriel Devillers Feb 20 '19 at 10:54
  • Avoid **multiple** forks when trying to work around ***nanoseconds***! Use `date -f - +%s.%N` in a [long running background process](https://stackoverflow.com/a/49195703/1765658) (look for ***Quick demo***). – F. Hauri - Give Up GitHub Nov 27 '19 at 10:57
  • 5
    Note that this solution can potentially sleep too long, and if your machine hibernates a bit, it can be much too long. – GregD May 24 '20 at 18:15
  • 1
    Note, that `sleep` has a flaw - it sleeps for actual machine time. In other words, it ignores your computer sleeping (sorry for the pun). If you `sleep` for 2min, while putting the machine into sleep (or hibernate) for 1min, then in "human" time your script will sleep for 3min. To be precise, the question is "Sleep _until_ a specific date/time", not "Sleep _for_ a specific time", so all answers with basic `sleep` are not fully correct – Oleksandr Boiko Nov 29 '22 at 16:48
53

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 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 , 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 , or (one pipe/fork more).

Pure 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 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 under GNU/Linux

Recent , 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 ...

F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
25

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!

Steve Schnepp
  • 4,620
  • 5
  • 39
  • 54
John Feminella
  • 303,634
  • 46
  • 339
  • 357
  • It's worth expanding on how you'd assign the value to a variable, since timeToWait=expr ... won't work directly, and you can't use backticks because they won't nest, so you'll have to use $(), or temporary variables. – SpoonMeiser Mar 14 '09 at 15:05
  • Good suggestion; I'll modify mine to make that clearer so that people don't get the wrong idea. – John Feminella Mar 14 '09 at 15:37
  • Note `-d` is a non-POSIX extension to date. On FreeBSD it tries to set the kernel's value for daylight saving time. – Dave C Feb 29 '20 at 17:15
16

Here is a simple Bash one-liner:

sleep $(expr $(date -d "03/21/2014 12:30" +%s) - $(date +%s))
Anthony O.
  • 22,041
  • 18
  • 107
  • 163
  • This is precisely the solution I was looking for. Very concise. – Aria Feb 01 '21 at 23:42
  • Doesn't work on Ubuntu 20.04.2 LTS. I get an error `sleep: invalid option -- '2'` – Jan Sep 09 '21 at 06:09
  • 3
    @Jan yes it still does, I've just tested. This message is displayed when the date you give as string after `-d` argument is in the past compared to your current system date. In order to show what is the current date of your system, just type in `date` in your console. – Anthony O. Sep 09 '21 at 18:22
12

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
}
Camusensei
  • 1,475
  • 12
  • 20
9

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.

SpoonMeiser
  • 19,918
  • 8
  • 50
  • 68
8

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!

kolomer
  • 81
  • 1
  • 1
  • A very minor detail if precision is important: if just before deadline (<1 minute) it always waits at least 59 seconds. Furthermore this script 59 out of 60 times on average won't run exactly on time. – Jan Sep 09 '21 at 06:14
  • Btw you can easily wrap the code in a function (no code changes needed) and call it inside a shell script like this `till 7 45` – Jan Sep 09 '21 at 09:58
7

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
$
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • I think you want to schedule a SIGCONT, rather than another SIGSTOP, so either the signal, or the comment is wrong. Otherwise, nice to see a proper example. – SpoonMeiser May 05 '09 at 23:09
  • Oops, I typo'd the comment. Now fixed. But you have to watch using numeric signals, since they can be different. – Dennis Williamson May 06 '09 at 05:19
  • I found that dash seems to not understand SIGCONT, so at first I used -18 which is non-portable, then I found that it likes CONT, so I edited the answer above to fix that. – Dennis Williamson May 07 '09 at 04:18
4

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
Ariel
  • 25,995
  • 5
  • 59
  • 69
  • Good solution, however, I think $SECONDS is a string so the defensive code should be something like get a substring first like so `SIGNCHAR=${SECONDS:0:1}` and then `if [ "$SIGNCHAR" = "-" ]`. That should do it. – H2ONaCl Jun 04 '14 at 10:36
2

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
1

You can calculate the number of seconds between now and the wake-up time and use the existing 'sleep' command.

Tim Frey
  • 9,901
  • 9
  • 44
  • 60
1

You could perhaps use 'at' to send a signal to your script, which sat waiting for that signal.

Kim Reece
  • 1,260
  • 9
  • 11
0

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.

Dima Tisnek
  • 11,241
  • 4
  • 68
  • 120
0
 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
Nick Constantine
  • 963
  • 9
  • 19
  • Can you add some sort of description to this solution? – cdomination Jul 19 '16 at 17:53
  • this is a bash script; function sleepuntil takes one argument; you set the variable target_time="11:59" or whatever time you want to sleep until; then you call the function with the target_time argument. It calculates how many seconds are till that target time and sleeps for that amount of seconds – Nick Constantine Jul 21 '16 at 04:36
0

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
0

To extend the main answer, here is some valid examples regarding the date string manipulation:

sleep $(($(date -f - +%s- <<< $'+3 seconds\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'3 seconds\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'3 second\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'+2 minute\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'tomorrow\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'tomorrow 21:30\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'3 weeks\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'3 week\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'next Friday 09:00\nnow')0)) && ls

sleep $(($(date -f - +%s- <<< $'2027-01-01 00:00:01 UTC +5 hours\nnow')0)) && ls
NVRM
  • 11,480
  • 1
  • 88
  • 87
0

Consider also this concise Bash one-liner:

sleep $((`date -d "22:00" +%s`-`date  +%s`))

This is a Bash v4.4.20(1) arithmetic expression, consisting of two date command substitutions. The first date command outputs your target until time in seconds (since 1970) and the latter the respective current time. These two numbers are separated by a minus sign for the arithmetic. As a result, you get seconds as a parameter for the sleep command.

Janne
  • 105
  • 1
  • 5
0

There's a nice little Rust CLI tool yawn that does the job. If you have Rust installed already then this might be an easy option (cargo install yawn). Otherwise, Apple and Linux executables are available on Github.

mcmayer
  • 1,931
  • 12
  • 22
0

This script will wait until 4 of July, 21:44:59

while (( $(date +"%Y%m%d%H%M%S") < 20220704214459 )); do :; done
echo "It is time!"
Liakos
  • 512
  • 5
  • 10
0

Here comes a final countdown solution using standard pv tool that comes with your distro.

  • Use any date compatible expression instead of 5 min.
  • Play with pv parameters to get output as you want.
SECS=$(( $(date -d '5 min' +%s)-$(date +%s) )) && pv -L1 -s$SECS -petlI <(seq $SECS) >/dev/null
brablc
  • 1,621
  • 18
  • 17
-1

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.

cnst
  • 25,870
  • 6
  • 90
  • 122
-1

Use tarry. It's a tool I wrote to specifically do this.

https://github.com/metaphyze/tarry/

This is a simple command line tool for waiting until a specific time. This is not the same as "sleep" which will wait for a duration of time. This is useful if you want to execute something at a specific time or more likely execute several things at exactly the same time such as testing if a server can handle multiple very simultaneous requests. You could use it like this with "&&" on Linux, Mac, or Windows:

   tarry -until=16:03:04 && someOtherCommand

This would wait until 4:03:04 PM and then execute someOtherCommand. Here's a Linux/Mac example of how to run multiple requests all scheduled to start at the same time:

   for request in 1 2 3 4 5 6 7 8 9 10
   do
       tarry -until=16:03:04 && date > results.$request &
   done

Ubuntu, Linux, and Windows binaries are available through links on the page.

metaphyze
  • 1,412
  • 1
  • 15
  • 13