Como calcular a diferença de tempo no script bash?

Eu imprimo a hora inicial e final usando date +"%T" , o que resulta em algo como:

 10:33:56 10:36:10 

Como eu poderia calcular e imprimir a diferença entre esses dois?

Eu gostaria de obter algo como:

 2m 14s 

O Bash tem uma variável interna SECONDS que rastreia o número de segundos que se passaram desde que o shell foi iniciado. Essa variável mantém suas propriedades quando atribuída e o valor retornado após a atribuição é o número de segundos desde a atribuição mais o valor atribuído.

Assim, você pode apenas definir SECONDS para 0 antes de iniciar o evento cronometrado, simplesmente ler SECONDS após o evento e fazer a aritmética de tempo antes de exibir.

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

Como esta solução não depende da date +%s (que é uma extensão GNU), é portável para todos os sistemas suportados pelo Bash.

Segundos

Para medir o tempo decorrido (em segundos), precisamos:

  • um inteiro que representa a contagem de segundos decorridos e
  • uma maneira de converter esse inteiro em um formato utilizável.

Um valor inteiro de segundos decorridos:

  • Existem duas formas internas para encontrar um valor inteiro para o número de segundos decorridos:

    1. Variável de Bash SECONDS (se o SECONDS não estiver definido, perde sua propriedade especial).

      • Definindo o valor de SECONDS como 0:

         SECONDS=0 sleep 1 # Process to execute elapsedseconds=$SECONDS 
      • Armazenando o valor da variável SECONDS no início:

         a=$SECONDS sleep 1 # Process to execute elapsedseconds=$(( SECONDS - a )) 
    2. Opção printf bash %(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 )) 

Converta tal inteiro em um formato utilizável

O printf interno do bash pode fazer isso diretamente:

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

similarmente

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

mas isso irá falhar por durações de mais de 24 horas, já que na verdade imprimimos um tempo de wallclock, não realmente uma duração:

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

Para os amantes do detalhe, de bash-hackers.org :

%(FORMAT)T retorna a string de data e hora resultante do uso de FORMAT como uma string de formato para strftime(3) . O argumento associado é o número de segundos desde o Epoch , ou -1 (tempo atual) ou -2 (tempo de boot do shell). Se nenhum argumento correspondente for fornecido, a hora atual é usada como padrão.

Então você pode querer apenas chamar textifyDuration $elpasedseconds onde textifyDuration é mais uma implementação de impressão de duração:

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

Data do GNU.

Para obter o tempo formatado, devemos usar uma ferramenta externa (data GNU) de várias maneiras para chegar a quase um ano e include nanossegundos.

Matemática dentro da data.

Não há necessidade de aritmética externa, faça tudo em uma etapa dentro da date :

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

Sim, existe um 0 zero na string de comando. É necessário.

Supondo que você poderia alterar o comando date +"%T" para um comando date +"%s" , os valores serão armazenados (impressos) em segundos.

Note que o comando está limitado a:

  • Valores positivos de $StartDate e $FinalDate segundos.
  • O valor em $FinalDate é maior (mais tarde) do que $StartDate .
  • Diferença de tempo menor que 24 horas.
  • Você aceita um formato de saída com Horas, Minutos e Segundos. Muito fácil de mudar.
  • É aceitável usar -u tempos UTC. Para evitar “DST” e correções de hora local.

Se você precisa usar a string 10:33:56 , basta convertê-la em segundos,
Além disso, a palavra segundos poderia ser abreviada como 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" 

Observe que a conversão do tempo de segundos (conforme apresentado acima) é relativa ao início do “dia” (Hoje).


O conceito poderia ser estendido para nanossegundos, assim:

 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" 

Se for necessário calcular as diferenças de tempo mais longas (até 364 dias), devemos usar o início de (alguns) anos como referência e o valor do formato %j (o número do dia no ano):

Igual a:

 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 

Infelizmente, neste caso, precisamos subtrair 1 ONE manualmente do número de dias. O comando de data visualiza o primeiro dia do ano como 1. Não tão difícil …

 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[@]}" 

O uso de um número longo de segundos é válido e documentado aqui:
https://www.gnu.org/software/coreutils/manual/html_node/Examples-of-date.html#Examples-of-date


Data do Busybox

Uma ferramenta usada em dispositivos menores (um executável muito pequeno para instalar): Busybox.

Ou faça um link para busybox chamado date:

 $ ln -s /bin/busybox date 

Use-o então chamando esta date (coloque-o em um diretório PATH incluído).

