How can I create a stopwatch in bash? - bash

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..)
?

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

If you want something simple that includes minutes, seconds, and centiseconds like a traditional stopwatch you could use 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

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"

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
}

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.

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.

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%).

Related

Output to terminal (from bash script) in precise, 1-second increments

I wrote a kitchen timer for the terminal a while back (macOS only for now, unfortunately). It runs for a specified period of time, and then exits and sounds a bell:
It does this by looping over some logic n times (where n is the number of seconds specified), adjusting the terminal output and sleeping for 1s within each loop.
I've noticed that it actually takes a small amount of time longer to complete than specified:
$ time timer 0.05
[13:18] Time expired
real 0m3.289s
user 0m0.118s
sys 0m0.098s
$ time timer 0.1
[13:18] Time expired
real 0m6.362s
user 0m0.134s
sys 0m0.127s
$ time timer 0.2
[13:19] Time expired
real 0m12.496s
user 0m0.166s
sys 0m0.183s
It looks like all the logic around each sleep 1 takes a small amount of time, and over a fifteen-minute period, those milliseconds add up. And when I set the timer to ring at repeating intervals (e.g., once every fifteen minutes indefinitely), then I can start it at 11a, have it ring at 11:15 and 11:30 etc., but by the end of the day, it's ringing at 4:07 and 4:22 or some such.
Any ideas on how to run a timer that updates the output one second at a time, but faithfully measures long spans of time?
Is this a better job for C than a bash script?
How about:
#!/bin/bash
now=$( date +%s )
remain=$(( $1 * 60 )) # supply minute(s) in integer as an argument
finish=$(( $now + $remain ))
while [ $remain -gt 0 ]; do
remain=$(( $finish - $( date +%s ) ))
mm=$(( $remain / 60 ))
ss=$(( $remain - $mm * 60 ))
printf "%02d:%02d" $mm $ss
sleep 1
echo -ne "\r"
done
# ring a bell or tell something here
It will be mostly accurate in a long term but the displayed clock occasionally skips by one second to absorb accumulated time difference. I'm not sure if the phenomenon is acceptable.

Bash - convert time interval string to nr. of seconds

