109

我希望我的 bash 脚本休眠到特定时间。所以,我想要一个像“sleep”这样的命令,它不需要间隔,而是一个结束时间,然后一直睡到那时。

“at”-daemon 不是解决方案,因为我需要在某个日期/时间之前阻止正在运行的脚本。

有这样的命令吗?

4

19 回答 19

115

正如 Outlaw Programmer 所提到的,我认为解决方案只是休眠正确的秒数。

要在 bash 中执行此操作,请执行以下操作:

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

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

sleep $sleep_seconds

要将精度提高到纳秒(实际上更接近毫秒),请使用以下语法:

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

请注意,macOS / OS X 不支持秒以下的精度,您需要使用coreutilsfrombrew代替 → 请参阅这些说明

于 2009-03-14T14:47:17.040 回答
38

样本编辑:2020 年 4 月 22 日星期三,10:30 到 10:55 之间
(对于阅读样本很重要)

通用方法(避免无用的分叉!)

正如 4 年前提出的这个问题,第一部分涉及旧的bash 版本:

(注意:这种方法使用date -f的不是 POSIX,并且在 MacOS 下不起作用!如果在 Mac 下,转到我的函数

为了减少forks,而不是运行date两次,我更喜欢使用这个:

简单的起始样本

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

wheretomorrow 21:30可以替换为date将来可以识别的任何日期和格式。

if read -rp "Sleep until: "  targetTime ;then
    sleep $(( $(date -f - +%s- <<< "$targetTime"$'\nnow') 0 ))
fi

高精度(纳秒)

几乎一样:

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

下次到达

如果可能,今天达到下一个HH:MM含义,明天如果太晚:

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

这适用于和其他现代 shell,但您必须使用:

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

较轻的外壳下。

方式,没有分叉!!

在 MacOS 下测试!

我写了一个两个小函数:sleepUntilsleepUntilHires

 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)

由于新版本的 bash 确实提供了printf检索日期的选项,因此对于这种新的睡眠方式,直到 HH:MM不使用date或任何其他分叉,我已经构建了一个小的函数。这里是:

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
}

然后:

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

如果目标此之前将一直睡到明天:

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在 GNU/Linux 下使用

最近的,从 5.0 版开始添加$EPOCHREALTIME带有微秒的新变量。由此有一个sleepUntilHires功能。

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
}

请注意:这个用法read -t是内置的,而不是sleep. 不幸的是,在后台运行时,如果没有真正的 TTY,这将不起作用。如果您打算在后台脚本中运行它,请随意替换read -t... (但对于后台进程,请考虑使用和/或代替所有这些)sleepcronat

跳过下一段进行测试和警告$ËPOCHSECONDS

较旧的方法使用/proc/timer_list,避免普通用户,由最近的内核!

在 Linux 内核下,您会找到一个名为 `/proc/timer_list` 的变量文件,您可以在其中读取 `offset` 和 `now` 变量,时间为 **纳秒**。因此,我们可以计算睡眠时间以达到*最高*所需的时间。

(我写这个是为了在非常大的日志文件上生成和跟踪特定事件,一秒钟包含千行)。

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
}

定义两个只读变量TIMER_LIST_OFFSETTIMER_LIST_SKIP后,该函数将非常快速地访问变量文件/proc/timer_list以计算睡眠时间:

小测试功能

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
}

可能会呈现如下内容:

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
  • 下一秒开始时,
  • 打印时间,然后
  • 等待 0.92 秒,然后
  • 打印时间,然后
  • 计算还剩 0.07 秒,到下一秒
  • 睡眠 0.07 秒,然后
  • 打印时间。

注意不要混合$EPOCHSECOND$EPOCHREALTIME

阅读我关于之间区别的警告$EPOCHSECOND$EPOCHREALTIME

此功能使用$EPOCHREALTIME所以不要$EPOCHSECOND用于建立下一秒

示例问题:尝试打印下一个四舍五入的时间 2 秒:

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

可能产生:

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
于 2013-09-28T13:46:44.693 回答
23

使用sleep,但使用 计算时间date。你会想用date -d这个。例如,假设您想等到下周:

expr `date -d "next week" +%s` - `date -d "now" +%s`

只需将“下周”替换为您想要等待的任何日期,然后将此表达式分配给一个值,然后休眠那么多秒:

startTime=$(date +%s)
endTime=$(date -d "next week" +%s)
timeToWait=$(($endTime- $startTime))
sleep $timeToWait

全部做完!

于 2009-03-14T14:46:23.390 回答
12

这是一个简单的 Bash 单行:

sleep $(expr $(date -d "03/21/2014 12:30" +%s) - $(date +%s))
于 2014-03-20T11:22:10.793 回答
10

这是一个完成工作并通知用户剩余时间的解决方案。我几乎每天都用它在夜间运行脚本(使用 cygwin,因为我无法cron在 Windows 上工作)

特征

  • 精确到秒
  • 检测系统时间变化并适应
  • 智能输出显示剩余时间
  • 24小时输入格式
  • 返回 true 以便能够与&&

样品运行

$ 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

(最后的日期不是函数的一部分,而是由于&& date

代码

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
}
于 2015-05-18T09:07:34.000 回答
8

您可以通过向进程发送 SIGSTOP 信号来停止进程的执行,然后通过向其发送 SIGCONT 信号使其恢复执行。

因此,您可以通过发送 SIGSTOP 来停止脚本:

kill -SIGSTOP <pid>

然后使用 at 守护进程以同样的方式发送一个 SIGCONT 来唤醒它。

大概,您的脚本会在让自己进入睡眠之前通知它何时想要被唤醒。

于 2009-03-14T15:19:30.160 回答
7

按照 SpoonMeiser 的回答,这里有一个具体的例子:

$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
$
于 2009-04-20T18:31:53.023 回答
7

我想要一个只检查小时和分钟的脚本,这样我就可以每天使用相同的参数运行脚本。我不想担心明天是哪一天。所以我使用了不同的方法。

target="$1.$2"
cur=$(date '+%H.%M')
while test $target != $cur; do
    sleep 59
    cur=$(date '+%H.%M')
done

脚本的参数是小时和分钟,所以我可以写如下:

til 7 45 && mplayer song.ogg

(til 是脚本的名称)

没有更多的工作迟到了,因为你打错了一天。干杯!

于 2010-01-19T03:47:43.833 回答
4

timeToWait = $(( $end - $start ))

请注意“timeToWait”可能是负数!(例如,如果您指定睡眠到“15:57”,现在是“15:58”)。所以你必须检查它以避免奇怪的消息错误:

#!/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
于 2009-05-05T09:55:32.470 回答
2

实际上,我为此确切目的写了https://tamentis.com/projects/sleepuntil/ 。大多数代码都来自 BSD 'at' 有点过头了,所以它相当符合标准:

$ sleepuntil noon && sendmail something
于 2017-10-30T13:11:25.963 回答
1

您可以计算从现在到唤醒时间之间的秒数,并使用现有的“睡眠”命令。

于 2009-03-14T14:39:32.160 回答
1

您也许可以使用“at”向您的脚本发送信号,该脚本正在等待该信号。

于 2009-03-14T15:09:17.440 回答
0

这是我刚才写的同步多个测试客户端的东西:

#!/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)

该脚本会一直休眠到下一个“舍入”或“尖锐”时间。

一个简单的用例是./sleep.py 10; ./test_client1.py在一个终端和./sleep.py 10; ./test_client2.py另一个终端中运行。

于 2013-06-11T12:41:05.297 回答
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
于 2016-07-19T17:29:33.633 回答
0

我组装了一个名为Hypnos的小实用程序来执行此操作。它使用 crontab 语法进行配置,并在此之前一直阻塞。

#!/bin/bash
while [ 1 ]; do
  hypnos "0 * * * *"
  echo "running some tasks..."
  # ...
done
于 2017-02-20T19:31:17.317 回答
0

为了扩展主要答案,这里有一些关于日期字符串操作的有效示例:

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
于 2020-06-12T11:13:18.430 回答
0

还要考虑这个简洁的 Bash 单行:

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

这是一个 Bash v4.4.20(1) 算术表达式,由两个日期命令替换组成。第一个 date 命令以秒为单位输出您的目标直到时间(自 1970 年以来),后者输出各自的当前时间。这两个数字用减号隔开,用于算术运算。结果,您将秒作为 sleep 命令的参数。

于 2021-10-04T14:45:31.110 回答
-1

OpenBSD上,以下内容可用于将*/55 分钟crontab(5)的作业压缩为00每小时的作业(以确保生成更少的电子邮件,同时以精确的时间间隔执行相同的任务):

#!/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

请注意,这date(1)也会破坏sleep(1)最终迭代的设计,因为60分钟不是有效时间(除非它是!),因此我们不必等待任何额外的时间才能收到我们的电子邮件报告。

另请注意,如果分配给它的迭代时间超过 5 分钟,则sleep同样会因设计而失败,因为根本不睡觉(由于将负数解释为命令行选项,而不是环绕到下一个小时甚至永恒),从而确保您的工作仍然可以在分配的小时内完成(例如,如果只有一个迭代需要超过 5 分钟,那么我们仍然有时间赶上,没有任何环绕到下一小时的东西)。

之所以需要,printf(1)是因为date分钟规范需要两位数。

于 2015-09-11T09:33:23.400 回答
-2

使用柏油。这是我专门为此编写的一个工具。

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

这是一个简单的命令行工具,用于等待特定时间。这与将等待一段时间的“睡眠”不同。如果您想在特定时间执行某事或更可能同时执行几件事,例如测试服务器是否可以处理多个非常同时的请求,这将非常有用。您可以在 Linux、Mac 或 Windows 上将它与“&&”一起使用:

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

这将等到下午 4:03:04,然后执行 someOtherCommand。这是一个 Linux/Mac 示例,说明如何运行所有计划同时启动的多个请求:

   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 和 Windows 二进制文件可通过页面上的链接获得。

于 2020-02-29T16:05:55.290 回答