如何在Bash脚本中计算经过的时间?

312

我使用date +"%T"打印出开始和结束时间,结果类似于:

10:33:56
10:36:10

我该如何计算并打印出这两者之间的差异?

我希望得到类似这样的结果:

2m 14s

5
另外,你能不能不使用time命令? - anishsane
1
使用Unix时间,即 date +%s,然后相减以获取秒数差异。 - roblogic
20个回答

667

Bash有一个方便的SECONDS内置变量,用于跟踪自 shell 启动以来经过的秒数。当对其进行赋值时,此变量保留其属性,并且赋值后返回的值是自赋值以来经过的秒数加上所赋的值。

因此,在启动计时事件之前,您可以将SECONDS设置为0,然后在事件结束后读取SECONDS,并在显示之前执行时间算术运算。

#!/usr/bin/env bash

SECONDS=0
# do some work
duration=$SECONDS
echo "$(($duration / 60)) minutes and $(($duration % 60)) seconds elapsed."

由于这个解决方案不依赖于date +%s(它是GNU扩展),因此它可以在所有受Bash支持的系统上使用。


69
顺便提一下,你可以通过编写date -u -d @"$diff" +'%-Mm %-Ss'来骗过date让它帮你进行时间算术运算(它会将$diff解释为自纪元以来的秒数,并计算出UTC中的分钟和秒数)。虽然这可能并不比原来更优雅,但它更加难以理解。:-P - ruakh
24
简单明了。如果有人需要计算小时数:echo "$(($diff / 3600)) 小时,$((($diff / 60) % 60)) 分钟和 $(($diff % 60)) 秒已经过去。" - chus
9
为避免在夏令时切换的日期发生竞争条件,建议使用 $(date -u +%s)。注意恶意跳秒 ;) - Tino
3
不错发现。然而这个方法有点脆弱。只有一个这样的变量,所以不能递归地用于性能测量,更糟糕的是,在unset SECONDS之后,它就消失了:echo $SECONDS; unset SECONDS; SECONDS=0; sleep 3; echo $SECONDS - Tino
7
@ParthianShot:很抱歉你这么认为。然而,我认为这个答案是大多数人在寻找类似问题的答案时所需要的:测量事件之间经过了多长时间,而不是计算时间。 - Daniel Kamil Kozar
显示剩余7条评论

155

为了测量经过的时间(以秒为单位),我们需要:

  • 一个代表经过秒数的整数,和
  • 一种将这样的整数转换为可用格式的方法。

经过的秒数的整数值:

  • 有两种Bash内部方法可以找到经过的秒数的整数值:
Bash变量SECONDS(如果未设置SECONDS,则会失去其特殊属性)。
- 将SECONDS的值设置为0:
``` SECONDS=0 sleep 1 # 执行的进程 elapsedseconds=$SECONDS ```
- 在开始时存储变量SECONDS的值:
``` a=$SECONDS sleep 1 # 执行的进程 elapsedseconds=$(( SECONDS - a )) ```
Bash printf选项`%(datefmt)T`:
``` a="$(TZ=UTC0 printf '%(%s)T\n' '-1')" ### `-1` 是当前时间 sleep 1 ### 执行的进程 elapsedseconds=$(( $(TZ=UTC0 printf '%(%s)T\n' '-1') - a )) ```
将这个整数转换为可用格式的方法:

使用bash内置的printf命令可以直接实现:

$ TZ=UTC0 printf '%(%H:%M:%S)T\n' 12345
03:25:45

同样地

$ elapsedseconds=$((12*60+34))
$ TZ=UTC0 printf '%(%H:%M:%S)T\n' "$elapsedseconds"
00:12:34

但是对于超过24小时的时间长度来说,这种方法会失败,因为我们实际上打印的是墙上的时钟时间,而不是真正的持续时间:

$ hours=30;mins=12;secs=24
$ elapsedseconds=$(( ((($hours*60)+$mins)*60)+$secs ))
$ TZ=UTC0 printf '%(%H:%M:%S)T\n' "$elapsedseconds"
06:12:24

对于细节爱好者,来自bash-hackers.org的内容:

%(FORMAT)T输出使用FORMAT作为格式字符串对 strftime(3)进行格式化得到的日期时间字符串。相关参数是自纪元以来的秒数,或-1(当前时间)或-2(shell启动时间)。如果没有提供相应的参数,则默认使用当前时间。

因此,您可能只需调用textifyDuration $elpasedseconds,其中textifyDuration是另一种持续时间打印的实现:

