web-dev-qa-db-fra.com

Comment calculer la différence de temps dans le script bash?

J'imprime les heures de début et de fin à l'aide de date +"%T", ce qui donne quelque chose comme:

10:33:56
10:36:10

Comment pourrais-je calculer et imprimer la différence entre ces deux?

J'aimerais avoir quelque chose comme:

2m 14s
184
Misha Moroshko

Bash a une variable intégrée SECONDS très pratique qui suit le nombre de secondes écoulées depuis le démarrage du shell. Cette variable conserve ses propriétés lorsqu'elle est affectée et la valeur renvoyée après l'affectation est le nombre de secondes écoulées depuis l'affectation plus la valeur affectée.

Ainsi, vous pouvez simplement définir SECONDS sur 0 avant de commencer l’événement chronométré, il suffit de lire SECONDS après l’événement et de calculer l’heure avant affichage.

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

Comme cette solution ne dépend pas de date +%s (qui est une extension GNU), elle est portable sur tous les systèmes pris en charge par Bash.

376
Daniel Kamil Kozar

Secondes

Pour mesurer le temps écoulé (en secondes), il faut:

  • un entier qui représente le nombre de secondes écoulées et
  • un moyen de convertir un tel entier en un format utilisable.

Un nombre entier de secondes écoulées:

  • Il existe deux méthodes internes bash pour trouver une valeur entière pour le nombre de secondes écoulées:

    1. Variable Bash SECONDS (si SECONDS n'est pas défini, il perd sa propriété spéciale).

      • Définir la valeur de SECONDS à 0:

        SECONDS=0
        sleep 1  # Process to execute
        elapsedseconds=$SECONDS
        
      • Stocker la valeur de la variable SECONDS au début:

        a=$SECONDS
        sleep 1  # Process to execute
        elapsedseconds=$(( SECONDS - a ))
        
    2. Option 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 ))
      

Convertir un tel entier en un format utilisable

La variable interne printf bash peut le faire directement:

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

de même

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

(mais) cela échouera pour des durées de plus de 24 heures, car nous imprimons en fait une heure d'horloge murale, pas vraiment une durée:

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

Pour les amoureux des détails, de bash-hackers.org :

%(FORMAT)T renvoie la chaîne date-heure résultant de l’utilisation de FORMAT en tant que chaîne de format pour strftime(3) . L'argument associé est le nombre de secondes depuis Époque , ou -1 (heure actuelle) ou -2 (heure de démarrage de Shell ). Si aucun argument correspondant n'est fourni, le courant le temps est utilisé par défaut.

Vous pouvez donc simplement appeler textifyDuration $elpasedsecondstextifyDuration est une autre implémentation de l'impression de durée:

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

Date GNU.

Pour obtenir le temps formaté, nous devons utiliser un outil externe (date GNU) de plusieurs manières pour obtenir une durée allant jusqu’à un an et incluant les nanosecondes.

Math dedans date.

Il n’ya pas besoin d’arithmétique externe, faites tout cela en une étape dans date:

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

Oui, il y a un 0 zéro dans la chaîne de commande. C'est nécessaire.

Cela suppose que vous puissiez remplacer la commande date +"%T" par une commande date +"%s" afin que les valeurs soient stockées (imprimées) en quelques secondes.

Notez que la commande est limitée à:

  • Les valeurs positives de $StartDate et $FinalDate secondes.
  • La valeur dans $FinalDate est plus grande (plus tard dans le temps) que $StartDate.
  • Décalage horaire inférieur à 24 heures.
  • Vous acceptez un format de sortie avec heures, minutes et secondes. Très facile à changer.
  • Il est acceptable d’utiliser des temps -u UTC. Pour éviter les corrections "DST" et l'heure locale.

Si vous devez utiliser la chaîne 10:33:56, eh bien, convertissez-la en secondes,
de plus, les secondes du mot pourraient être abrégées en 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"

