Display runnning countdown in a bash script?

I am looking for a way to display on a single line, a running countdown for a given amount of time in a terminal using a bash script.

I am looking for this to use as part of a larger bash script that captures Video. The script sets up a bunch of parameters for DVgrab, and one of the parameters set is the tape length. I would like to use this $tape_length as the input for a running countdown so that at any time during capture I can see how much longer the capture will run for.

On a side note, in my script $tape_length is entered in the form hh:mm:ss

I feel like this should be easy, but despite some extensive searching I can't find any leads or even scripts to steal and modify! Any help is much appreciated, I'm very new to bash scripting...

This uses a few tricks to do what you want:

  • The printf command does NOT append a linefeed automatically unless you tell it to.
  • Setting IFS to something else lets the shell split things apart into arrays on whatever delimiter you want, in this case , :.
  • The date command can be used to produce time in seconds.
  • Carriage returns return the cursor to the beginning of the line without moving to the next line.
function countdown
{
        local OLD_IFS="${IFS}"
        IFS=":"
        local ARR=( $1 )
        local SECONDS=$((  (ARR[0] * 60 * 60) + (ARR[1] * 60) + ARR[2]  ))
        local START=$(date +%s)
        local END=$((START + SECONDS))
        local CUR=$START

        while [[ $CUR -lt $END ]]
        do
                CUR=$(date +%s)
                LEFT=$((END-CUR))

                printf "\r%02d:%02d:%02d" \
                        $((LEFT/3600)) $(( (LEFT/60)%60)) $((LEFT%60))

                sleep 1
        done
        IFS="${OLD_IFS}"
        echo "        "
}

countdown "00:07:55"

[LEFT]That is exactly what I needed Thanks! At first it was counting down and then starting the capture, but I just added an "&" and she works great!

Timer

(Just wanted to include this for search purposes...)
[/LEFT]

Great little script, Corona688, just what I needed! :b:

Here's a little variant I wrote that allows you to prefix the countdown timer with a custom message:

countdown "00:07:55" Time left before entering the matrix

... which produces something like:

Time left before entering the Matrix > 00:00:08

Here's my patch:

pvdb@localhost ~ $ diff original prefix 
5c5,7
<         local ARR=( $1 )
---
>         local ARR=( $1 ) ; shift
>         IFS="${OLD_IFS}"
>         local PREFIX="$*" ; [ -n "${PREFIX}" ] && PREFIX="${PREFIX} > "
16c18
<                 printf "\r%02d:%02d:%02d" \
---
>                 printf "\r${PREFIX}%02d:%02d:%02d" \
21d22
<         IFS="${OLD_IFS}"
pvdb@localhost ~ $ _

Great idea, thanks for the patch! But since your copy/paste of it wasn't identical down to the last bit of whitespace and newline(possible disagreement on spaces per tab, etc) I'm having trouble applying it... diff -U is generally preferred these days as it gives more complete information on what's being replaced where, but a full code-listing really can't be that much larger.

But I think I see a potential problem in your use of printf: if your PREFIX variable includes control sequences like %d it will cause printf to do strange things since it will insert parameters from the commandline. Try inserting %s instead of PREFIX, and making "${PREFIX}" the first argument printf is given. This will guarantee the string is printed as 100% literals.

That will fail for 00:08:09.

Here is my version, which will work in any POSIX shell:

countdown()
(
  IFS=:
  set -- $*
  secs=$(( ${1#0} * 3600 + ${2#0} * 60 + ${3#0} ))
  while [ $secs -gt 0 ]
  do
    sleep 1 &
    printf "\r%02d:%02d:%02d" $((secs/3600)) $(( (secs/60)%60)) $((secs%60))
    secs=$(( $secs - 1 ))
    wait
  done
  echo
)

Hi cfajohnson,

Thanks for your version of the script... very enlightening!

Quick question: instead of doing a "raw" sleep...

sleep 1

your version does this instead...

    sleep 1 &
    ...
    wait

I'm assuming doing it does way minimizes the "skew" on the actual time spent in the function, ie. every cycle is as close to 1 second as possible, as opposed to 1 second + however long it takes to do all the work.

Is that correct, or is there another benefit in doing it this way?

Regards,

PVDB

That's exactly right.

Hi Corona688,

All good points you raise... here's a unified diff of the two scripts, which also addresses the issue you correctly point out with printf control characters in the prefix:

pvdb@localhost ~$ diff -U 3 original prefix 
--- original	2009-04-02 22:08:47.000000000 +0100
+++ prefix	2009-04-02 22:13:06.000000000 +0100
@@ -2,7 +2,9 @@
 {
         local OLD_IFS="${IFS}"
         IFS=":"
-        local ARR=( $1 )
+        local ARR=( $1 ) ; shift
+        IFS="${OLD_IFS}"
+        local PREFIX="$*" ; [ -n "${PREFIX}" ] && PREFIX="${PREFIX} > "
         local SECONDS=$((  (ARR[0] * 60 * 60) + (ARR[1] * 60) + ARR[2]  ))
         local START=$(date +%s)
         local END=$((START + SECONDS))
@@ -13,12 +15,11 @@
                 CUR=$(date +%s)
                 LEFT=$((END-CUR))
 
-                printf "\r%02d:%02d:%02d" \
-                        $((LEFT/3600)) $(( (LEFT/60)%60)) $((LEFT%60))
+                printf "\r%s%02d:%02d:%02d" \
+                        "${PREFIX}" $((LEFT/3600)) $(( (LEFT/60)%60)) $((LEFT%60))
 
                 sleep 1
         done
-        IFS="${OLD_IFS}"
         echo "        "
 }
 
pvdb@localhost ~/Desktop $ _

... as well as the patched script in its entirity:

function countdown
{
        local OLD_IFS="${IFS}"
        IFS=":"
        local ARR=( $1 ) ; shift
        IFS="${OLD_IFS}"
        local PREFIX="$*" ; [ -n "${PREFIX}" ] && PREFIX="${PREFIX} > "
        local SECONDS=$((  (ARR[0] * 60 * 60) + (ARR[1] * 60) + ARR[2]  ))
        local START=$(date +%s)
        local END=$((START + SECONDS))
        local CUR=$START

        while [[ $CUR -lt $END ]]
        do
                CUR=$(date +%s)
                LEFT=$((END-CUR))

                printf "\r%s%02d:%02d:%02d" \
                        "${PREFIX}" $((LEFT/3600)) $(( (LEFT/60)%60)) $((LEFT%60))

                sleep 1
        done
        echo "        "
}

Regards,

PVDB

That still fails if the arg includes 08 or 09.

Hi cfajohnson,

Yes, absolutely agree, and your version of the script is definitely the way to go, but by posting my corrected patch, I was just finishing what I started, so to speak :slight_smile:

I should have added that yours is easily fixed:

local SECONDS=$(( ${ARR[0]#0} * 3600 + ${ARR[1]#0} * 60 + ${ARR[2]#0} ))