I'm trying to convert strings, describing a time interval, to the corresponding number of seconds.
After some experimenting I figured out that I can use date like this:
soon=$(date -d '5 minutes 10 seconds' +%s); now=$(date +%s)
echo $(( $soon-$now ))
but I think there should be an easier way to convert strings like "5 minutes 10 seconds" to the corresponding number of seconds, in this example 310. Is there a way to do this in one command?
Note: although portability would be useful, it isn't my top priority.
You could start at epoch
date -d"1970-01-01 00:00:00 UTC 5 minutes 10 seconds" "+%s"
310
You could also easily sub in times
Time="1 day"
date -d"1970-01-01 00:00:00 UTC $Time" "+%s"
86400
There is one way to do it, without using date command in pure bash (for portability)
Assuming you just have an input string to convert "5 minutes 10 seconds" in a bash variable with a : de-limiter as below.
$ convertString="00:05:10"
$ IFS=: read -r hour minute second <<< "$convertString"
$ secondsValue=$(((hour * 60 + minute) * 60 + second))
$ printf "%s\n" "$secondsValue"
310
You can run the above commands directly on the command-line without the $ mark.
This will do (add the epoch 19700101):
$ date -ud '19700101 5 minutes 10 seconds' +%s
310
It is important to add a -u to avoid local time (and DST) effects.
$ TZ=America/Los_Angeles date -d '19700101 5 minutes 10 seconds' +%s
29110
Note that date could do some math:
$ date -ud '19700101 +5 minutes 10 seconds -47 seconds -1 min' +%s
203
The previous suggestions didn't work properly on alpine linux, so here's a small helper function that is POSIX compliant, is easy to use and also supports calculations (just as a side effect of the implementation).
The function always returns an integer based on the provided parameters.
$ durationToSeconds '<value>' '<fallback>'
$ durationToSeconds "1h 30m"
5400
$ durationToSeconds "$someemptyvar" 1h
3600
$ durationToSeconds "$someemptyvar" "1h 30m"
5400
# Calculations also work
$ durationToSeconds "1h * 3"
10800
$ durationToSeconds "1h - 1h"
0
# And also supports long forms for year, day, hour, minute, second
$ durationToSeconds "3 days 1 hour"
262800
# It's also case insensitive
$ durationToSeconds "3 Days"
259200
function durationToSeconds () {
set -f
normalize () { echo $1 | tr '[:upper:]' '[:lower:]' | tr -d "\"\\\'" | sed 's/years\{0,1\}/y/g; s/months\{0,1\}/m/g; s/days\{0,1\}/d/g; s/hours\{0,1\}/h/g; s/minutes\{0,1\}/m/g; s/min/m/g; s/seconds\{0,1\}/s/g; s/sec/s/g; s/ //g;'; }
local value=$(normalize "$1")
local fallback=$(normalize "$2")
echo $value | grep -v '^[-+*/0-9ydhms]\{0,30\}$' > /dev/null 2>&1
if [ $? -eq 0 ]
then
>&2 echo Invalid duration pattern \"$value\"
else
if [ "$value" = "" ]; then
[ "$fallback" != "" ] && durationToSeconds "$fallback"
else
sedtmpl () { echo "s/\([0-9]\+\)$1/(0\1 * $2)/g;"; }
local template="$(sedtmpl '\( \|$\)' 1) $(sedtmpl y '365 * 86400') $(sedtmpl d 86400) $(sedtmpl h 3600) $(sedtmpl m 60) $(sedtmpl s 1) s/) *(/) + (/g;"
echo $value | sed "$template" | bc
fi
fi
set +f
}
Edit : Yes. I developed for OP after comment and checked on Mac OS X, CentOS and Ubuntu. One liner, POSIX compliant command for converting "X minutes Y seconds" format to seconds. That was the question.
echo $(($(echo "5 minutes 10 seconds" | cut -c1-2)*60 + $(echo "5 minutes 10 seconds" | cut -c1-12 | awk '{print substr($0,11)}')))
OP told me via comment that he wants for "X minutes Y seconds" format not for HH:MM:SS format. The command with date and "+%s" is throwing error on (my) Mac. OP wanted to grab the numerical values from "X minutes Y seconds" format and convert it to seconds. First I extracted the minute in digit (take it as equation A) :
echo "5 minutes 10 seconds" | cut -c1-2)
then I extracted the seconds part (take it as equation B) :
echo "5 minutes 10 seconds" | cut -c1-12 | awk '{print substr($0,11)}'
Now multiply minute by 60 then add with the other :
echo $((equation A)*60) + (equation B))
OP should ask the others to check my developmental version (but working) of command before using it for automatic repeated usage like we do with cron on a production server.
If we want to run this on a log file with values in "X minutes Y seconds" format, we have to change echo "5 minutes 10 seconds" to cat file | ... like command. I kept a gist of it too if I or others ever need we can use it with cat to run on server log files with x minutes y seconds like log format.
Although off-topic (what I understood, question has not much to do with current time), this is not working for POSIX-compliant OS to get current time in seconds :
date -d "1970-01-01 00:00:00 UTC 5 minutes 10 seconds" "+%s"
It will throw error on MacOS X but work on most GNU/Linux distro. That +%s part will throw error on POSIX-compliant OS upon complicated usage. These commands are mostly suitable to get current time in seconds on POSIX compliant to any kind of unix like OS :
awk 'BEGIN{srand(); print srand()}'
perl -le 'print time'
If OP needs can extend it by generating current time in seconds and subtract. I hope it will help.
---- OLD Answer before EDIT ----
You can get the current time without that date -- echo | awk '{print systime();}' or wget -qO- http://www.timeapi.org/utc/now?\\s. Other way to convert time to second is echo "00:20:40.25" | awk -F: '{ print ($1 * 3600) + ($2 * 60) + $3 }'.
The example with printf shown in another answer is near perfect.
That thing you want is always needed by the basic utilities of GNU/Linux - gnu.org/../../../../../Setting-an-Alarm.html
Way to approach really depends how much foolproof way you need.

Subtract date & time from current date & time in bash

Lets say I have below two variables.
!#/bin/bash
MEMFILE=lock-file
date +%s > $MEMFILE
sleep 130
UPTIME= `date +%s`
I want to take the output of ( $UPTIME - $MEMFILE) in minutes and seconds.
Eg:
"Total downtime was 2 minutes and 5 seconds"
Several possibilities:
Subtracting times obtained from date:
#!/bin/bash
startdate=$(date +%s)
sleep 130
enddate=$(date +%s)
timetaken=$((enddate-startdate))
printf 'Total downtime was %d minutes and %d seconds\n' "$((timetaken/60))" "$((timetaken%60))"
The same without the external process date (since Bash 4.2):
#!/bin/bash
printf -v startdate '%(%s)T' -1
sleep 130
printf -v enddate '%(%s)T' -1
timetaken=$((enddate-startdate))
printf 'Total downtime was %d minutes and %d seconds\n' "$((timetaken/60))" "$((timetaken%60))"
The subtraction of times and computing the minutes and seconds are done using arithmetic expansion.
Using Bash's SECONDS variable (probably the best for you):
#!/bin/bash
SECONDS=0 # reset the SECONDS variable
sleep 130
timetaken=$SECONDS
printf 'Total downtime was %d minutes and %d seconds\n' "$((timetaken/60))" "$((timetaken%60))"
After being set to an integer value the special variable SECONDS is incremented each second.
Using Bash's time keyword with an appropriate TIMEFORMAT (here, we won't be able to write the elapsed time as MM minutes and SS seconds; it'll be shown in the form of MmSs, i.e., Total downtime was 2m10s).
#!/bin/bash
TIMEFORMAT='Total downtime was %0lR'
time {
# do your stuff in this block
sleep 130
}
Note that the linked answer already contains a lot of material.

