How to Create a Bash Stopwatch

How to Create a Bash Stopwatch

How to Create a Bash Stopwatch

I built my first Bash stopwatch out of pure necessity while timing how long a batch of database migrations took to run overnight. I didn’t want to install anything extra, and a stopwatch felt like exactly the kind of small, satisfying tool that Bash is perfectly suited for. In this article I’ll walk you through building one from the ground up, starting with the simplest possible version and working up to a stopwatch with lap times and a clean, readable display.

The Core Idea Behind a Bash Stopwatch

A stopwatch fundamentally just needs to do two things: record a starting timestamp, and continuously calculate the difference between “now” and that starting point. Bash gives us everything we need for this through the built-in $SECONDS variable and the date command.

$SECONDS is a special Bash variable that automatically tracks the number of seconds since the shell (or script) started running. It resets to zero whenever you assign it a new value, which makes it perfect for this purpose.

Step 1: The Simplest Possible Stopwatch

Let’s start with a minimal version. Save this as stopwatch.sh:

#!/usr/bin/env bash

SECONDS=0

echo "Stopwatch started. Press Ctrl+C to stop."

trap 'echo; echo "Elapsed time: ${SECONDS} seconds"; exit 0' SIGINT

while true; do
    echo -ne "Elapsed: ${SECONDS}s\r"
    sleep 1
done

Make it executable and run it:

chmod +x stopwatch.sh
./stopwatch.sh

You’ll see the elapsed time updating every second in place, and pressing Ctrl+C will stop it and print the final elapsed time.

Explaining How This Works

Step 2: Formatting Time as HH:MM:SS

Displaying raw seconds gets hard to read once you pass a minute or two. Let’s format it properly:

#!/usr/bin/env bash

SECONDS=0

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

trap 'echo; echo "Final time: $(format_time $SECONDS)"; exit 0' SIGINT

echo "Stopwatch started. Press Ctrl+C to stop."

while true; do
    echo -ne "Elapsed: $(format_time $SECONDS)\r"
    sleep 1
done

The format_time function does the arithmetic to convert total seconds into hours, minutes, and seconds using integer division (/) and modulo (%), then printf "%02d:%02d:%02d" pads each value to two digits with leading zeros, giving you a clean 00:03:27 style display.

Step 3: Adding Lap Times

A real stopwatch usually supports “laps” — recording a split time without stopping the overall timer. Here’s a version that lets you press Enter to record a lap:

#!/usr/bin/env bash

SECONDS=0
LAP=1

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

echo "Stopwatch started."
echo "Press ENTER to record a lap, or Ctrl+C to stop."

trap 'echo; echo "Total time: $(format_time $SECONDS)"; exit 0' SIGINT

while true; do
    read -t 1 -s && {
        echo "Lap $LAP: $(format_time $SECONDS)"
        LAP=$((LAP + 1))
    }
done

Explaining the Lap Logic

The trick here is read -t 1 -s. Normally read waits indefinitely for input, but -t 1 gives it a one-second timeout, and -s keeps it silent (no echoing of typed characters). This means the loop checks once per second whether Enter was pressed; if it was, read returns success (exit code 0), and the && runs the block that prints the lap. If a second passes with no input, read times out, returns a non-zero exit code, and the loop just continues — effectively giving us a non-blocking, one-second polling stopwatch.

Step 4: A Countdown-Style Progress Bar Variant

Sometimes I want a visual bar rather than just numbers, especially when timing something with a rough expected duration. Here’s a stopwatch with a simple animated progress indicator:

#!/usr/bin/env bash

SECONDS=0
SPINNER='/-\|'

trap 'echo; echo "Final time: ${SECONDS}s"; exit 0' SIGINT

i=0
while true; do
    i=$(( (i + 1) % 4 ))
    printf "\r[%c] Elapsed: %ds " "${SPINNER:$i:1}" "$SECONDS"
    sleep 0.25
done

This cycles through the characters / - \ | to create a simple spinning animation alongside the elapsed seconds, updating four times per second for a smoother feel than a plain number ticking up.

Real-World Use Cases

Automation Example: Wrapping Another Command with Timing

A practical variant is timing how long an arbitrary command takes, then reporting it in a friendly format:

#!/usr/bin/env bash

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

SECONDS=0
"$@"
STATUS=$?

echo "Command finished in $(format_time $SECONDS) with exit code $STATUS"

Usage:

./timed_run.sh ./backup_script.sh

This is genuinely one of my most-used personal scripts — I wrap any long command with it when I’m curious how long something takes, without needing to install time alternatives or parse time‘s sometimes awkward output format.

Best Practices

Optimization Tips

Troubleshooting Common Issues

The elapsed time display doesn’t update correctly, or lines stack up instead of overwriting — Make sure you’re using echo -ne (or printf without a newline) along with \r, and that your terminal supports carriage return behavior properly; some non-interactive contexts like log files won’t render \r visually.

Ctrl+C doesn’t stop the script cleanly — Confirm your trap is registered before the loop starts, and double-check you haven’t accidentally overridden it later in the script.

Time appears to drift over long runs — sleep 1 doesn’t account for the time the rest of the loop body takes to execute, so over very long durations small drift can accumulate. For precision-critical timing, calculate elapsed time from an actual start timestamp (date +%s) rather than accumulating sleep intervals.

Common Mistakes to Avoid

Frequently Asked Questions

Can I get millisecond precision in a Bash stopwatch? Yes, but you’ll need date +%s%N for nanosecond-resolution timestamps and calculate differences yourself, since $SECONDS only has one-second granularity.

Why does my stopwatch drift by a few seconds over a long run? Because sleep 1 doesn’t account for the small amount of time your loop body itself takes to execute. For long-running or precision-sensitive stopwatches, base your elapsed time calculation on the difference between the current timestamp and a fixed start timestamp rather than accumulating sleep calls.

Can I run this stopwatch in the background while doing other terminal work? Yes, run it with & to background it, though you’ll lose the live in-place display since it competes with your terminal’s other output. A better approach for background timing is to just record start and end timestamps and calculate the difference afterward.

How do I add a pause/resume feature? You’d need to track accumulated elapsed time separately from the current run segment, pausing by recording the current elapsed time and stopping the loop, then resuming by resetting $SECONDS to 0 and adding the previously accumulated time when displaying.

Summary

A Bash stopwatch is a small project, but it touches on several genuinely useful Bash concepts: the $SECONDS built-in, trap for graceful interruption, printf for clean formatted output, and non-blocking input handling with read -t. Starting from the basic version, you can extend it with lap times, spinner animations, or wrap it around other commands to time your everyday scripts. It’s one of those small tools that, once built, ends up getting used far more often than you’d expect.

References

Exit mobile version