Notez que la conversion du temps en secondes (comme présenté ci-dessus) est relative au début de "ce" jour (aujourd'hui).


Le concept pourrait être étendu aux nanosecondes, comme ceci:

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"

Si vous devez calculer des différences de temps plus longues (jusqu’à 364 jours), vous devez utiliser le début de (une) année comme référence et la valeur de format %j (le numéro du jour de l’année):

Semblable à:

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

Malheureusement, dans ce cas, nous devons soustraire manuellement 1 ONE du nombre de jours . La commande de date considère le premier jour de l'année comme 1 . Pas si difficile ...

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



L’utilisation du nombre long de secondes est valide et documentée ici:
https://www.gnu.org/software/coreutils/manual/html_node/Examples-of-date.html#Examples-of-date


Date d'occupation

Un outil utilisé dans les plus petits périphériques (un très petit exécutable à installer): Busybox.

Faites un lien vers busybox appelé date:

$ ln -s /bin/busybox date

Utilisez-le ensuite en appelant cette date (placez-la dans un répertoire inclus PATH).

Ou faites un alias comme:

$ alias date='busybox date'

Busybox date a une option intéressante: -D pour recevoir le format de l'heure d'entrée . Cela ouvre beaucoup de formats à utiliser comme heure . En utilisant l'option -D, nous pouvons convertir l'heure 10:33 : 56 directement:

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

Et comme vous pouvez le voir à la sortie de la commande ci-dessus, le jour est supposé être "aujourd'hui". Pour avoir l'heure de départ sur Epoch:

$ 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

La date Busybox peut même recevoir l'heure (dans le format ci-dessus) sans -D:

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

Et le format de sortie pourrait même être quelques secondes depuis Epoch.

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

Pour les deux fois, et un peu de maths bash (busybox ne peut pas faire le calcul, pour le moment):

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 formaté:

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

Voici comment je l'ai fait:

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

Vraiment simple, prenez le nombre de secondes au début, puis le nombre de secondes à la fin et imprimez la différence en minutes: secondes.

30
Dorian

Une autre option consiste à utiliser datediff à partir 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

Vous pouvez également utiliser gawk. mawk 1.3.4 a également strftime et mktime mais les anciennes versions de mawk et nawk ne le sont pas.

$ 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 voici une autre façon de le faire avec 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
13
nisetama

Je voudrais proposer une autre façon d'éviter de rappeler la commande date. Cela peut être utile si vous avez déjà rassemblé des horodatages au format 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"

nous pouvons même gérer des millisecondes de la même manière.

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"
11
Zskdan

Voici un peu de magie:

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 remplace un : par une formule à convertir en 1/60. Puis le calcul du temps fait par bc

6
redolent

Suite à la réponse de Daniel Kamil Kozar, pour afficher les heures/minutes/secondes:

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

Donc le script complet serait:

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

À compter de la date (GNU coreutils) 7.4, vous pouvez maintenant utiliser -d pour faire de l'arithmétique:

$ date -d -30days
Sat Jun 28 13:36:35 UTC 2014

$ date -d tomorrow
Tue Jul 29 13:40:55 UTC 2014

Les unités que vous pouvez utiliser sont les jours, les années, les mois, les heures, les minutes et les secondes:

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

Ou envelopper un peu

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

Alors ça marche.

timerstart; sleep 2; timerstop
seconds=2
2
user3561136

Avec 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
1
user5207022

date peut vous donner la différence et la formater pour vous (options OS X affichées)

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

Certains traitements de chaîne peuvent supprimer ces valeurs vides

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

Cela ne fonctionnera pas si vous placez le temps antérieur en premier. Si vous devez gérer cela, changez $(($(date ...) - $(date ...))) en $(echo $(date ...) - $(date ...) | bc | tr -d -)

1
Dave

Voici une solution utilisant uniquement les capacités des commandes date utilisant "ago", et n'utilisant pas de seconde variable pour stocker l'heure de fin

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

cela donne:

$ ./time_elapsed.sh 
elapsed_time: 1.002257120
1
Zoltan K.
% start=$(date +%s)
% echo "Diff: $(date -d @$(($(date +%s)-$start)) +"%M minutes %S seconds")"
Diff: 00 minutes 11 seconds
1
Joseph

Généraliser la solution de @ nisetama en utilisant la date GNU (Ubuntu 14.04 LTS):

start=`date`
# <processing code>
stop=`date`
duration=`date -ud@$(($(date -ud"$stop" +%s)-$(date -ud"$start" +%s))) +%T`

echo $start
echo $stop
echo $duration

cédant:

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

J'avais besoin d'un script de décalage horaire à utiliser avec mencoder (son --endpos est relatif) et ma solution consiste à appeler un script Python:

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

les fractions de secondes sont également supportées:

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

et cela peut vous dire que la différence entre 200 et 120 est d'1h20m:

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

et peut convertir n'importe quel nombre (probablement fractionnel) de secondes, minutes ou heures en 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 __== "__main__":
   difftime(sys.argv[1],sys.argv[2])
0
18446744073709551615

Je me rends compte qu’il s’agit d’un article plus ancien, mais j’en ai parlé aujourd’hui alors que je travaillais sur un script qui prendrait les dates et les heures d’un fichier journal et calculerait le delta. Le script ci-dessous est certainement excessif, et je recommande fortement de vérifier ma logique et mes maths.

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

Vous obtiendrez une sortie comme suit.

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.

J'ai légèrement modifié le script pour le rendre autonome (c'est-à-dire, il suffit de définir des valeurs de variable), mais peut-être que l'idée générale se dégage également. Vous voudrez peut-être une erreur supplémentaire lors de la vérification des valeurs négatives.

0
Daniel Blackmon

Je ne peux pas commenter la réponse de mcaleaa, c'est pourquoi je poste ceci ici. La variable "diff" devrait être sur le petit cas. Voici un exemple.

[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 ~]# 

La variable précédente a été déclarée en minuscule. C'est ce qui s'est passé quand les majuscules sont utilisées.

[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 ~]# 

Donc, la solution rapide serait comme ça

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