textifyDuration() {
   local duration=$1
   local shiff=$duration
   local secs=$((shiff % 60));  shiff=$((shiff / 60));
   local mins=$((shiff % 60));  shiff=$((shiff / 60));
   local hours=$shiff
   local splur; if [ $secs  -eq 1 ]; then splur=''; else splur='s'; fi
   local mplur; if [ $mins  -eq 1 ]; then mplur=''; else mplur='s'; fi
   local hplur; if [ $hours -eq 1 ]; then hplur=''; else hplur='s'; fi
   if [[ $hours -gt 0 ]]; then
      txt="$hours hour$hplur, $mins minute$mplur, $secs second$splur"
   elif [[ $mins -gt 0 ]]; then
      txt="$mins minute$mplur, $secs second$splur"
   else
      txt="$secs second$splur"
   fi
   echo "$txt (from $duration seconds)"
}

GNU日期

为了获得格式化的时间,我们应该使用外部工具(GNU日期)以多种方式获取长达一年的时间长度,包括纳秒。

日期内的数学计算。

没有必要进行外部算术运算,在date内一步完成所有操作:

date -u -d "0 $FinalDate seconds - $StartDate seconds" +"%H:%M:%S"

是的,在命令字符串中有一个0零。它是必需的。

这是假设您可以将date +"%T"命令更改为date +"%s"命令,以便值将以秒为单位存储(打印)。

请注意,该命令仅限于:

  • $StartDate$FinalDate秒数的正值。
  • $FinalDate中的值大于(比时间晚)$StartDate
  • 时间差小于24小时。
  • 接受具有小时,分钟和秒钟的输出格式。非常容易更改。
  • 使用- u UTC时间是可以接受的。避免“DST”和本地时间校正。

如果你必须使用字符串10:33:56,那就将其转换为秒。另外,“秒”这个词可以缩写为“sec”。
string1="10:33:56"
string2="10:36:10"
StartDate=$(date -u -d "$string1" +"%s")
FinalDate=$(date -u -d "$string2" +"%s")
date -u -d "0 $FinalDate sec - $StartDate sec" +"%H:%M:%S"

请注意,上述的秒数时间转换是相对于“今天”开始的(即当天)。

这个概念可以扩展到纳秒,像这样:

string1="10:33:56.5400022"
string2="10:36:10.8800056"
StartDate=$(date -u -d "$string1" +"%s.%N")
FinalDate=$(date -u -d "$string2" +"%s.%N")
date -u -d "0 $FinalDate sec - $StartDate sec" +"%H:%M:%S.%N"

如果需要计算更长(长达364天)的时间差异,我们必须使用某年的开始作为参考,并使用格式值%j(一年中的日期编号):

类似于:

string1="+10 days 10:33:56.5400022"
string2="+35 days 10:36:10.8800056"
StartDate=$(date -u -d "2000/1/1 $string1" +"%s.%N")
FinalDate=$(date -u -d "2000/1/1 $string2" +"%s.%N")
date -u -d "2000/1/1 $FinalDate sec - $StartDate sec" +"%j days %H:%M:%S.%N"

Output:
026 days 00:02:14.340003400

不幸的是,在这种情况下,我们需要手动从天数中减去1 ONE。 日期命令将一年的第一天视为1。 这并不难...

