How to Create a Bash Countdown Timer

How to Create a Bash Countdown Timer

How to Create a Bash Countdown Timer

I built my first countdown timer script to remind myself to stand up and stretch every 25 minutes while working through a big refactor. It sounds trivial, but a well-built countdown timer that runs right in the terminal ended up being one of the small tools I use almost every day. In this article I’ll show you how to build one, starting from the simplest version and working up to a full Pomodoro-style timer with notifications.

The Core Idea

A countdown timer is essentially the mirror image of a stopwatch: instead of counting up from zero, you start with a target duration and count down to zero, then trigger some kind of alert. The building blocks are simple — a loop, arithmetic, and sleep — but there are a few details worth getting right so the timer feels smooth and accurate.

Step 1: A Basic Countdown Timer

Save this as countdown.sh:

#!/usr/bin/env bash

SECONDS_LEFT="${1:-60}"

while [ "$SECONDS_LEFT" -gt 0 ]; do
    echo -ne "Time remaining: ${SECONDS_LEFT}s\r"
    sleep 1
    SECONDS_LEFT=$((SECONDS_LEFT - 1))
done

echo -e "\nTime's up!"

Run it with a duration in seconds:

chmod +x countdown.sh
./countdown.sh 30

If you don’t pass an argument, it defaults to 60 seconds thanks to ${1:-60}.

Explaining the Basic Script

Step 2: Formatting as MM:SS

For anything longer than a minute or two, raw seconds are hard to read at a glance:

#!/usr/bin/env bash

TOTAL_SECONDS="${1:-300}"

format_time() {
    local total=$1
    printf "%02d:%02d" $((total / 60)) $((total % 60))
}

while [ "$TOTAL_SECONDS" -gt 0 ]; do
    echo -ne "Time remaining: $(format_time $TOTAL_SECONDS)\r"
    sleep 1
    TOTAL_SECONDS=$((TOTAL_SECONDS - 1))
done

echo -e "\nTime's up!"

Run it for a 5-minute countdown:

./countdown.sh 300

Step 3: Accepting Human-Readable Input

Typing raw seconds is inconvenient for longer durations. I like accepting input like 5m or 1h30m:

#!/usr/bin/env bash

parse_duration() {
    local input="$1"
    local total=0
    local hours minutes seconds

    if [[ "$input" =~ ([0-9]+)h ]]; then
        hours="${BASH_REMATCH[1]}"
        total=$((total + hours * 3600))
    fi
    if [[ "$input" =~ ([0-9]+)m ]]; then
        minutes="${BASH_REMATCH[1]}"
        total=$((total + minutes * 60))
    fi
    if [[ "$input" =~ ([0-9]+)s ]]; then
        seconds="${BASH_REMATCH[1]}"
        total=$((total + seconds))
    fi
    if [[ "$total" -eq 0 && "$input" =~ ^[0-9]+$ ]]; then
        total="$input"
    fi

    echo "$total"
}

format_time() {
    local total=$1
    printf "%02d:%02d:%02d" $((total / 3600)) $(((total % 3600) / 60)) $((total % 60))
}

TOTAL_SECONDS=$(parse_duration "${1:-5m}")

while [ "$TOTAL_SECONDS" -gt 0 ]; do
    echo -ne "Time remaining: $(format_time $TOTAL_SECONDS)\r"
    sleep 1
    TOTAL_SECONDS=$((TOTAL_SECONDS - 1))
done

echo -e "\nTime's up!"

Now you can run:

./countdown.sh 1h30m
./countdown.sh 25m
./countdown.sh 45s

How the Duration Parsing Works

This is the part I find most interesting to explain. [[ "$input" =~ ([0-9]+)h ]] uses Bash’s regex matching operator =~ to check whether the input contains a number immediately followed by h. If it matches, the captured group (the digits) is available in the special array BASH_REMATCH, where index 1 holds the first captured group. I repeat this check for hours, minutes, and seconds independently, accumulating the total in a single variable. The final fallback handles plain numeric input (like 30) with no suffix at all, treating it as raw seconds for backward compatibility with the earlier version of the script.

Step 4: Adding an Audible and Visual Alert

A silent countdown that finishes while you’re not looking at the terminal isn’t very useful. Let’s add an alert:

#!/usr/bin/env bash

TOTAL_SECONDS="${1:-300}"

format_time() {
    printf "%02d:%02d" $(($1 / 60)) $(($1 % 60))
}

while [ "$TOTAL_SECONDS" -gt 0 ]; do
    echo -ne "Time remaining: $(format_time $TOTAL_SECONDS)\r"
    sleep 1
    TOTAL_SECONDS=$((TOTAL_SECONDS - 1))
