Subtract date & time from current date & time in bash - 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.

Related

Convert asterisk uptime string to date

how can i convert "System uptime: 2 days, 9 hours, 16 minutes, 5 seconds" to a bash date?
I want to compare this date with another one.
Thx in advance.
Here you find a robust way that will convert your duration into the total number of seconds. This is robust as it does not need to have all the quantities listed. I.e. the output can be any of the following:
System uptime: 16 minutes, 2 seconds
System uptime: 12 days, 3 seconds
System uptime: 15minutes,12hours
Create a file convert.awk which contains
BEGIN { a["seconds"]=1; a["minutes"]=60
a["hours"]=3600; a["days"]=86400;
a["weeks"]=7*86400 }
{ tmp=tolower($0) }
{ while (match(tmp,/[0-9]+[^a-z]*[a-z]+/)) {
u=substr(tmp,RSTART,RLENGTH); q=u+0; gsub(/[^a-z]/,"",u)
t+=q*a[u]
tmp=substr(tmp,RSTART+RLENGTH)
}
print t
}
Run the following command:
$ echo "System uptime: 2 days, 9 hours, 16 minutes, 5 seconds" | awk -f convert.awk
206165
$ echo "System uptime: 2 weeks, 9 days, 16 minutes, 5 seconds" | awk -f convert.awk
1988165
How does this work:
When we read a line, eg.
tmp="system uptime: 16 minutes, 2 seconds"
The match command will search for any substring which starts with numbers and ends with a character string. match will automatically set the variables RSTART and RLENGTH to the position of the found substring and the length of it. This way, the command u=substr(tmp,RSTART,RLENGTH) will set
u="16 minutes"
You can now just do an arhtmetic operation on this, to extract the first number. This works with awk as it will convert any string starting with numbers into a number by ignoring anything after it. So q=u+0 creates
q=16
Finally, we remove anything which is not a character from u leading to
u="minutes"
The next step we redefine tmp by removing whatever we already processed.
tmp=", 2 seconds"
and start the process all over.
Assuming that the message will always include all the date components at fixed places, Unix timestamp (seconds past epoch) can be calculated using.
# Set t from command, or file.
t="System uptime: 2 days, 9 hours, 16 minutes, 5 seconds"
# Split into words
read -a p <<< "$t"
# Uptime in seconds
uptime=$((p[2]*24*60*60+p[4]*60*60+p[6]*60+p[8]))
now=$(date '+%s')
up_ts=$((now-uptime))
echo "Timestamp: $up_ts"
# Convert to human readable date.
up_date=$(date +'%Y-%m-%d %H:%M:%S' -d "#$up_ts")
echo "Date: $up_date"
You can also use bash built-in date formatting on the 'up_ts'.
If the format of the uptime message depends on the values (e.g., days is not included if uptime < 1 day), additional parsing will be needed.
Note that the uptime command gets the data from /proc/uptime
So one approach is to get the first field value which is the total elapsed time in seconds and feed it to date command:-
here is an example:-
$ cat /proc/uptime
8743161.15 7738916.50
$ date -d'-8743161.15 seconds'
Sat Jul 6 06:00:37 EDT 2019

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.

Calculate time (h:m:s or m:s) difference in bash

I want to display the duration of a command in my script. Depeding the duration I want somethng like this: 12 minutes 04 seconds or 01 hours 15 minutes 02 seconds. Always with a leading zero.
I tried different things I found here, but didn't get the result.
BTW: that's my first try in the bash.
#!/bin/bash
DATE1=$(date +%s)
# ... your commands
DATE2=$(date +%s)
DIFF=$(awk -vdate1=$DATE1 -vdate2=$DATE2 'BEGIN{print strftime("%H hour %M minutes %S seconds",date2-date1,1)}')
echo $DIFF
Time is converted to seconds and stored in variables DATE1 and DATE2
pre-condition DATE2 > DATE1
DATE1=$(date +%s)
# ... your commands
DATE2=$(date +%s)
strftime is used to get time-diff in seconds and formatted
1 is passed as 3rd argument as UTC Flag
strftime("%H hour %M minutes %S seconds",date2-date1,1)
You could use the time executable (not the bash builtin because the external program features the format option -f and delivers the time as [hours:]minutes:seconds and I am not in the mood right now to wait an hour to find out how the builtin shows us the hours :) ) and awk like this (using the example sleep 2):
/usr/bin/time -f "%E" sleep 2 2> >(
awk -F : '
{ if(FN>2) printf("%02d hours %02d minutes %02d seconds\n", $1, $2, $3)
else printf("%02d minutes %02d seconds\n", $1, $2)
}'
)
Here we use /usr/bin/time with its -f option. Then we pipe the output of stderr into awk splitting the string at :. (time writes to stderr, thus we need the 2> to redirect stderr into the >( awk ... ) filter.
The awk filter decides on the number of fields in NF what printf statement it is using.

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

How can I echo minutes since midnight in any timezone using the date command on OS X?

Here's what isn't working:
> echo $(( ($(date +%s) - $(date +%s -d$(date +%Y-%m-%d))) / 60 ))
date: illegal time format
usage: date [-jnu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
[-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
I've also tried:
> echo $(date +%s -f%Y-%m-%d $(date +%Y-%m-%d))
The following works, but only with a fixed UTC offset that will break during standard time:
> echo $[ ( ( $(date "+%s") - 28800 ) % 86400 ) / 60 ]
Reference: OS X date manual
The number of minutes (or seconds) since midnight can be computed directly from a time alone.
echo $(( $(date "+10#%H * 60 + 10#%M") )) # Minutes since midnight
echo $(( $(date "+(10#%H * 60 + 10#%M) * 60 + 10#%S") )) # Seconds since midnight
Note that this requires only minimal support from date, so will work with either GNU or BSD date.
This works by having date output a string which can be passed directly to the shell's arithmetic expression construct.
Thanks to Petesh for pointing out the need to force numbers with leading zeros to be treated as decimal.
The problem with your first attempt is that you're trying to apply a gnu date option to the BSD date that's on OSX. This has caught me out a lot as I've tried to make scripts compatible between both platforms.
One way is:
seconds_now=$(date +%s)
seconds_midnight=$(date -j -f'%Y-%m-%d %H:%M:%S' "$(date +%Y-%m-%d) 00:00:00" +%s)
minutes_now=$(((seconds_now - seconds_midnight) / 60))
You have to use the full format for the time, otherwise date takes the actual hours, minutes and seconds of the current time, which is not what you want.
Another way is:
Use date +%z to get the offset from UTC, and apply it to the number of minutes past midnight on the day.
offset=$(date +%z) # get TZ offset as [+-]<HH><MM> - for *now*
sign=${offset:0:1} # get sign
hours=${offset:1:2} # get hours
mins=${offset:3:2} # get minutes
minoff=$((10#$hours * 60 + 10#$mins)) # offset in minutes from UTC
from_midnight_utc_mins=$((($(date +%s) % 86400) / 60))
from_midnight_local=$(($from_midnight_utc_mins $sign $minoff))
It's seriously gack, though.
I use 10# for all the numbers in the minoff calculation to prevent the case where two digit numbers with a leading 0 are interpreted as octal, which can yield miscalculations/errors.

Resources