Как рассчитать время, прошедшее в bash-скрипте?

Я печатаю начальное и конечное время с помощью date +"%T", что приводит к чему-то вроде:

10:33:56
10:36:10

Как я мог рассчитать и распечатать разницу между этими двумя?

Я хотел бы получить что-то вроде:

2m 14s

Ответ 1

Bash имеет удобную встроенную переменную SECONDS, которая отслеживает количество секунд, прошедших с момента запуска оболочки. Эта переменная сохраняет свои свойства при назначении, а значение, возвращаемое после присвоения, - это количество секунд с момента назначения плюс назначенное значение.

Таким образом, вы можете просто установить SECONDS на 0 перед запуском события с таймированием, просто прочитайте SECONDS после события и выполните арифметику времени перед отображением.

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

Поскольку это решение не зависит от date +%s (которое является расширением GNU), оно переносится во все системы, поддерживаемые Bash.

Ответ 2

секунды

Чтобы измерить прошедшее время (в секундах) нам нужно:

  • целое число, представляющее количество прошедших секунд и
  • способ конвертировать такое целое число в пригодный для использования формат.

Целочисленное значение прошедших секунд:

  • Существует два внутренних способа bash найти целое значение количества прошедших секунд:

    1. Переменная Bash SECONDS (если SECONDS не установлена, она теряет свое специальное свойство).

      • Установка значения SECONDS на 0:

        SECONDS=0
        sleep 1  # Process to execute
        elapsedseconds=$SECONDS
        
      • Сохранение значения переменной SECONDS в начале:

        a=$SECONDS
        sleep 1  # Process to execute
        elapsedseconds=$(( SECONDS - a ))
        
    2. Опция Bash Printf %(datefmt)T:

      a="$(TZ=UTC0 printf '%(%s)T\n' '-1')"    ### '-1'  is the current time
      sleep 1                                  ### Process to execute
      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 (оболочки время запуска). Если соответствующий аргумент не указан, текущий время используется по умолчанию.

Поэтому вы можете просто позвонить 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. Чтобы избежать "летнего времени" и корректировки локального времени.

Если вы должны использовать строку 10:33:56, просто преобразуйте ее в секунды,
Кроме того, слово секунд может быть сокращено как сек:

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 ОДИН из числа дней. Команда date отображает первый день года как 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[@]}"



Использование длинного количества секунд допустимо и задокументировано здесь:
https://www.gnu.org/software/coreutils/manual/html_node/Examples-of-Date.html#Examples-of-Date


Дата занятого места

Инструмент, используемый на небольших устройствах (очень маленький исполняемый файл для установки): Busybox.

Либо сделайте ссылку на busybox с именем date:

$ ln -s /bin/busybox date

Затем используйте его, вызвав этот date (поместите его в каталог, включенный в PATH).

Или создайте псевдоним:

$ alias date='busybox date'

У даты Busybox есть хорошая опция: -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

