我希望我的bash脚本能够睡眠,直到特定的时间。因此,我想要一个像 "sleep" 命令一样的命令,它不需要间隔时间,而是需要一个结束时间,并一直睡眠到该时间。
"at" 守护进程不是解决方案,因为我需要阻止正在运行的脚本直到某个特定日期/时间。
是否有这样的命令?
我希望我的bash脚本能够睡眠,直到特定的时间。因此,我想要一个像 "sleep" 命令一样的命令,它不需要间隔时间,而是需要一个结束时间,并一直睡眠到该时间。
"at" 守护进程不是解决方案,因为我需要阻止正在运行的脚本直到某个特定日期/时间。
是否有这样的命令?
正如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
。如果需要,请查看这些说明。
target_epoch=$(date -d 'tomorrow 03:00' +%s)
。 - Yajosleep
存在一个缺陷 - 它会按照实际机器时间进行休眠。换句话说,它会忽略计算机的睡眠状态(抱歉,这是个双关语)。如果您使用 sleep
休眠 2 分钟,同时将机器置于睡眠(或休眠)状态 1 分钟,则从“人类”时间来看,您的脚本将休眠 3 分钟。准确地说,问题是“休眠直到特定日期/时间”,而不是“休眠一段特定的时间”,因此所有基本 sleep
的答案都不完全正确。 - Oleksandr Boiko示例已编辑: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 %s\nnow\n' 21:30 | date -f - +%s-)0 )%86400 ))
在像busybox, ash或dash这样的轻型外壳下(一个管道/分叉更多)。
在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' $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 _
}
然后:
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[.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 _
}
read -t
而非sleep
。不幸的是,如果在后台运行且没有真正的TTY,则无法工作。如果您计划在后台脚本中运行,请随意将read -t
替换为sleep
...(但对于后台进程,请考虑使用cron
和/或at
代替所有这些)。
关于RonJohn's comment,如果目标是每15分钟启动一次,则sleep
命令可以变成:
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
}
然后持续15分钟:
sleepUntilNext 900
Sleep 662.334231...
Done: Wed 26 16:15:00.000171
20分钟的时间:
sleepUntilNext 1200
Sleep 11.509587...
Done: Wed 26 16:20:00.000168
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
(将read -t
替换为sleep
或使用伪文件描述符,请参见有关睡眠和/或read -t的信息)
测试和警告$Ë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
}
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:
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.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
GregD的评论警告说如果在计算机进入深度睡眠模式之前启动,会导致睡眠时间过长...
为此,我已经重新编写了sleepUntilHires
函数:当被唤醒时,它们不会唤醒计算机,而是立即结束(最多20毫秒),并显示时间间隔。
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
}
好的,我现在得开始启动我的笔记本电脑了...
sleepUntilHires 15:13:18.1234
sleep 0.002148s, -> lun 16 jan 2023 15:13:18 CET.123400 ...
Script done 0.000746 seconds late.
这是一次正常运行。现在我将把我的笔记本电脑置于休眠状态:
sleepUntilHires 15:15:18.1234
sleep 86.256328s, -> lun 16 jan 2023 15:15:18 CET.123400 ...
Script done 111.469551 seconds late.
然后当被唤醒时,立即结束睡眠,并显示他的最终差距。
$EPOCHSECOND
和$EPOCHREALTIME
!请阅读我的有关$EPOCHSECOND
和$EPOCHREALTIME
之间差异的警告
此函数使用$EPOCHREALTIME
,因此不要使用$EPOCHSECOND
来确定下一秒:
示例问题:尝试打印下一秒四舍五入后的时间:
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
可能会产生:
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
你将要等待1天而不是2秒钟!!!
你必须使用$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
sleep
和/或 read -t
在这些函数中,我使用 read -t
(超时)替换了 sleep
,有两个原因:
IFS = read -sn 1 -t $slp _
,允许用户通过按下任意键来中断)。sleep
隐含调用 (fork) 到 /bin/sleep
因为它不是一个内置的命令。为了避免键盘交互而使其在非交互式模式下运行,您可以:
sleepUntil.source
脚本开头创建一个伪终端(文件描述符)以重定向 read
的输入: exec {_dummySleepFd}<> <(:)
然后替换
read -t $slp _
由
read -u $_dummySleepFd -t $slp _
sleep
可加载内置(请参见sudo apt install bash-builtins
): enable -f sleep sleep
$BASH_LOADABLES_PATH
已设置,否则: enable -f /usr/lib/bash/sleep sleep
卸载/禁用:
enable -d sleep
exec {_dummySleepFd}<> <(:)
s=${EPOCHREALTIME/.};read -u $_dummySleepFd -t .01;echo $((${EPOCHREALTIME/.}-s))
10975
s=${EPOCHREALTIME/.};read -u $_dummySleepFd -t .01;echo $((${EPOCHREALTIME/.}-s))
10977
使用 /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
/bin/sleep
从文件系统中被加载。虽然这需要更长的时间。但是无论如何,即使在缓存中,运行一个fork来执行sleep
也会消耗资源...enable -f /usr/lib/bash/sleep sleep
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
10803
s=${EPOCHREALTIME/.};sleep .01;echo $((${EPOCHREALTIME/.}-s))
10760
看起来比使用read -u $_dummySleepFd -t ...
稍微好一点。
%86400
技巧仅在两个时间不跨越夏令时转换时才有效。 - 200_successsleepUntil
函数时出现了新的错误:bash: 0-(3600*08: value too great for base (error token is "08")
。 - hlidkaTZ
*的错误:通过LANG=C TZ=UTC+9 sleepUntil 09:09:09 09
和LANG=C TZ=UTC+9 sleepUntilHires 09:09:09.009 09
进行了测试。 - F. Hauri - Give Up GitHub使用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
完成了!
-d
是一个非POSIX扩展的日期命令。在FreeBSD上,它试图设置内核对夏令时的值。 - Dave C这是一个简单的Bash一行命令:
sleep $(expr $(date -d "03/21/2014 12:30" +%s) - $(date +%s))
sleep: invalid option -- '2'
。 - Jan这里有一个解决方案,既可以完成工作,又可以告知用户剩余时间。我几乎每天都使用它来在晚上运行脚本(使用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信号来唤醒它。
想必你的脚本在休眠之前会告知at何时想要被唤醒。
我希望编写一个脚本,只检查小时和分钟,这样我就可以每天用相同的参数运行脚本了。 我不想担心明天是哪一天。所以我采用了不同的方法。
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是该脚本的名称)
再也不会因为输入错误的日期而迟到了,干杯!
till 7 45
。 - Jan接着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
$
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
SIGNCHAR=${SECONDS:0:1}
,然后再判断if [ "$SIGNCHAR" = "-" ]
。这样就可以了。 - H2ONaCl我实际上为了这个目的编写了 https://tamentis.com/projects/sleepuntil/。它有点过度,大部分代码来自于BSD的'at',因此符合标准:
$ sleepuntil noon && sendmail something