Schlaf bis zu einer bestimmten Zeit / einem bestimmten Datum


89

Ich möchte, dass mein Bash-Skript bis zu einer bestimmten Zeit in den Ruhezustand wechselt. Ich möchte also einen Befehl wie "sleep", der kein Intervall außer einer Endzeit benötigt und bis dahin schläft.

Der "at" -Dämon ist keine Lösung, da ich ein laufendes Skript bis zu einem bestimmten Datum / einer bestimmten Uhrzeit blockieren muss.

Gibt es so einen Befehl?


1
Beachten Sie, dass eine Lösung, die einfach einen langen Ruhezustand verwendet, dem eine Berechnung vorausgeht, möglicherweise zu lange, möglicherweise viel zu lange, insbesondere wenn Sie einen Computer haben, der in den Ruhezustand versetzt werden kann. Der Befehl posix sleep macht keine Zusagen, nicht zu lange zu schlafen. Die Lösung von @Camusensei spricht das Problem sehr gut an.
GregD

Antworten:


97

Wie von Outlaw Programmer erwähnt, besteht die Lösung meiner Meinung nach darin, nur für die richtige Anzahl von Sekunden zu schlafen.

Gehen Sie dazu wie folgt vor:

current_epoch=$(date +%s)
target_epoch=$(date -d '01/01/2010 12:00' +%s)

sleep_seconds=$(( $target_epoch - $current_epoch ))

sleep $sleep_seconds

Verwenden Sie zB diese Syntax, um die Genauigkeit auf Nanosekunden (effektiv mehr um Millisekunden herum) zu erhöhen:

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

Beachten Sie, dass macOS / OS X nicht Präzision unter Sekunden nicht unterstützt, würden Sie brauchen , um coreutilsaus brewstatt → siehe diese Anleitung


8
Vielen Dank dafür, ich habe ein kleines Shell-Skript geschrieben, um einen "sleepuntil" -Befehl zu erhalten.
Theomega

1
@enigmaticPhysicist: Es gibt einen ähnlichen Befehlsaufruf "at", der asynchron ausgeführt wird.
Voidlogic

6
Wenn Sie morgen um 3 Uhr morgens aufhören möchten, können Sie target_epoch=$(date -d 'tomorrow 03:00' +%s)stattdessen verwenden.
Yajo

Sie können dasselbe tun, indem Sie eine Gabel dateanstelle von zwei verwenden! Siehe 1. Teil meiner Antwort
F. Hauri

1
Beachten Sie, dass diese Lösung möglicherweise zu lange schlafen kann. Wenn Ihr Computer etwas in den Ruhezustand wechselt, kann sie viel zu lang sein.
GregD

23

Da diese Frage vor 4 Jahren gestellt wurde, betrifft dieser erste Teil alte Bash-Versionen:

Letzte Änderung: Mi 22. April 2020, etwas zwischen 10:30 und 10h: 55 (Wichtig zum Lesen von Proben)

Allgemeine Methode (Vermeiden Sie nutzlose Gabeln!)

(Nota: Diese Methode verwendet date -fkein POSIX und funktioniert nicht unter MacOS! Wenn unter Mac, gehe zu meinem reinenFunktion )

Um zu reduzieren forks, anstatt datezweimal zu laufen , bevorzuge ich Folgendes:

Einfache Startprobe

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

wo tomorrow 21:30könnte in Zukunft durch jede Art von Datum und Format ersetzt werden, die von anerkannt datewerden.

Mit hoher Präzision (Nanosec)

Fast gleich:

sleep $(bc <<<s$(date -f - +'t=%s.%N;' <<<$'07:00 tomorrow\nnow')'st-t')

Das nächste Mal erreichen

Um heute, wenn möglich, die nächsteHH:MM Bedeutung zu erreichen, morgen, wenn es zu spät ist:

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

Das funktioniert unter , und andere moderne Muscheln, aber Sie müssen verwenden:

sleep $(( ( $(printf 'tomorrow 21:30\nnow\n' | date -f - +%s-)0 )%86400 ))

unter leichteren Muscheln wie oder .

Rein übrigens keine Gabel !!

Unter MacOS getestet!

Ich schrieb eine zwei kleine Funktionen: sleepUntilundsleepUntilHires

 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)