Adding Date variable

I want to add the code below to my script but it's not showing the total_time value although CurrentTime is showing correctly. In this I want to change epoch time to current system time and then add 20 minutes to it.
CurrentTime=`date -d #$2`
echo "CurrentTime : $CurrentTime " >> ${LOGFILE}
Total_time=`"$CurrentTime" -d "+20 min"`
How can I do it?
Change your totaltime assignment like this:
Total_time=`date -d "$CurrentTime +20 mins"`
The reason this isn't working is that by the time you're trying to assign the value to $Total_time, your $CurrentTime variable has already been set to a time. It's not a command anymore, it's a string that is the result of a command.
Each time you want to calculate a new date, you need a new invocation of the `date` command. That's what Guru's answer provides you with, though he didn't explain why.
If what you need is to make a "base" date to which you apply modifiers, you can still do this, but I'd recommend a slightly different notation:
#!/bin/bash
start=$(date '+%s')
# do stuff
sleep 20
duration=$((`date '+%s'` - $start))
You can then use your $duration as an easier basis for other calculations, AND you can use it with relative dates:
printf "[%s] Start of job\n" "$(date -d #$start '+%Y-%m-%d %T')"
...
printf "[%s] End of job\n" "$(date -d #"$((start + duration))" '+%Y-%m-%d %T')"
Probably better to format your log files in a more standard format than date's default.

How do I calculate the previous business day in ksh shell script?

What is the most elegant way to calculate the previous business day in shell ksh script ?
What I got until now is :
#!/bin/ksh
set -x
DAY_DIFF=1
case `date '+%a'` in
"Sun")
DAY_DIFF=2
;;
"Mon")
DAY_DIFF=3
;;
esac
PREV_DT=`perl -e '($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time()-${DAY_DIFF}*24*60*60);printf "%4d%02d%02d",$year+1900,$mon+1,$mday;'`
echo $PREV_DT
How do I make the ${DAY_DIFF} variable to be transmitted as value and not as string ?
#!/bin/ksh
# GNU date is a veritable Swiss Army Knife...
((D=$(date +%w)+2))
if [ $D -gt 3 ]; then D=1; fi
PREV_DT=$(date -d "-$D days" +%F)
Here is a solution that doesn't use Perl. It works both with ksh and sh.
#!/bin/ksh
diff=-1
[ `date +%u` == 1 ] && diff=-3
seconds=$((`date +%s` + $diff * 24 * 3600))
format=+%Y-%m-%d
if date --help 2>/dev/null | grep -q -- -d ; then
# GNU date (e.g., Linux)
date -d "1970-01-01 00:00 UTC + $seconds seconds" $format
else
# For BSD date (e.g., Mac OS X)
date -r $seconds $format
fi
Well, if running Perl counts as part of the script, then develop the answer in Perl. The next question is - what defines a business day? Are you a shop/store that is open on Sunday? Saturday? Or a 9-5 Monday to Friday business? What about holidays?
Assuming you're thinking Monday to Friday and holidays are temporarily immaterial, then you can use an algorithm in Perl that notes that wday will be 0 on Sunday through 6 on Saturday, and therefore if wday is 1, you need to subtract 3 * 86400 from time(); if wday is 0, you need to subtract 2 * 86400; and if wday is 6, you need to subtract 1 * 86400. That's what you've got in the Korn shell stuff - just do it in the Perl instead:
#!/bin/perl -w
use strict;
use POSIX;
use constant SECS_PER_DAY => 24 * 60 * 60;
my(#days) = (2, 3, 1, 1, 1, 1, 1);
my($now) = time;
my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime($now);
print strftime("%Y-%m-%d\n", localtime($now - $days[$wday] * SECS_PER_DAY));
This does assume you have the POSIX module; if not, then you'll need to do roughly the same printf() as you used. I also use ISO 8601 format for dates by preference (also used by XSD and SQL) - hence the illustrated format.
This should work for Solaris and Linux. It's realy complicating on Unix that you can not use the same commandline arguments on all Unix derivates.
On Linux you can use date -d '-d24 hour ago' to get the last day
on Solaris its TZ=CET+24 date. I guess other UNIX'es works the same way as Solaris does.
#!/usr/bin/ksh
lbd=5 # last business day (1=Mon, 2=Thu ... 6=Sat, 7=Sun)
lbd_date="" # last business day date
function lbdSunOS
{
typeset back=$1
typeset tz=`date '+%Z'` # timezone
lbd_date=`TZ=${tz}+$back date '+%Y%m%d'`
}
function lbdLinux
{
typeset back=$1
lbd_date=`date -d "-d$back hour ago"`
}
function calcHoursBack
{
typeset lbd=$1
typeset dow=`date '+%u'` # day of the week
if [ $dow -ge $lbd ]
then
return $(((dow-lbd)*24))
else
return $(((dow-lbd+7)*24))
fi
}
# Main
calcHoursBack $lbd
lbd`uname -s` $?
echo $lbd_date

Resources