58

Is it possible to easily format seconds as a human-readable time in bash?

I don't want to format it as a date, but as the number of days/hours/minutes, etc...

2
  • Could you provide an example/several examples please?
    – gabe.
    Dec 16, 2011 at 22:51
  • 2
    Are you saying you have an interval, or a date since the epoch? Dec 16, 2011 at 23:22

11 Answers 11

69

You can use something like this:

function displaytime {
  local T=$1
  local D=$((T/60/60/24))
  local H=$((T/60/60%24))
  local M=$((T/60%60))
  local S=$((T%60))
  (( $D > 0 )) && printf '%d days ' $D
  (( $H > 0 )) && printf '%d hours ' $H
  (( $M > 0 )) && printf '%d minutes ' $M
  (( $D > 0 || $H > 0 || $M > 0 )) && printf 'and '
  printf '%d seconds\n' $S
}

Examples:

$ displaytime 11617
3 hours 13 minutes and 37 seconds
$ displaytime 42
42 seconds
$ displaytime 666
11 minutes and 6 seconds
1
  • 2
    This is really nice — the only thing it's missing is dropping the -s from singular day(s), hour(s), minute(s) and second(s). Feb 3, 2021 at 11:26
34

Easiest and cleanest way is this one liner (here assuming GNU date):

If the number of seconds is, say:

seconds=123456789 # as in one of the answers above

date -ud "@$seconds" +"$(( $seconds/3600/24 )) days %H hours %M minutes %S seconds"

--> output: 1428 days 21 hours 33 minutes 09 seconds

3
  • 3
    If your duration will always be less than a day you can use +%T format for Hours:Minutes:Seconds for this date +%T "1970-01-01 + ${seconds} seconds" to get 21:33:09 Nov 9, 2019 at 4:58
  • 1
    @YzmirRamirez your command is unnecessarily complicated because it factors in the timezone... more straight forward would be still with -u to simply stay in UTC since it is only a duration: date -u +"%T" -d "@${seconds}". Oct 11, 2023 at 5:09
  • Pretty nice answer especially when combined with @DJCrashdummy comment! 👍
    – Masked Man
    Feb 22 at 0:00
12

Credit goes to Stéphane Gimenez but if someone would like to display seconds only if a period is less than a minute here is my modified version that I use (also with fixed pluralization):

converts()
{
    local t=$1

    local d=$((t/60/60/24))
    local h=$((t/60/60%24))
    local m=$((t/60%60))
    local s=$((t%60))

    if [[ $d > 0 ]]; then
            [[ $d = 1 ]] && echo -n "$d day " || echo -n "$d days "
    fi
    if [[ $h > 0 ]]; then
            [[ $h = 1 ]] && echo -n "$h hour " || echo -n "$h hours "
    fi
    if [[ $m > 0 ]]; then
            [[ $m = 1 ]] && echo -n "$m minute " || echo -n "$m minutes "
    fi
    if [[ $d = 0 && $h = 0 && $m = 0 ]]; then
            [[ $s = 1 ]] && echo -n "$s second" || echo -n "$s seconds"
    fi  
    echo
}

An alternative example in POSIX:

converts(){
    t=$1

    d=$((t/60/60/24))
    h=$((t/60/60%24))
    m=$((t/60%60))
    s=$((t%60))

    if [ $d -gt 0 ]; then
            [ $d = 1 ] && printf "%d day " $d || printf "%d days " $d
    fi
    if [ $h -gt 0 ]; then
            [ $h = 1 ] && printf "%d hour " $h || printf "%d hours " $h
    fi
    if [ $m -gt 0 ]; then
            [ $m = 1 ] && printf "%d minute " $m || printf "%d minutes " $m
    fi
    if [ $d = 0 ] && [ $h = 0 ] && [ $m = 0 ]; then
            [ $s = 1 ] && printf "%d second" $s || printf "%d seconds" $s
    fi
    printf '\n'
}
1
  • This worked really well. I simply pasted the function into my script and ran convert $s to get pretty output.
    – flickerfly
    Oct 19, 2016 at 19:25
6

I'd do it like this:

$ seconds=123456789; echo $((seconds/86400))" days "$(date -d "1970-01-01 + $seconds seconds" "+%H hours %M minutes %S seconds")
1428 days 21 hours 33 minutes 09 seconds
$

Here's the one liner above, broken down so that it's easier to understand:

$ seconds=123456789
$ echo $((seconds/86400))" days"\
     $(date -d "1970-01-01 + $seconds seconds" "+%H hours %M minutes %S seconds")

In the above I'm echoing out the output of another command that's run inside the $( ... ) sub-command. That sub-command is doing this, calculating the number of days (seconds/86400), then using the date command in another sub-command $(date -d ... ), to generate the hours, minutes, and seconds for a given number of seconds.

2
  • 1
    why are you adding an unnecessary echo? and factoring in the timezone also makes no sense! || much easier would be date -u +"$(( ${seconds} / 86400 )) days %H hours %M minutes %S seconds" -d "@${seconds}". Oct 11, 2023 at 8:10
  • @DJCrashdummy, thank you for asking. It is the first solution that uses 'date' to do the calculations, and it's not without its flaws. I deeply appreciate the contributions from all commenters who have enhanced this method.
    – atti
    Jan 19 at 19:16
4

