睡到特定时间/日期
Posted
技术标签:
【中文标题】睡到特定时间/日期【英文标题】:Sleep until a specific time/date 【发布时间】:2010-10-13 08:36:07 【问题描述】:我希望我的 bash 脚本休眠到特定时间。所以,我想要一个像“sleep”这样的命令,它不需要任何时间间隔,而是一个结束时间,然后一直休眠。
“at”-daemon 不是解决方案,因为我需要在某个日期/时间之前阻止正在运行的脚本。
有这样的命令吗?
【问题讨论】:
请注意,仅在计算之前使用长睡眠的解决方案可能会睡眠太久,可能太长,尤其是如果您有一台可以进入休眠状态的机器。 posix sleep 命令没有承诺不要睡太久。 @Camusensei 的解决方案很好地解决了这个问题。 【参考方案1】:示例编辑:2020 年 4 月 22 日星期三,10:30 到 10:55 之间(阅读示例很重要)
通用方法(避免无用的分叉!)
正如 4 年前提出的这个问题,第一部分涉及 旧 bash 版本:
(注意:此方法使用date -f
,它不是 POSIX,在 MacOS 下不起作用!如果在 Mac 下,转到我的 纯bash 函数)
为了减少forks
,而不是运行date
两次,我更喜欢用这个:
简单的起始样本
sleep $(( $(date -f - +%s- <<< "tomorrow 21:30"$'\nnow') 0 ))
tomorrow 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))
这适用于bash、ksh 和其他现代shell,但你必须使用:
sleep $(( ( $(printf 'tomorrow 21:30\nnow\n' | date -f - +%s-)0 )%86400 ))
在ash 或dash 等lighter shell 下。
纯 bash 方式,没有叉子!!
在 MacOS 下测试!
我写了一个两个小函数:sleepUntil
和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)
由于新版本的 bash 确实提供了 printf
选项来检索日期,因此这种新的方式睡眠到 HH:MM 不使用 date
或任何其他叉子,我构建了一个小bash 函数。这里是:
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
在 GNU/Linux 下使用 bash 的 HiRes 时间
最近的 bash,从 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
替换为 sleep
计划在后台脚本中运行它...(但对于后台进程,请考虑使用cron
和/或at
而不是所有这些)
关于$ËPOCHSECONDS
的测试和警告请跳过下一段!
最近的内核使用/proc/timer_list
的旧方法,普通用户避免使用!!
(我写这个是为了在非常大的日志文件上生成和跟踪特定事件,一秒钟内包含千行)。
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_OFFSET
和TIMER_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
!
阅读我的warning about difference between $EPOCHSECOND
and $EPOCHREALTIME
这个函数使用$EPOCHREALTIME
所以不要使用$EPOCHSECOND
来建立下一秒:
示例问题:尝试打印下一个四舍五入的时间:
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
【讨论】:
哇!计算bash下的纳秒! 原生bash v5+下计算微秒!! 迂腐提示:%86400
技巧仅在两次不跨越夏令时转换时才有效。
@200_success 您可以申请时间偏移:在我的答案中搜索tzoff
!【参考方案2】:
这是一个简单的 Bash 单行代码:
sleep $(expr $(date -d "03/21/2014 12:30" +%s) - $(date +%s))
【讨论】:
这正是我正在寻找的解决方案。非常简洁。 不适用于 Ubuntu 20.04.2 LTS。我收到一个错误sleep: invalid option -- '2'
@Jan 是的,它仍然可以,我刚刚测试过。当您在-d
参数之后以字符串形式提供的日期与您当前的系统日期相比是过去的日期时,将显示此消息。为了显示系统的当前日期,只需在控制台中输入date
。【参考方案3】:
考虑一下这个简洁的 Bash 单行:
sleep $((`date -d "22:00" +%s`-`date +%s`))
这是一个 Bash v4.4.20(1) 算术表达式,由两个日期命令替换组成。第一个 date 命令以秒为单位输出您的目标直到时间(自 1970 年以来),后者输出各自的当前时间。这两个数字用减号隔开以进行算术运算。结果,您将秒作为 sleep 命令的参数。
【讨论】:
【参考方案4】:为了扩展主要答案,这里有一些关于日期字符串操作的有效示例:
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
【讨论】:
【参考方案5】:使用 tarry。这是我专门为此编写的一个工具。
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 二进制文件可通过页面上的链接获得。
【讨论】:
【参考方案6】:我实际上为此写了https://tamentis.com/projects/sleepuntil/。大部分代码都来自 BSD 'at' 有点过头了,所以它相当符合标准:
$ sleepuntil noon && sendmail something
【讨论】:
关心differences between c time() and gettimeofday()【参考方案7】:这是一个解决方案,它可以完成这项工作并通知用户剩余的时间。
我几乎每天都用它在夜间运行脚本(使用 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
【讨论】:
【参考方案8】:我组装了一个名为Hypnos 的小实用程序来执行此操作。它使用 crontab 语法进行配置,并在此之前阻塞。
#!/bin/bash
while [ 1 ]; do
hypnos "0 * * * *"
echo "running some tasks..."
# ...
done
【讨论】:
【参考方案9】:正如 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 不支持秒以下的精度,您需要改用 brew
中的 coreutils
→ see these instructions
【讨论】:
谢谢你,我写了一个小shell脚本来获得一个“sleepuntil”命令。 @enigmaticPhysicist:有一个类似的命令调用“at”,它运行异步。 如果你想在明天凌晨 3 点停下来,你可以改用target_epoch=$(date -d 'tomorrow 03:00' +%s)
。
你可以用一个叉子来date
而不是两个!见my answer的第一部分
请注意,此解决方案可能会休眠太久,如果您的机器休眠了一点,它可能会太长。【参考方案10】:
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
【讨论】:
你能在这个解决方案中添加一些描述吗? 这是一个 bash 脚本;函数 sleepuntil 接受一个参数;你设置变量 target_time="11:59" 或者你想睡到的任何时间;然后您使用 target_time 参数调用该函数。它计算距离目标时间还有多少秒,并在该秒数内休眠【参考方案11】:在 OpenBSD 上,以下内容可用于将 */5
5 分钟 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
期望分钟规格正好是两位数。
【讨论】:
【参考方案12】: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
【讨论】:
很好的解决方案,但是,我认为 $SECONDS 是一个字符串,所以防御代码应该是先获取一个子字符串,例如SIGNCHAR=$SECONDS:0:1
,然后是if [ "$SIGNCHAR" = "-" ]
。应该这样做。【参考方案13】:
这是我刚才写的同步多个测试客户端的东西:
#!/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
。
【讨论】:
我的问题在哪里? 很容易扩展,以便将until
设置为特定的日期/时间【参考方案14】:
使用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
全部完成!
【讨论】:
值得扩展如何将值分配给变量,因为 timeToWait=expr ... 不能直接工作,并且您不能使用反引号,因为它们不会嵌套,所以你必须使用 $() 或临时变量。 好建议;我会修改我的以使其更清晰,以免人们误解。 注意-d
是迄今为止的非 POSIX 扩展。在 FreeBSD 上,它会尝试设置内核的夏令时值。【参考方案15】:
我想要一个只检查小时和分钟的脚本,这样我就可以每天使用相同的参数运行脚本。我不想担心明天是哪一天。所以我使用了不同的方法。
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 是脚本的名称)
再也不会因为你打错一天而迟到了。干杯!
【讨论】:
一个非常小的细节,如果精度很重要:如果就在截止日期之前( 顺便说一句,您可以轻松地将代码包装在一个函数中(无需更改代码)并在这样的 shell 脚本中调用它till 7 45
【参考方案16】:
按照 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
$
【讨论】:
我想你想安排一个 SIGCONT,而不是另一个 SIGSTOP,所以要么是信号,要么是注释是错误的。否则,很高兴看到一个合适的例子。 糟糕,我把评论打错了。现在修好了。但是您必须使用数字信号进行观察,因为它们可能不同。 我发现dash好像看不懂SIGCONT,所以一开始我用的是不可移植的-18,后来发现它喜欢CONT,所以我编辑了上面的答案来解决这个问题。 【参考方案17】:你可以通过发送一个 SIGSTOP 信号来停止一个进程的执行,然后通过发送一个 SIGCONT 信号让它恢复执行。
所以你可以通过发送一个 SIGSTOP 来停止你的脚本:
kill -SIGSTOP <pid>
然后使用 at 守护进程以同样的方式发送一个 SIGCONT 来唤醒它。
大概,您的脚本会在让自己进入睡眠之前通知它想要被唤醒的时间。
【讨论】:
【参考方案18】:您也许可以使用 'at' 向您的脚本发送信号,该脚本正在等待该信号。
【讨论】:
哇,伙计,我只是在写另一个答案。 嗯,实际上,不,我的略有不同。【参考方案19】:您可以计算从现在到唤醒时间之间的秒数,并使用现有的“睡眠”命令。
【讨论】:
以上是关于睡到特定时间/日期的主要内容,如果未能解决你的问题,请参考以下文章