done

echo -e "\nTime's up!"

# Terminal bell, repeated 3 times
for i in {1..3}; do
    echo -ne "\a"
    sleep 0.3
done

# Optional desktop notification (Linux with notify-send installed)
if command -v notify-send &> /dev/null; then
    notify-send "Countdown Timer" "Time's up!"
fi

The \a escape sequence triggers the terminal bell, which most terminal emulators play as an audible beep or visual flash. The command -v notify-send &> /dev/null check verifies whether the notify-send utility is available before trying to use it, so the script doesn’t error out on systems without a desktop notification daemon.

Step 5: A Pomodoro-Style Work/Break Timer

Once the basic countdown worked reliably, I extended it into a simple Pomodoro timer, cycling between work and break periods:

#!/usr/bin/env bash

WORK_MIN="${1:-25}"
BREAK_MIN="${2:-5}"
CYCLES="${3:-4}"

countdown() {
    local seconds=$1
    local label=$2
    while [ "$seconds" -gt 0 ]; do
        printf "\r%s: %02d:%02d " "$label" $((seconds / 60)) $((seconds % 60))
        sleep 1
        seconds=$((seconds - 1))
    done
    echo -e "\n$label finished!"
}

for ((i = 1; i <= CYCLES; i++)); do
    echo "=== Cycle $i of $CYCLES ==="
    countdown $((WORK_MIN * 60)) "Work"
    echo -ne "\a"
    if [ "$i" -lt "$CYCLES" ]; then
        countdown $((BREAK_MIN * 60)) "Break"
        echo -ne "\a"
    fi
done

echo "All cycles complete. Great work!"

Run a default Pomodoro session (25 min work, 5 min break, 4 cycles):

./pomodoro.sh

Or customize it:

./pomodoro.sh 50 10 3

Real-World Use Cases

Automation Example: Countdown Before a Destructive Action

I use this pattern before any script that does something irreversible, like deleting files or dropping a database table, to give myself one last chance to cancel:

#!/usr/bin/env bash

echo "This will delete all files in /tmp/old_backups. Press Ctrl+C to cancel."

for i in {5..1}; do
    echo -ne "Proceeding in $i...\r"
    sleep 1
done

echo -e "\nProceeding with deletion."
rm -rf /tmp/old_backups/*
echo "Done."

Best Practices

Optimization Tips

Troubleshooting Common Issues

The terminal bell doesn’t make any sound — Many terminal emulators disable the audible bell by default and use a visual flash instead, or disable it entirely; check your terminal’s preferences/settings for “bell” behavior.

notify-send command not found — This utility is part of libnotify-bin on Debian/Ubuntu systems; install it with sudo apt install libnotify-bin, or skip that step on systems without a desktop environment.

The countdown “jumps” or skips numbers — This usually happens if something inside the loop takes noticeable time to execute; keep the loop body minimal, and rely on sleep 1 alone for timing rather than adding extra slow operations inside the loop.

Duration parsing doesn’t work as expected for inputs like “90s” — Verify your regex patterns account for the exact suffixes you’re testing; the parsing function shown above expects lowercase h, m, s suffixes specifically.

Common Mistakes to Avoid

Frequently Asked Questions

Can I pause and resume a Bash countdown timer? Not natively within a simple loop-based script, since Bash doesn’t have built-in pause/resume for a running loop. You’d need to trap a signal (like SIGTSTP for Ctrl+Z) and track remaining time in a variable that persists across the pause.

How accurate is a Bash-based countdown timer over long durations? Reasonably accurate for durations of a few hours, though minor drift can occur since sleep 1 doesn’t perfectly account for the small overhead of the rest of the loop body. For mission-critical timing, compare against an absolute end timestamp calculated with date +%s rather than purely decrementing a counter.

Can I run multiple countdown timers at once? Yes, run each in the background with &, though their terminal output will interleave unless you redirect each one’s output to a separate location, such as a log file or a separate terminal pane.

How do I make the countdown timer send an email or Slack message when it finishes instead of a desktop notification? Replace or supplement the notify-send call with a curl request to a webhook (for Slack) or a call to mail/sendmail (for email), triggered once the countdown loop exits.

Summary

A countdown timer is a small, satisfying script to build, and it introduces some genuinely useful Bash techniques along the way — parameter expansion with defaults, regex matching with BASH_REMATCH, formatted output with printf, and simple notification integration. From a basic countdown to a full Pomodoro-style work/break cycle, this is exactly the kind of tool that earns a permanent spot in your personal scripts folder once you’ve built it.

References

Exit mobile version