I'm building on atti's answer which I liked as an idea.

You can do this with the bash builtin printf which will take the seconds since the epoch as an argument. No need to fork to run date.

You have to set the timezone to UTC for printf because it formats the time in your local timezone and you will get the wrong answer if you are not in UTC time.

$ seconds=123456789
$ TZ=UTC printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1428 days 21 hours 33 minutes 09 seconds

In my local time (which is currently NZDT - +1300) the answer is wrong if I do not set the timezone

$ seconds=123456789
$ printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1428 days 09 hours 33 minutes 09 seconds

With and without setting the timezone

$ seconds=$(( 3600 * 25))
$ printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1 days 13 hours 00 minutes 00 seconds

$ TZ=UTC printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1 days 01 hours 00 minutes 00 seconds
1
  • Note that bash's printf introduced this %(datefmt)T notation beginning with bash-4.2-alpha.
    – bishop
    Mar 27, 2019 at 15:40
3

I modified the displaytime function above... as follows:

seconds2time ()
{
   T=$1
   D=$((T/60/60/24))
   H=$((T/60/60%24))
   M=$((T/60%60))
   S=$((T%60))

   if [[ ${D} != 0 ]]
   then
      printf '%d days %02d:%02d:%02d' $D $H $M $S
   else
      printf '%02d:%02d:%02d' $H $M $S
   fi
}

because I always want to see HH:MM:SS, even if they are zeros.

2

In addition to the other answers, to get an output in [[[[d and ]hh:]mm:]ss | ss 's'] format it could be done like this:

function format_seconds() {
  (($1 >= 86400)) && printf '%d days and ' $(($1 / 86400)) # days
  (($1 >= 3600)) && printf '%02d:' $(($1 / 3600 % 24))     # hours
  (($1 >= 60)) && printf '%02d:' $(($1 / 60 % 60))         # minutes
  printf '%02d%s\n' $(($1 % 60)) "$( (($1 < 60 )) && echo ' s.' || echo '')"
}

For example, if we execute:

format_seconds 1000000
format_seconds 86450
format_seconds 9000
format_seconds 2500
format_seconds 60
format_seconds 34

We will get the following output:

11 days and 13:46:40
1 days and 00:00:50
02:30:00
41:40
01:00
34 s.

Although the question was asked more than 8 years ago i hope it helps to someone :)

1

Building on Stéphane Gimenez's answer, but an alternative to dimir's:

Since printf is already in play, may as well use %s and pass is the s (or not) to pluralize when needed:

displaytime() {
  local T=$1
  local D=$((T/60/60/24))
  local H=$((T/60/60%24))
  local M=$((T/60%60))
  local S=$((T%60))
  (( $D > 0 )) && printf '%d day%s ' $D $( (( $D > 1 )) && echo s)
  (( $H > 0 )) && printf '%d hour%s ' $H $( (( $H > 1 )) && echo s)
  (( $M > 0 )) && printf '%d minute%s ' $M $( (( $M > 1 )) && echo s)
  (( $D > 0 || $H > 0 || $M > 0 )) && printf 'and '
  printf '%d second%s\n' $S $( (( $S != 1 )) && echo s)
}
1

I slightly improved @Jacques' answer into this:

FormatSeconds () { 
  seconds="$1"
  date -ud @${seconds} +"$(( seconds/3600/24 ))d %-Hh %-Mm %-Ss" \
    | sed -E 's/\b0(s|m|h|d) ?//g; s/ +$//; s/^$/0s/'
}

which will just use "d/h/m/s", strip off leading zeroes and omit zero parts. So:

> FormatSeconds 123456789
1428d 21h 33m 9s
> FormatSeconds $((1 * 24 * 60 * 60 + 60 + 1))
1d 1m 1s
6
  • +1 for the idea with sed, but your implementation is way too complicated! || date can prevent leading 0 by itself with %- and this makes the sed part even more easier than it could be already: date -u +"$(( ${seconds} / 86400 ))d %-Hh %-Mmin %-Ss" -d "@${seconds}" | sed -E -e 's/\<0(d|h|min|s)\> ?//g ; s/^$/0s/' Oct 11, 2023 at 9:12
  • BTW: the 2nd seconds in your date-command misses at least a preceded $! even better would be ${seconds}. Oct 11, 2023 at 9:16
  • @DJCrashdummy The second seconds does not need a $ because that's how arithmetic evaluation works, see e.g. bash -c 'ONE=1 ; echo $((ONE + 1))' . Oct 15, 2023 at 22:03
  • @DJCrashdummy Your version of the command leaves an extra blank at the end for seconds=$((60*60+60)). Oct 15, 2023 at 22:21
  • i just wanted to be on the safe side with the parameter expansion, since it can't be about execution time... else you would write .../86400 instead of .../3600/24 and use ${1} straight instead of ${seconds}. Oct 16, 2023 at 7:36
-1

Here one

secs=378444
echo $(($secs/86400))d $(($(($secs - $secs/86400*86400))/3600))h:$(($(($secs - $secs/86400*86400))%3600/60))m:$(($(($secs - $secs/86400*86400))%60))s

Output:

4d 9h:7m:24s
-3

date --date '@1005454800' gives you Sun Nov 11 00:00:00 EST 2001, which is 1005454800 seconds after the Unix epoch. You can format that with the date +FORMAT option.

1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .