18

I created a simple stopwatch (bash function) for counting time, but for now it's showing current time with milliseconds.

The code:

function stopwatch() {
    date +%H:%M:%S:%N
    while true; do echo -ne "`date +%H:%M:%S:%N`\r"; done;
}

I tried to change it as explained in this answer, but it works only with second since Unix Epoch.

When I used date format +%s.%N the subtraction from the answer above stopped working due to the fact that bash subtraction takes only integer.

How can I solve it and have a terminal stopwatch that prints time like so:

0.000000000
0.123123123
0.435345345
(and so on..)

?

Community
  • 1
  • 1
lewiatan
  • 1,126
  • 2
  • 21
  • 37

7 Answers7

22

One possible (& hacky) mechanism that can work for a day:

$ now=$(date +%s)sec
$ while true; do
     printf "%s\r" $(TZ=UTC date --date now-$now +%H:%M:%S.%N)
     sleep 0.1
  done

Bonus: You can press enter at any time to get the LAP times. ;-)

Note: This is a quick fix. Better solutions should be available...

watch based variant (same logic):

$ now=$(date +%s)sec; watch -n0.1 -p TZ=UTC date --date now-$now +%H:%M:%S.%N
anishsane
  • 20,270
  • 5
  • 40
  • 73
  • Won't the nanoseconds always be off? – 123 Jun 23 '16 at 09:08
  • No. Nanoseconds are on and this is even better than my old solution (in another answer). Great! (although I also added some sleep due to CPU usage :-) ) – lewiatan Jun 23 '16 at 09:32
  • @lewiatan You may aswell use milli/microseconds if you are using sleep. – 123 Jun 23 '16 at 09:50
  • @123: I just translated OP's code & added date subtraction logic. – anishsane Jun 23 '16 at 09:56
  • @anishsane Yes, i was mistaken, i thought the nanaseconds would be off due to only using seconds for the starting date, apparently it doesn't matter though. – 123 Jun 23 '16 at 10:00
  • @123 - Yes I could if they would be supported by `date` but since it's only nanoseconds and I don't want any extra calculations I prefer executing this less often. – lewiatan Jun 23 '16 at 11:24
  • 1
    This will eat up quite some resources. Adding a sleep will improve this. I'm aware that OP mentioned milliseconds. Assuming that this is for human readability (and interaction) I think a small sleep like `sleep 0.1s` might be a good compromise. – exhuma Oct 04 '18 at 10:06
  • My god. This is beautifull! – Cuauhtli Nov 01 '20 at 19:31
17

If you want something simple that includes minutes, seconds, and centiseconds like a traditional stopwatch you could use sw.

sw

Install

wget -q -O - http://git.io/sinister | sh -s -- -u https://raw.githubusercontent.com/coryfklein/sw/master/sw

Usage

# start a stopwatch from 0, save start time in ~/.sw
sw

# resume the last run stopwatch
sw --resume 
Cory Klein
  • 51,188
  • 43
  • 183
  • 243
  • To anyone looking at this after Jul, 2020. The app seems to be broken! – Karthik Nayak Jul 28 '20 at 04:58
  • 1
    I just installed it fresh on macOS Catalina 10.15.5 and it worked no problem. Feel free to file an issue on GitHub with your details @KarthikNayak. – Cory Klein Jul 29 '20 at 15:33
  • There is a pending issue with the same issue here: https://github.com/coryfklein/sw/issues/2 – Karthik Nayak Jul 30 '20 at 07:11
  • 1
    Worked for me today. – Brett Feb 02 '22 at 22:13
  • Since this technically doesn't answer the OP and a good number of us coming here are just looking for a functional terminal stopwatch, [termdown](https://github.com/trehn/termdown) has more features (if that's what one wants). Found via [this Super User answer](https://superuser.com/a/764336/988483). – joeljpa Apr 27 '23 at 09:43
9
time cat

then press Ctrl-c or Ctrl-d to stop the timer and show the time. The first number is the time.

I've further refined it into this bash alias

alias stopwatch="echo Press Ctrl-c to stop the timer; TIMEFORMAT=%R; time cat; unset TIMEFORMAT"
Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
  • 1
    plus1. Another possibility is to use `read -n1` instead of `cat`. Then Any character would terminate it. – anishsane May 13 '20 at 04:24
4

Here's a nicer function I grabbed a while ago:

function stopwatch() {
    local BEGIN=$(date +%s)
    echo Starting Stopwatch...

    while true; do
        local NOW=$(date +%s)
        local DIFF=$(($NOW - $BEGIN))
        local MINS=$(($DIFF / 60))
        local SECS=$(($DIFF % 60))
        local HOURS=$(($DIFF / 3600))
        local DAYS=$(($DIFF / 86400))

        printf "\r%3d Days, %02d:%02d:%02d" $DAYS $HOURS $MINS $SECS
        sleep 0.5
    done
}

In response to a comment, here's a version that will exit once the user presses the Enter key:

function stopwatch_with_cancel() {
    local BEGIN=$(date +%s)
    echo Starting Stopwatch...

    while true; do
        local NOW=$(date +%s)
        local DIFF=$(($NOW - $BEGIN))
        local MINS=$(($DIFF / 60))
        local SECS=$(($DIFF % 60))
        local HOURS=$(($DIFF / 3600))
        local DAYS=$(($DIFF / 86400))

        printf "\r%3d Days, %02d:%02d:%02d" $DAYS $HOURS $MINS $SECS
        read -rsN1 -t1 key
        if [ "$key" == $'\x0a' ] ;then
            # echo -e "\n [Enter] Pressed"
            break
        fi
    done
}
Keith
  • 543
  • 3
  • 8
2

Based on a gist by rawaludin:

function stopwatch() {
  local BEGIN=$(date +%s)

  while true; do
    local NOW=$(date +%s)
    local DIFF=$(($NOW - $BEGIN))
    local MINS=$(($DIFF / 60 % 60))
    local SECS=$(($DIFF % 60))
    local HOURS=$(($DIFF / 3600 % 24))
    local DAYS=$(($DIFF / 86400))
    local DAYS_UNIT
    [ "$DAYS" == 1 ] && DAYS_UNIT="Day" || DAYS_UNIT="Days"

    printf "\r  %d %s, %02d:%02d:%02d  " $DAYS $DAYS_UNIT $HOURS $MINS $SECS
    sleep 0.25
  done
}

For people who are not familiar with this: in English, only when it is 1 do we use singular -- Day. When it is 0, 2, 3, 4, 5..., we use plural "Days", so note that it is 0 Days.

nonopolarity
  • 146,324
  • 131
  • 460
  • 740
1

Here is another take on a bash stopwatch, drawing much from other answers in this thread. Ways in which this version differs from the others include:

  • This version uses bash arithmetic rather than calling bc which I found (by timing it) to be way less cpu time.
  • I have addressed the 25th-hour limitation that someone had pointed out by tacking 24 hours onto the hour part for every day elapsed. (So now I guess it's the ~31st-day limitation.)
  • I leave the cursor just to the right of the output, unlike the version in the accepted answer. That way you can easily measure laps (or more generally mark important event times) just by hitting enter, which will move the timer to the next line, leaving the time at keypress visible.
#!/bin/bash

start_time=$(date +%s)

while true; do
  current_time=$(date +%s)
  seconds_elapsed=$(( $current_time - $start_time ))
  timestamp=$(date -d"@$seconds_elapsed" -u +%-d:%-H:%-M:%-S)

  IFS=':' read -r day hour minute second <<< "$timestamp"
  hour="$(( $hour+24*($day-1) ))"

  printf "\r%02d:%02d:%02d" $hour $minute $second
  sleep 0.5
done;

Here is sample output from running stopwatch (as an executable script in the PATH) and hitting the return key at 7 and 18 seconds, and hitting Ctrl-C after about 9 minutes:

$ stopwatch
00:00:07
00:00:18
00:09:03^C
$

Notes:

  • I use the +%-d:%-H:%-M:%-S output format for date (this dashes mean "leave off any leading zero please") because printf seems to interpret digit strings with a leading zero as octal and eventually complains about invalid values.
  • I got rid of the nanoseconds simply because for my purposes I don't need beyond 1-second precision. Therefore I adjusted the sleep duration to be longer to save on compute.
Misha Akopov
  • 12,241
  • 27
  • 68
  • 82
Carl Smith
  • 710
  • 1
  • 8
  • 11
0

For the subtraction you should use bc (An arbitrary precision calculator language).

Here is the example code that fulfill your requirements:

function stopwatch() {
    date1=`date +%s.%N`
    while true; do
        curr_date=`date +%s.%N`
        subtr=`echo "$curr_date - $date1" | bc`
        echo -ne "$subtr\r";
        sleep 0.03
    done;
}

Additional sleep is added to lower the CPU usage (without it on my machine it was almost 15% and with this sleep it lowered to 1%).

lewiatan
  • 1,126
  • 2
  • 21
  • 37
  • 1
    You can certainly improve this slightly: you don't need the `curr_date` variable if you directly use: `subtr=$(date "+%s.%N-$date1" | bc)`. You're saving one subshell on each iteration! You could also put `bc` out of the loop, and then use another loop to replace the `\n` output by `bc` by a `\r`. – gniourf_gniourf Jun 23 '16 at 09:27
  • 1
    Something like: `stopwatch() { local a date1=$(date +%s.%N); while :; do date "+%s.%N-$date1"; sleep 0.03; done | bc | while read a; do printf '%s\r' "$a"; done; }`. With the `printf` you can use another format too, e.g., `printf '%.3f\r' "$a"`. – gniourf_gniourf Jun 23 '16 at 09:30