Дата занятой ячейки может даже получать время (в формате выше) без -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 math (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

Ответ 3

Вот как я это сделал:

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

На самом деле просто, возьмите количество секунд в начале, затем возьмите количество секунд в конце и распечатайте разницу в минутах: секунды.

Ответ 4

Другой вариант - использовать datediff из dateutils (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 также имеет strftime и mktime, но более старые версии mawk и nawk этого не делают.

$ 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 [email protected]$(($(date -ud'1970-01-01 10:36:10' +%s)-$(date -ud'1970-01-01 10:33:56' +%s))) +%T
00:02:14

Ответ 5

Я хотел бы предложить другой способ, чтобы избежать вызова команды 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"

Ответ 6

Вот какая магия:

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 заменяет a : формулой для преобразования в 1/60. Тогда вычисление времени, сделанное bc

Ответ 7

Начиная с даты (GNU coreutils) 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

Ответ 8

Следуя от Даниэля Камиля Козара, ответьте, чтобы показать часы/минуты/секунды:

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

Таким образом, полный script будет:

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

Ответ 9

Или немного оберните его

alias timerstart='starttime=$(date +"%s")'
alias timerstop='echo seconds=$(($(date +"%s")-$starttime))'

Тогда это работает.

timerstart; sleep 2; timerstop
seconds=2

Ответ 10

Вот решение, использующее только команды date с использованием "назад", и не использующее вторую переменную для сохранения времени окончания:

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

Ответ 11

% start=$(date +%s)
% echo "Diff: $(date -d @$(($(date +%s)-$start)) +"%M minutes %S seconds")"
Diff: 00 minutes 11 seconds

Ответ 12

Мне понадобилась разница во времени script для использования с mencoder (ее --endpos относительна), и мое решение вызовите Python script:

$ ./timediff.py 1:10:15 2:12:44
1:02:29

также поддерживаются доли секунд:

$ echo "diff is `./timediff.py 10:51.6 12:44` (in hh:mm:ss format)"
diff is 0:01:52.4 (in hh:mm:ss format)

и он может сказать вам, что разница между 200 и 120 составляет 1h 20m:

$ ./timediff.py 120:0 200:0
1:20:0

и может преобразовывать любое (возможно, дробное) количество секунд или минут или часов в hh: mm: ss

$ ./timediff.py 0 3600
1:00:0
$ ./timediff.py 0 3.25:0:0
3:15:0

timediff.py:

#!/usr/bin/python

import sys

def x60(h,m):
    return 60*float(h)+float(m)

def seconds(time):
    try:
       h,m,s = time.split(':')
       return x60(x60(h,m),s)
    except ValueError:
       try:
          m,s = time.split(':')
          return x60(m,s)
       except ValueError:
          return float(time)

def difftime(start, end):
    d = seconds(end) - seconds(start)
    print '%d:%02d:%s' % (d/3600,d/60%60,('%02f' % (d%60)).rstrip('0').rstrip('.'))

if __name__ == "__main__":
   difftime(sys.argv[1],sys.argv[2])

Ответ 13

С GNU units:

$ units
2411 units, 71 prefixes, 33 nonlinear units
You have: (10hr+36min+10s)-(10hr+33min+56s)
You want: s
    * 134
    / 0.0074626866
You have: (10hr+36min+10s)-(10hr+33min+56s)
You want: min
    * 2.2333333
    / 0.44776119

Ответ 14

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 -)

Ответ 15

Я понимаю, что это более старая статья, но сегодня я перешел к ней во время работы над script, который будет принимать даты и время из файла журнала и вычислять дельта. Ниже приведено script ниже, и я настоятельно рекомендую проверить свою логику и математику.

#!/bin/bash

dTime=""
tmp=""

#firstEntry="$(head -n 1 "$LOG" | sed 's/.*] \([0-9: -]\+\).*/\1/')"
firstEntry="2013-01-16 01:56:37"
#lastEntry="$(tac "$LOG" | head -n 1 | sed 's/.*] \([0-9: -]\+\).*/\1/')"
lastEntry="2014-09-17 18:24:02"

# I like to make the variables easier to parse
firstEntry="${firstEntry//-/ }"
lastEntry="${lastEntry//-/ }"
firstEntry="${firstEntry//:/ }"
lastEntry="${lastEntry//:/ }"

# remove the following lines in production
echo "$lastEntry"
echo "$firstEntry"

# compute days in last entry
for i in `seq 1 $(echo $lastEntry|awk '{print $2}')`; do {
  case "$i" in
   1|3|5|7|8|10|12 )
    dTime=$(($dTime+31))
    ;;
   4|6|9|11 )
    dTime=$(($dTime+30))
    ;;
   2 )
    dTime=$(($dTime+28))
    ;;
  esac
} done

# do leap year calculations for all years between first and last entry
for i in `seq $(echo $firstEntry|awk '{print $1}') $(echo $lastEntry|awk '{print $1}')`; do {
  if [ $(($i%4)) -eq 0 ] && [ $(($i%100)) -eq 0 ] && [ $(($i%400)) -eq 0 ]; then {
    if [ "$i" = "$(echo $firstEntry|awk '{print $1}')" ] && [ $(echo $firstEntry|awk '{print $2}') -lt 2 ]; then {
      dTime=$(($dTime+1))
    } elif [ $(echo $firstEntry|awk '{print $2}') -eq 2 ] && [ $(echo $firstEntry|awk '{print $3}') -lt 29 ]; then {
      dTime=$(($dTime+1))
    } fi
  } elif [ $(($i%4)) -eq 0 ] && [ $(($i%100)) -ne 0 ]; then {
    if [ "$i" = "$(echo $lastEntry|awk '{print $1}')" ] && [ $(echo $lastEntry|awk '{print $2}') -gt 2 ]; then {
      dTime=$(($dTime+1))
    } elif [ $(echo $lastEntry|awk '{print $2}') -eq 2 ] && [ $(echo $lastEntry|awk '{print $3}') -ne 29 ]; then {
      dTime=$(($dTime+1))
    } fi
  } fi
} done

# substract days in first entry
for i in `seq 1 $(echo $firstEntry|awk '{print $2}')`; do {
  case "$i" in
   1|3|5|7|8|10|12 )
    dTime=$(($dTime-31))
    ;;
   4|6|9|11 )
    dTime=$(($dTime-30))
    ;;
   2 )
    dTime=$(($dTime-28))
    ;;
  esac
} done

dTime=$(($dTime+$(echo $lastEntry|awk '{print $3}')-$(echo $firstEntry|awk '{print $3}')))

