我希望我的 bash 脚本休眠到特定时间。所以,我想要一个像“sleep”这样的命令,它不需要间隔,而是一个结束时间,然后一直睡到那时。
“at”-daemon 不是解决方案,因为我需要在某个日期/时间之前阻止正在运行的脚本。
有这样的命令吗?
正如 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 不支持秒以下的精度,您需要使用coreutils
frombrew
代替 → 请参阅这些说明
样本编辑: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 ))
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))
这适用于bash、ksh和其他现代 shell,但您必须使用:
sleep $(( ( $(printf 'tomorrow 21:30\nnow\n' | date -f - +%s-)0 )%86400 ))
在 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
最近的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
$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
使用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
全部做完!
这是一个简单的 Bash 单行:
sleep $(expr $(date -d "03/21/2014 12:30" +%s) - $(date +%s))
这是一个完成工作并通知用户剩余时间的解决方案。我几乎每天都用它在夜间运行脚本(使用 cygwin,因为我无法cron
在 Windows 上工作)
&&
$ 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
}
您可以通过向进程发送 SIGSTOP 信号来停止进程的执行,然后通过向其发送 SIGCONT 信号使其恢复执行。
因此,您可以通过发送 SIGSTOP 来停止脚本:
kill -SIGSTOP <pid>
然后使用 at 守护进程以同样的方式发送一个 SIGCONT 来唤醒它。
大概,您的脚本会在让自己进入睡眠之前通知它何时想要被唤醒。
按照 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
$
我想要一个只检查小时和分钟的脚本,这样我就可以每天使用相同的参数运行脚本。我不想担心明天是哪一天。所以我使用了不同的方法。
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 是脚本的名称)
没有更多的工作迟到了,因为你打错了一天。干杯!
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
实际上,我为此确切目的写了https://tamentis.com/projects/sleepuntil/ 。大多数代码都来自 BSD 'at' 有点过头了,所以它相当符合标准:
$ sleepuntil noon && sendmail something
您可以计算从现在到唤醒时间之间的秒数,并使用现有的“睡眠”命令。
您也许可以使用“at”向您的脚本发送信号,该脚本正在等待该信号。
这是我刚才写的同步多个测试客户端的东西:
#!/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
另一个终端中运行。
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
我组装了一个名为Hypnos的小实用程序来执行此操作。它使用 crontab 语法进行配置,并在此之前一直阻塞。
#!/bin/bash
while [ 1 ]; do
hypnos "0 * * * *"
echo "running some tasks..."
# ...
done
为了扩展主要答案,这里有一些关于日期字符串操作的有效示例:
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
还要考虑这个简洁的 Bash 单行:
sleep $((`date -d "22:00" +%s`-`date +%s`))
这是一个 Bash v4.4.20(1) 算术表达式,由两个日期命令替换组成。第一个 date 命令以秒为单位输出您的目标直到时间(自 1970 年以来),后者输出各自的当前时间。这两个数字用减号隔开,用于算术运算。结果,您将秒作为 sleep 命令的参数。
在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
分钟规范需要两位数。
使用柏油。这是我专门为此编写的一个工具。
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 二进制文件可通过页面上的链接获得。