Da neue Versionen von Bash eine printfOption zum Abrufen des Datums bieten , habe ich für diese neue Art des Schlafens bis HH: MM ohne Verwendung dateoder mit einer anderen Gabel ein wenig gebautFunktion. Hier ist es:

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\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 + ${2:-0}*86400
    ))
    $quiet || printf 'sleep %ss, -> %(%c)T\n' $slp $((now+slp))
    sleep $slp
}

Dann:

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

Wenn das Ziel vorher ist, wird dies bis morgen schlafen:

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 Zeit mit unter GNU / Linux

Kürzlich , ab Version 5.0 neue $EPOCHREALTIMEVariable mit Mikrosekunden hinzufügen . Daraus ergibt sich eine sleepUntilHiresFunktion.

sleepUntilHires () { # args [-q] <HH[:MM[:SS]]> [more days]
    local slp tzoff now quiet=false musec musleep;
    [ "$1" = "-q" ] && shift && quiet=true;
    local -a hms=(${1//:/ });
    printf -v now '%(%s)T' -1;
    IFS=. read now musec <<< $EPOCHREALTIME;
    musleep=$[2000000-10#$musec];
    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+10#${hms[2]} -
            tzoff - now - 1
            ) % 86400 ) + ${2:-0} * 86400
          )).${musleep:1};
    $quiet || printf 'sleep %ss, -> %(%c)T\n' $slp $((now+${slp%.*}+1));
    read -t $slp foo
}

Bitte beachten Sie: Diese Verwendung read -tist anstelle von integriert sleep. Leider funktioniert dies nicht, wenn Sie im Hintergrund ohne echte TTY ausgeführt werden. Fühlen Sie sich frei zu ersetzen read -tdurch , sleepwenn Sie planen , diese Scripts im Hintergrund laufen zu lassen ... (Aber für Hintergrundprozess, sollten Sie mit cronund / oder atanstelle von alledem)

Überspringen Sie den nächsten Absatz für Tests und Warnungen $ËPOCHSECONDS!

Aktuelle Kernel vermeiden /proc/timer_listvon Benutzer zu verwenden!

Unter dem aktuellen Linux-Kernel finden Sie eine Variablendatei mit dem Namen "/ proc / timer_list", in der Sie eine "Offset" - und eine "Now" -Variable in ** Nanosekunden ** lesen können. Wir können also die Schlafzeit berechnen, um die * höchste * gewünschte Zeit zu erreichen.

(Ich habe dies geschrieben, um bestimmte Ereignisse in sehr großen Protokolldateien zu generieren und zu verfolgen, die für eine Sekunde tausend Zeilen enthalten.)

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
}

Nach dem Definieren von zwei schreibgeschützten Variablen TIMER_LIST_OFFSETund TIMER_LIST_SKIPgreift die Funktion sehr schnell auf die Variablendatei zu, um die Ruhezeit /proc/timer_listzu berechnen:

Kleine Testfunktion

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
}

Kann so etwas wie rendern:

sleep 0.244040s, -> Wed Apr 22 10:34:39 2020
2020-04-22-10:34:39.001685312
2020-04-22-10:34:39.922291769
sleep 0.077012s, -> Wed Apr 22 10:34:40 2020
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.

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 nextH "%(%T)T" $(((EPOCHSECONDS/2)*2+2))
    sleepUntilHires $nextH
    IFS=. read now musec <<<$EPOCHREALTIME
    printf "%(%c)T.%s\n" $now $musec
done

May produce:

sleep 0.587936s, -> Wed Apr 22 10:51:26 2020
Wed Apr 22 10:51:26 2020.000630
sleep 86399.998797s, -> Thu Apr 23 10:51:26 2020
^C

1
Wow! Computing nanoseconds under bash!
F. Hauri

Computing micro seconds under native bash v5+!!
F. Hauri

21

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!


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

Good suggestion; I'll modify mine to make that clearer so that people don't get the wrong idea.
John Feminella

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

9

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
}

8

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.


8

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`)

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
$

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

Oops, I typo'd the comment. Now fixed. But you have to watch using numeric signals, since they can be different.
Paused until further notice.

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.
Paused until further notice.

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!


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

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

1

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


1

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


Aww man, I was just writing up another answer to this effect.
SpoonMeiser

Well, actually, no, mine was slightly different.
SpoonMeiser


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.


Where is the connection to my question?
theomega

it's easy to extend so that until is set to a specific date/time
Dima Tisnek

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

Can you add some sort of description to this solution?
cdomination

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

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

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


-2

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.