# The above gives number of days for sample. Now we need hours, minutes, and seconds
# As a bit of hackery I just put the stuff in the best order for use in a for loop
dTime="$(($(echo $lastEntry|awk '{print $6}')-$(echo $firstEntry|awk '{print $6}'))) $(($(echo $lastEntry|awk '{print $5}')-$(echo $firstEntry|awk '{print $5}'))) $(($(echo $lastEntry|awk '{print $4}')-$(echo $firstEntry|awk '{print $4}'))) $dTime"
tmp=1
for i in $dTime; do {
  if [ $i -lt 0 ]; then {
    case "$tmp" in
     1 )
      tmp="$(($(echo $dTime|awk '{print $1}')+60)) $(($(echo $dTime|awk '{print $2}')-1))"
      dTime="$tmp $(echo $dTime|awk '{print $3" "$4}')"
      tmp=1
      ;;
     2 )
      tmp="$(($(echo $dTime|awk '{print $2}')+60)) $(($(echo $dTime|awk '{print $3}')-1))"
      dTime="$(echo $dTime|awk '{print $1}') $tmp $(echo $dTime|awk '{print $4}')"
      tmp=2
      ;;
     3 )
      tmp="$(($(echo $dTime|awk '{print $3}')+24)) $(($(echo $dTime|awk '{print $4}')-1))"
      dTime="$(echo $dTime|awk '{print $1" "$2}') $tmp"
      tmp=3
      ;;
    esac
  } fi
  tmp=$(($tmp+1))
} done

echo "The sample time is $(echo $dTime|awk '{print $4}') days, $(echo $dTime|awk '{print $3}') hours, $(echo $dTime|awk '{print $2}') minutes, and $(echo $dTime|awk '{print $1}') seconds."

Вы получите вывод следующим образом.

2012 10 16 01 56 37
2014 09 17 18 24 02
The sample time is 700 days, 16 hours, 27 minutes, and 25 seconds.

Я немного изменил script, чтобы сделать его автономным (т.е. просто установить значения переменных), но, возможно, общая идея также встречается. Возможно, вам понадобится дополнительная проверка ошибок для отрицательных значений.

Ответ 16

Обобщение решения @nisetama с использованием даты GNU (верный Ubuntu 14.04 LTS):

start='date'
# <processing code>
stop='date'
duration='date [email protected]$(($(date -ud"$stop" +%s)-$(date -ud"$start" +%s))) +%T'

echo $start
echo $stop
echo $duration

получают:

Wed Feb 7 12:31:16 CST 2018
Wed Feb 7 12:32:25 CST 2018
00:01:09

Ответ 17

Я не могу комментировать mcaleaa ответ, поэтому я публикую это здесь. Переменная diff должна быть в маленьком регистре. вот пример.

[[email protected] ~]# date1=$(date +"%s"); date
Wed Feb 21 23:00:20 MYT 2018
[[email protected] ~]# 
[[email protected] ~]# date2=$(date +"%s"); date
Wed Feb 21 23:00:33 MYT 2018
[[email protected] ~]# 
[[email protected] ~]# diff=$(($date2-$date1))
[[email protected] ~]# 

Предыдущая переменная была объявлена в нижнем регистре. это то, что произошло, когда используется верхний регистр.

[[email protected] ~]# echo "Duration: $(($DIFF / 3600 )) hours $((($DIFF % 3600) / 60)) minutes $(($DIFF % 60)) seconds"
-bash: / 3600 : syntax error: operand expected (error token is "/ 3600 ")
[[email protected] ~]# 

Итак, быстрое исправление будет выглядеть так

[[email protected] ~]# echo "Duration: $(($diff / 3600 )) hours $((($diff % 3600) / 60)) minutes $(($diff % 60)) seconds"
Duration: 0 hours 0 minutes 13 seconds
[[email protected] ~]# 

Ответ 18

#!/bin/bash

START_TIME=$(date +%s)

sleep 4

echo "Total time elapsed: $(date -ud "@$(($(date +%s) - $START_TIME))" +%T) (HH:MM:SS)"
$ ./total_time_elapsed.sh 
Total time elapsed: 00:00:04 (HH:MM:SS)

Ответ 19

Здесь моя реализация bash (с битами, взятыми из других SO; -)

function countTimeDiff() {
    timeA=$1 # 09:59:35
    timeB=$2 # 17:32:55

    # feeding variables by using read and splitting with IFS
    IFS=: read ah am as <<< "$timeA"
    IFS=: read bh bm bs <<< "$timeB"

    # Convert hours to minutes.
    # The 10# is there to avoid errors with leading zeros
    # by telling bash that we use base 10
    secondsA=$((10#$ah*60*60 + 10#$am*60 + 10#$as))
    secondsB=$((10#$bh*60*60 + 10#$bm*60 + 10#$bs))
    DIFF_SEC=$((secondsB - secondsA))
    echo "The difference is $DIFF_SEC seconds.";

    SEC=$(($DIFF_SEC%60))
    MIN=$((($DIFF_SEC-$SEC)%3600/60))
    HRS=$((($DIFF_SEC-$MIN*60)/3600))
    TIME_DIFF="$HRS:$MIN:$SEC";
    echo $TIME_DIFF;
}

$ countTimeDiff 2:15:55 2:55:16
The difference is 2361 seconds.
0:39:21

Не проверено, может быть ошибкой.