a=( $(date -u -d "2000/1/1 $FinalDate sec - $StartDate sec" +"%j days %H:%M:%S.%N") )
a[0]=$((10#${a[0]}-1)); echo "${a[@]}"

使用大量秒数是有效的并且在21.1.7 日期示例中有记录。


BusyBox日期

这是一个在小型设备中使用的工具(非常小的可执行文件):BusyBox

要么创建一个名为date的BusyBox链接:

$ ln -s /bin/busybox date

通过调用此 date(将其放置在路径包含的目录中)来使用它。

或者创建一个类似的别名:

$ alias date='busybox date'

BusyBox date 命令有一个很好的选项:-D,可以接收输入时间的格式。

这打开了许多可用作时间的格式。 使用 -D 选项,我们可以直接转换时间 10:33:56:

date -D "%H:%M:%S" -d "10:33:56" +"%Y.%m.%d-%H:%M:%S"

正如你从上面命令的输出中所看到的,日期被假定为“今天”。要从纪元开始获取时间:

$ string1="10:33:56"
$ date -u -D "%Y.%m.%d-%H:%M:%S" -d "1970.01.01-$string1" +"%Y.%m.%d-%H:%M:%S"
1970.01.01-10:33:56

BusyBox date甚至可以在没有-D的情况下接收时间(以上述格式):

$ date -u -d "1970.01.01-$string1" +"%Y.%m.%d-%H:%M:%S"
1970.01.01-10:33:56

而且输出格式甚至可以是自纪元以来的秒数。

$ date -u -d "1970.01.01-$string1" +"%s"
52436

对于两个时间点,以及一些简单的Bash数学计算(BusyBox目前还不能进行数学计算):

string1="10:33:56"
string2="10:36:10"
t1=$(date -u -d "1970.01.01-$string1" +"%s")
t2=$(date -u -d "1970.01.01-$string2" +"%s")
echo $(( t2 - t1 ))

或格式化:

$ date -u -D "%s" -d "$(( t2 - t1 ))" +"%H:%M:%S"
00:02:14

8
这是一篇非常精彩的答案,涵盖了很多方面,并且有很好的解释。谢谢! - Devy
我不得不查找传递给bash中的printf命令的%(FORMAT)T字符串。来自bash-hackers.org的解释是:%(FORMAT)T输出使用FORMAT作为strftime(3)格式字符串生成的日期时间字符串。相关参数是自纪元以来的秒数,或-1(当前时间)或-2(shell启动时间)。如果没有提供相应的参数,则默认使用当前时间。这很深奥。在这种情况下,%s作为提供给strftime(3)的参数表示“自纪元以来的秒数”。 - David Tonhofer

48

这是我是如何做到的:

START=$(date +%s);
sleep 1; # Your stuff
END=$(date +%s);
echo $((END-START)) | awk '{print int($1/60)":"int($1%60)}'

这很简单。在开始时记录秒数,在结束时记录秒数,然后打印出以分钟:秒为单位的差值。


7
跟我的解决方案有何不同?在这种情况下,我真的看不出来调用awk的好处,因为Bash同样能很好地处理整数运算。 - Daniel Kamil Kozar
2
你的答案也是正确的。有些人,比如我,更喜欢使用awk而不是bash的不一致性。 - Dorian
2
你能详细说明一下Bash在整数运算方面的不一致性吗?我想了解更多,因为我之前并不知道这些。 - Daniel Kamil Kozar
15
我正在寻找这个。我不理解对这个回答的批评。我喜欢看到一个问题有多种解决方案。而且,我更喜欢使用awk命令而不是bash(即使只是因为awk在其他Shell中也可以工作)。我更喜欢这个解决方案。但那只是我的个人意见。 - rpsml
6
前导零:echo $((END-START)) | awk '{printf "%02d:%02d\n",int($1/60), int($1%60)}' - Jon Strayer
或者用 printf 函数;输出小时、分钟和秒数:printf '%02dh:%02dm:%02ds\n' $(($secs/3600)) $(($secs%3600/60)) $(($secs%60)) - Ville

20
另一个选择是使用dateutils中的datediff函数(http://www.fresse.org/dateutils/#datediff):
$ datediff 10:33:56 10:36:10
134s

$ datediff 10:33:56 10:36:10 -f%H:%M:%S
0:2:14

$ datediff 10:33:56 10:36:10 -f%0H:%0M:%0S
00:02:14

你也可以使用Gawk。版本为mawk 1.3.4同样具有strftimemktime,但旧版本的mawknawk则没有。

$ TZ=UTC0 awk 'BEGIN{print strftime("%T",mktime("1970 1 1 10 36 10")-mktime("1970 1 1 10 33 56"))}'

00:02:14

或者这里有另外一种使用 GNU date 的方法:

$ date -ud@$(($(date -ud'1970-01-01 10:36:10' +%s)-$(date -ud'1970-01-01 10:33:56' +%s))) +%T

00:02:14

1
我在寻找像你的GNU date方法这样的东西。太聪明了。 - Haohmaru
请删除我的评论...抱歉。 - Bill Gale
1
通过apt安装了dateutils之后,没有datediff命令 - 必须使用dateutils.ddiff - Suzana

12

我想提出另一种避免调用date命令的方法。如果您已经使用%T日期格式收集了时间戳,这可能会有所帮助:

ts_get_sec()
{
  read -r h m s <<< $(echo $1 | tr ':' ' ' )
  echo $(((h*60*60)+(m*60)+s))
}

start_ts=10:33:56
stop_ts=10:36:10

START=$(ts_get_sec $start_ts)
STOP=$(ts_get_sec $stop_ts)
DIFF=$((STOP-START))

echo "$((DIFF/60))m $((DIFF%60))s"

我们甚至可以以同样的方式处理毫秒。
ts_get_msec()
{
  read -r h m s ms <<< $(echo $1 | tr '.:' ' ' )
  echo $(((h*60*60*1000)+(m*60*1000)+(s*1000)+ms))
}

start_ts=10:33:56.104
stop_ts=10:36:10.102

START=$(ts_get_msec $start_ts)
STOP=$(ts_get_msec $stop_ts)
DIFF=$((STOP-START))

min=$((DIFF/(60*1000)))
sec=$(((DIFF%(60*1000))/1000))
ms=$(((DIFF%(60*1000))%1000))

echo "${min}:${sec}.$ms"

1
有没有一种处理毫秒的方法,例如10:33:56.104? - Umesh Rajbhandari
毫秒处理会出现问题,如果毫秒字段以零开头。例如,ms="033"将给出尾数为"027",因为ms字段被解释为八进制。将“echo”...“+ms))”更改为...“+ $ {ms## +(0)}))”将修复此问题,只要在脚本中的某个地方出现“shopt -s extglob”。还应该从h、m和s中删除前导零... - Eric Towers
3
这个命令是将时分秒转换为总秒数。如果您遇到“value too great for base”错误,可以添加 $((10#$h))$((10#$m))$((10#$s)) 来解决问题。 - Niklas

10

Daniel Kamil Kozar的回答的基础上,展示小时/分钟/秒:

echo "Duration: $(($DIFF / 3600 )) hours $((($DIFF % 3600) / 60)) minutes $(($DIFF % 60)) seconds"

因此完整的脚本将是:

date1=$(date +"%s")
date2=$(date +"%s")
DIFF=$(($date2-$date1))
echo "Duration: $(($DIFF / 3600 )) hours $((($DIFF % 3600) / 60)) minutes $(($DIFF % 60)) seconds"

许多解决方案无法处理超过86399秒的时间间隔。而这个解决方案没有这样的限制。 - Jeff Holt

7

这里有一些魔法:

time1=14:30
time2=$( date +%H:%M ) # 16:00
diff=$(  echo "$time2 - $time1"  | sed 's%:%+(1/60)*%g' | bc -l )
echo $diff hours
# outputs 1.5 hours

sed: 替换为一个公式,以转换为 1/60。然后使用 bc 进行时间计算。


7
截至日期GNU Core Utilities)7.4版本,您现在可以使用-d进行算术运算:
$ date -d -30days

Sat Jun 28 13:36:35 UTC 2014

$ date -d tomorrow

Tue Jul 29 13:40:55 UTC 2014

你可以使用的单位包括:天、年、月、小时、分钟和秒。
$ date -d tomorrow+2days-10minutes

Thu Jul 31 13:33:02 UTC 2014

6

这里是一个仅使用date命令的能力,使用“ago”,而不使用第二个变量来存储完成时间的解决方案:

#!/bin/bash

# Save the current time
start_time=$( date +%s.%N )

# Tested program
sleep 1

# The current time after the program has finished
# minus the time when we started, in seconds.nanoseconds
elapsed_time=$( date +%s.%N --date="$start_time seconds ago" )

echo elapsed_time: $elapsed_time

这将会得到:

$ ./time_elapsed.sh

elapsed_time: 1.002257120

你还可以添加一个 printf 来格式化为两位小数。例如:% echo 经过时间:$(printf'%.2f' $ {elapsed_time})秒 - Timothy C. Quinn

3

date 命令可以为您提供差异并为您格式化它(显示 OS X 选项)

date -ujf%s $(($(date -jf%T "10:36:10" +%s) - $(date -jf%T "10:33:56" +%s))) +%T
# 00:02:14

date -ujf%s $(($(date -jf%T "10:36:10" +%s) - $(date -jf%T "10:33:56" +%s))) \
    +'%-Hh %-Mm %-Ss'
# 0h 2m 14s

有些字符串处理可以去除那些空值。

date -ujf%s $(($(date -jf%T "10:36:10" +%s) - $(date -jf%T "10:33:56" +%s))) \
    +'%-Hh %-Mm %-Ss' | sed "s/[[:<:]]0[hms] *//g"
# 2m 14s

如果您首先放置较早的时间,这将无法正常工作。如果您需要处理该问题,请将$(($(date ...) - $(date ...)))更改为$(echo $(date ...) - $(date ...) | bc | tr -d -)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接