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

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.

Related

Foreach hour:min:sec.ms

I would like to know how can I parse an foreach specifying hours, minutes, seconds and milliseconds. Something like this:
DURATION=00:14:17.82 // (00 hours, 14 minutes, 17 seconds and 82ms)
for ((i=1;i<=DURATION;i++)); do
// output the hour, minute, second and ms here
done
I want to iterate every milisecond, second, minute, hour! Like this:
00:14:17.82
00:14:17.83
00:14:17.84
00:14:17.85
...
00:14:18.01
00:14:18.02
...
01:00:10.15
01:00:10.16
01:00:10.17
...
I just know creating this for seconds and just seconds! But how can I make for hours, minutes, seconds and ms? Thank you so much!
NOTE: The following ideas assume a max DURATION of 23:59:59:99 or 24:00:00.00; upper ranges can certainly be increased but keep in mind the extra overhead (cpu and/or memory) for brace expansions, especially if generating huge ranges that dwarf DURATION (ie, generating large volumes of timestamps that you'll never process).
If you're going to manually enter the value for DURATION then you could manually enter the following brace expansion series:
for i in {00..00}:{00..14}:{00..17}.{00..82}
do
echo $i
done
NOTE: This relatively small set of of brace expansions was almost instantaneous and took up ~6 MB (RAM) on my system.
This generates:
00:00:00.00
00:00:00.01
00:00:00.02
00:00:00.03
00:00:00.04
... snip ...
00:14:17.78
00:14:17.79
00:14:17.80
00:14:17.81
00:14:17.82
Alternatively:
DURATION=00:14:17.82
for i in {00..23}:{00..59}:{00..59}.{00..99}
do
[[ "${i}" > "${DURATION}" ]] && break
# do other stuff
done
NOTE:
on my system this took about about 10 seconds and 1.6 GB (RAM) to expand the series of brace expansions before the loop was processed
this particular set of brace expansions creates 8.64 million timestamps but ...
we break out of the loop after processing ~ 86K timestamps (or ~1% of the timestamps generated by the brace expansions) so ...
we wasted a lot of cpu cycles and RAM generating timestamps that we never got around to using
Another idea is to write a 4-deep set of for loops and break out once we hit 00:14:17.82, eg:
DURATION='00:14:17.82'
for a in {00..23}; do
for b in {00..59}; do
for c in {00..59}; do
for d in {00..99}; do
[[ "${a}:${b}:${c}.${d}" > "${DURATION}" ]] && break 4
# do other stuff here
done
done
done
done
NOTE: While each brace expansion is fast (and uses a miniscule amount of RAM), the nested/repeated expansions is going to eat up a lot of time the longer it takes to get to ${DURATION}; however, unlike the previous ideas this one will at least let you start processing your timestamp values immediately, with intermittent (brief) delays as a new brace expansion is performed.
Of course we could just skip the brace expansion overhead and use 'normal' for loops, eg:
DURATION='00:14:17.82'
for ((a=0; a<=23; a++)); do
for ((b=0; b<=59; b++)); do
for ((c=0; c<=59; c++)); do
for ((d=0; d<=99; d++)); do
printf -v ts "%02d:%02d:%02d.%02d" ${a} ${b} ${c} ${d}
[[ "${ts}" > "${DURATION}" ]] && break 4
# do other stuff here
done
done
done
done
NOTES:
this eliminates the cpu/time/RAM overhead of the brace expansions
assumes you have a newer version of bash that supports printf -v
And for the masochists:
DURATION='00:14:17.82'
for ((i=0; i<8640000; i++)); do
hr=$(( ( i / 100 / 60 / 60 ) % 24 ))
min=$(( ( i / 100 / 60 ) % 60 ))
sec=$(( ( i / 100 ) % 60 ))
msec=$(( i % 100 ))
printf -v ts "%02d:%02d:%02d.%02d" ${hr} ${min} ${sec} ${msec}
[[ "${ts}" > "${DURATION}" ]] && break
# do other stuff
done
Just iterate over milliseconds and convert to the representation you want upon use. First, we write conversion functions:
millis_to_human() {
local h m s ms
# calculate all those hours, minutes, seconds...
((
ms = $1,
s = ms / 100,
ms %= 100,
m = s / 60,
s %= 60,
h = m / 60,
h %= 60,
1
))
printf "%02d:%02d:%02d.%02d\n" "$h" "$m" "$s" "$ms"
}
human_to_millis() {
# Match with regex
[[ $1 =~ ([0-9]*):([0-9]*):([0-9]*).([0-9]*) ]]
# Using 10# so that the number is always in base 10, even with leading 0.
echo $(( ( (
10#${BASH_REMATCH[1]} * 60 +
10#${BASH_REMATCH[2]} ) * 60 +
10#${BASH_REMATCH[3]} ) * 100 +
10#${BASH_REMATCH[4]} ))
}
Then just:
duration='00:14:17.82'
duration_ms=$(human_to_millis "$duration")
for ((i = 1; i <= duration_ms; ++i)); do
millis_to_human "$i"
done

Reading current time and increment 1 min for next 2 hrs and run some commands

I have to read current system time and increment 1 min for next 2 hrs. I need to run few commands continuously for 2 hrs with 1 min interval.
for example: current time : 8:30 PM i need to run for next 2 hrs for 8.30, 8.31.... so on for 2 hrs. I am expecting this run should print 120 output results. please help.
Need this in shell script.
Here's a quick and dirty answer, if you don't mind hard coding 120 minutes in the for expression:
for min in {1..120}
do
command & # launch in background to minimize drift
sleep 60
done
Alternatively, modify crontab entry (avoid all drift considerations):
crontab -l > /tmp/oldcron.$$
crontab << EOF
$(crontab -l )
*/2 * * * * command # run command every 2 minutes forever
EOF
sleep $(( 2 * 60 * 60 )) # sleep 2 hours
crontab < /tmp/oldcron.$$
The above probably suffers by an off-by-1 type of error. You may need to add 59 seconds to the sleep.
#!/usr/bin/env bash
count=0
max=120 # 2 * 60 minutes
while [ true ]; do
((count++))
sleep 60
# terminal -e command
if (( $count > $max )); then
break
fi
done

How can I create a stopwatch in 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%).

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.

Print elapsed time in secs with three trailing digits

For a framework I'm writing I would like to measure how much a piece of (bash) code takes to execute and then print the elapsed time in seconds.
For measuring, I do the following:
start=$(date +%s%N)
# do something here
elapsed_time=$(($(date +%s%N) - start))
This gives me the elapsed seconds concatenated with the elapsed nanos. If I now divide this by 1000000, I'll get the time in ms
elapsed_time_in_ms=$(($elapsed time / 1000000))
This is all nice and seems to work but the real problem is that I want to print it in this format:
12.789s
where before the . are the seconds and after the dot are last 3 digits of the ms value.
How would I achieve something like that?
EDIT
I am aware that the time would probably not of much use, still I would like to implement this (even if only for cosmetic reasons :-)).
EDIT 2
For anyone facing the same problem:
In the end I've chosen to use time as it doesn't require a fork and seems to be the most portable solution.
Have a look at the it function and the global total_elapsed_time variable here to see how I implemented this.
You can use bc command:
elapsed_time_in_ms=$(echo "scale=3;$elapsed_time/1000000" | bc)
The scale basically sets the number of digits you want after the .
This can also be done using shell parameter expansion or via printf. For example, running the following two sequences one after the other printed out 1349883230.715 and 1349883230.721 in one run, and 1349884003.025 and 1349884003.032 in another. (The %N date format fills with leading zeroes.)
s=$(date +%s.%N); s=${s%??????}; echo $s
t=$(printf "%20.3f" $(date +%s.%N)); echo $t
As mentioned in man bash under Pattern Matching, special pattern character ? matches any single character. As mentioned under Parameter Expansion, the form
${parameter%word}
removes a matching suffix pattern: “If the pattern matches a trailing portion ... the expansion is the expanded value of parameter with the shortest matching pattern (the "%" case) or the longest matching pattern (the "%%" case) deleted.”
Just cut the digits after the first three off from the nanoseconds.
printf "%d.%.3ss\n" date +%S date +%N # It might not be terribly unwise to run date only once, btw.
-- print a digit followed by a dot, then treat the next argument as a string and print only the first three characters.
$ i=0; while [ $i -lt 9 ];do i=$((i+1)); sleep 0.1; \
printf "%d.%.3ss\n" `date +%S` `date +%N` ;done
33.917s
34.025s
34.133s
34.240s
34.348s
34.457s
34.566s
34.674s
34.784s
As using bc implie forks which take time, I prefer this pure bash solution:
start=$(date +%s%N);sleep 1.1;elapsed_time=$(($(date +%s%N) - start))
_Z9=000000000
[ ${#elapsed_time} -lt 9 ] && \
elapsed_time=${_Z9:0:9-${#elapsed_time}}$elapsed_time;
printf "%.3fs\n" ${elapsed_time:0:${#elapsed_time}-9
}.${elapsed_time:${#elapsed_time}-9}
print:
1.107s
For that kind of things, I've wrote a little bash source file that will do
the job quickly.
It use on Linux /proc/timer_list, or on Linux /proc/uptime or date +%s%n not well tested on other systems (feed-back would be welcome ;).
It use two counters (one for each invocation and the other as an overall counter) and accept some arguments (read comments):
. elap.bash
elap -R ; for i in {1..10};do sleep .1; elap Counter $i;done;elap -t total
0.110488478 Counter 1
0.111014783 Counter 2
0.117158015 Counter 3
0.112897232 Counter 4
0.111928207 Counter 5
0.108822248 Counter 6
0.113464053 Counter 7
0.117487421 Counter 8
0.115716626 Counter 9
0.110493433 Counter 10
0.008513430 1.137983926 total
And, as bc could be useful:
time echo 'scale=1000;pi=4*a(1);0'|bc -l
0
real 0m1.590s
user 0m0.768s
sys 0m0.008s
or
elap -R;elap BC answer is: $(echo 'scale=1000;pi=4*a(1);0'|bc -l )
1.539957483 BC answer is: 0
Using this script with /proc/timer_list could do:
elap -R ; for i in {1..10};do elap Counter $i;done;elap -t total
0.001299574 Counter 1
0.001574097 Counter 2
0.005771637 Counter 3
0.001428803 Counter 4
0.010423721 Counter 5
0.004037965 Counter 6
0.001392464 Counter 7
0.008092812 Counter 8
0.001634280 Counter 9
0.001365652 Counter 10
0.008201473 0.045222478 total
While same script whithout access to /proc, using fork to date +%s%N give:
elap -R ; for i in {1..10};do elap Counter $i;done;elap -t total
0.012148259 Counter 1
0.013415551 Counter 2
0.008279329 Counter 3
0.013700332 Counter 4
0.012837796 Counter 5
0.015562929 Counter 6
0.008062369 Counter 7
0.016810494 Counter 8
0.011537439 Counter 9
0.009731194 Counter 10
0.012959840 0.135045532 total
Where we could see that fork have a cost (near 1/100th sec in this case).
Well, finally, for matching exact format for SO question this little script could be patched, for sample in this way:
eval "$(sed -e < elap.bash '
/6d/{ s/6d.%/10.3f/g;p;N;
:a;
s/^.*\n//g;N;s/" \\\n[ \t]*"/./; p;
s/^.*//g; N;/elaP_elap2/ba; }')"
elap -R ; for i in {1..10};do elap Counter $i;done;elap -t total
0.001s Counter 1
0.006s Counter 2
0.007s Counter 3
0.004s Counter 4
0.003s Counter 5
0.002s Counter 6
0.001s Counter 7
0.001s Counter 8
0.006s Counter 9
0.002s Counter 10
0.004s 0.038s total
38s total

Resources