Ou crie um alias como:

 $ alias date='busybox date' 

Data do Busybox tem uma boa opção: -D para receber o formato do tempo de input. Isso abre muitos formatos para serem usados ​​como tempo. Usando a opção -D, podemos converter o tempo de 10:33:56 diretamente:

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

E como você pode ver na saída do comando acima, o dia é assumido como “hoje”. Para começar o tempo começando na época:

 $ 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 

Data do Busybox pode até receber o tempo (no formato acima) sem -D:

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

E o formato de saída pode ser segundos desde a época.

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

Para ambas as vezes, e um pouco bash matemática (busybox não pode fazer as contas, ainda):

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

Ou formatado:

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

Aqui está como eu fiz:

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

Realmente simples, tome o número de segundos no início, depois tire o número de segundos no final e imprima a diferença em minutos: segundos.

Outra opção é usar datediff de 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 

Você também pode usar o gawk . mawk 1.3.4 também possui mawk e mktime mas versões antigas de mawk e nawk não.

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

Ou aqui está outra maneira de fazer isso com a date GNU:

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

Eu gostaria de propor outra maneira que evite chamar o comando date . Pode ser útil no caso de você já ter reunido os timestamps no formato de data %T :

 ts_get_sec() { read -rhms <<< $(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" 

Podemos até mesmo processar milissegundos da mesma maneira.

 ts_get_msec() { read -rhms 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" 

Aqui está alguma mágica:

 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 substitui a : com uma fórmula para converter para 1/60. Então o cálculo de tempo que é feito por bc

A partir da data (GNU coreutils) 7.4 agora você pode usar -d para fazer aritmética:

 $ date -d -30days Sat Jun 28 13:36:35 UTC 2014 $ date -d tomorrow Tue Jul 29 13:40:55 UTC 2014 

As unidades que você pode usar são dias, anos, meses, horas, minutos e segundos:

 $ date -d tomorrow+2days-10minutes Thu Jul 31 13:33:02 UTC 2014 

Na sequência da resposta de Daniel Kamil Kozar, para mostrar horas / minutos / segundos:

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

Então o roteiro completo seria:

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

Ou embrulhe um pouco

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

Então isso funciona.

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

Com units GNU:

 $ 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 

date pode lhe dar a diferença e formatá-lo para você (opções do OS X mostradas)

 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 

Algum processamento de string pode remover esses valores vazios

 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 

Isso não funcionará se você colocar o horário anterior primeiro. Se você precisar lidar com isso, altere $(($(date ...) - $(date ...))) para $(echo $(date ...) - $(date ...) | bc | tr -d -)

Aqui está uma solução usando apenas os resources de comandos de date usando “ago” e não usando uma segunda variável para armazenar o horário de término:

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

isto dá:

 $ ./time_elapsed.sh elapsed_time: 1.002257120 

Eu percebo que este é um post mais antigo, mas eu o vi hoje enquanto estava trabalhando em um script que pegava datas e horas de um arquivo de log e calculava o delta. O script abaixo é certamente um exagero, e eu recomendo fortemente verificar minha lógica e matemática.

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

Você obterá a saída da seguinte maneira.

 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. 

Eu modifiquei o script um pouco para torná-lo autônomo (ou seja, apenas defina valores variables), mas talvez a idéia geral também apareça. Você pode querer algum erro adicional para verificar valores negativos.

Eu precisava de um script de diferença de tempo para usar com o mencoder (seu --endpos é relativo), e minha solução é chamar um script Python:

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

frações de segundos também são suportadas:

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

e pode dizer que a diferença entre 200 e 120 é 1h 20m:

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

e pode converter qualquer número (provavelmente fracionário) de segundos ou minutos ou horas para 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]) 

Generalizando a solução do @ nisetama usando a data GNU (confiável Ubuntu 14.04 LTS):

 start=`date` #  stop=`date` duration=`date -ud@$(($(date -ud"$stop" +%s)-$(date -ud"$start" +%s))) +%T` echo $start echo $stop echo $duration 

produzindo:

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

Não posso comentar a resposta de mcaleaa, por isso eu postei isso aqui. A variável “diff” deve estar em maiúsculas. Aqui está um exemplo.

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

Variável anterior foi declarada em checkbox baixa. Foi o que aconteceu quando maiúsculas são usadas.

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

Então, uma solução rápida seria assim

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

Aqui está minha implementação bash (com bits tirados de outro 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 

Não testado, pode ser buggy.