10

I've this silly script to track execution time of my cron jobs to know when script started and stopped.

start=$(date +'%Y-%m-%d %H:%M:%S') # This line converts to 2019-09-10 00:50:48
echo "$start"

./actual_script_runs_here

stop=$(date +'%Y-%m-%d %H:%M:%S')
echo "$stop"

This method works. But I've to calculate time it took to run the script by myself. Is it possible to calculate this in bash?

Or I could use time command. But it returns 3 different times with 3 new lines. I'd prefer to see that time result in one line together with my script result.

3 Answers3

14

You could use date +%s to get a date format that you can easily do math on (number of seconds [with caveats] since Jan 1 1970 00:00:00Z). You can also convert your ISO-format date back (possibly incorrectly, due to issues like daylight saving time) using date -d "$start" +%s. It's not ideal not only due to DST issues when converting back, but also if someone changes the time while your program is running, or if something like ntp does, then you'll get a wrong answer. Possibly a very wrong one.

A leap second would give you a slightly wrong answer (off by a second).

Unfortunately, checking time's source code, it appears time suffers from the same problems of using wall-clock time (instead of "monotonic" time). Thankfully, Perl is available almost everywhere, and can access the monotonic timers:

start=$(perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC -E 'say clock_gettime(CLOCK_MONOTONIC)')

That will get a number of seconds since an arbitrary point in the past (on Linux, since boot, excluding time the machine was suspended). It will count seconds, even if the clock is changed. Since it's somewhat difficult to deal with real numbers (non-integers) in shell, you could do the whole thing in Perl:

#!/usr/bin/perl
use warnings qw(all);
use strict;

use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);

my $cmd   = $ARGV[0];
my $start = clock_gettime(CLOCK_MONOTONIC);

system $cmd @ARGV; # this syntax avoids passing it to "sh -c"

my $exit_code = $?;
my $end       = clock_gettime(CLOCK_MONOTONIC);
printf STDERR "%f\n", $end - $start;

exit($exit_code ? 127 : 0);

I named that "monotonic-time"; and it's used just like "time" (without any arguments of its own). So:

$ monotonic-time sleep 5
5.001831                 # appeared ≈5s later
$ monotonic-time fortune -s
Good news is just life's way of keeping you off balance.
0.017800
derobert
  • 109,670
  • What issues with DST? If you convert both date strings to seconds since the epoch, you can just subtract $start from $stop and get the number of seconds the script was running from. Isn't that all the OP needs? How would DST issues enter into that? – terdon Sep 09 '19 at 17:31
  • 1
    @terdon Script starts at 01:59:59 DST. At 02:00:00 DST, daylight saving ends. It's now 01:00:00 ST. Then the script ends. Conclusion: script has run for nearly a negative hour. (This comes from trying to convert two ambiguous time stamps; since the format OP picked is local time without a time zone, it's not possible to do correctly) – derobert Sep 09 '19 at 17:33
  • Oh. Damn. I hate time zones. Fair point. – terdon Sep 09 '19 at 17:35
  • of course you could change the output format to +'%Y-%m-%d %H:%M:%S %z' (or %Z), which (I think) would make the conversion possible. But converting from human-readable time to Unixtime is just silly. To deal with time steps, I suppose something interfacing with CLOCK_MONOTONIC would be needed.. – ilkkachu Sep 09 '19 at 17:58
  • @ilkkachu Yep, I suggest if using date that you store in +%s format, then convert that for display. date -d @$start_time will do that. I had presumed time did the right thing and used CLOCK_MONOTONIC, but checking the source it appears it does not. Ugh. – derobert Sep 09 '19 at 18:09
  • @ilkkachu I added a way to get CLOCK_MONOTONIC from shell, using Perl of course :-/ – derobert Sep 09 '19 at 19:00
  • @derobert, ooh! I'm sorry I only have one upvote to give :) – ilkkachu Sep 09 '19 at 19:19
  • Thanks for the detailed answer @derobert. Very much appreciated. Now my time function moved out of my bash script. How can I use the script together with original script I have. Is it correct to use the script like this?

    ./monotonic-time /app/test.sh

    or in crontab

    * * * * * /app/monotonic-time /app/test.sh

    – Shinebayar G Sep 10 '19 at 14:32
  • @ShinebayarG yes, that's a correct way to use it (provided /app/test.sh is executable — if it isn't, you'll get an obvious error along the lines of "permission denied" or "not executable".) – derobert Sep 10 '19 at 20:37
14

In Bash, you can also use the magic variable SECONDS:

SECONDS
Each time this parameter is referenced, the number of seconds since shell invocation is returned.

so:

bash -c 'a=$SECONDS; sleep 5; b=$SECONDS; printf "%d seconds passed\n" "$((b-a))"'

outputs 5 seconds passed.

SECONDS only has full-second granularity, though, but so does date +"... %S". With GNU date you could use %N with %s to get time times in nanoseconds.

a=$(date +%s%N)
sleep 1.234
b=$(date +%s%N)
diff=$((b-a))
printf "%s.%s seconds passed\n" "${diff:0: -9}" "${diff: -9:3}"

prints:

1.237 seconds passed

Bash can't do arithmetic on floating point numbers, so you need to either use an external utility to do the calculation, or calculate in units smaller than second like I did above. (The numbers won't fit in 32 bits, so that might not work on a 32-bit system. I'm not sure if Bash uses 64 bits for arithmetic even on 32-bit systems.)

SECONDS also misbehaves if the system time is modified during the measurement period, but really you should only have the time set once during startup and then let NTP adjust the clock to stay in time.

ilkkachu
  • 138,973
  • @ikkachu , thanks for the answer. Your second example kinda what I needed. But as you mentioned actual sleep value and print output are different. For example if I run a=$(date +%s%N) sleep 1.235 b=$(date +%s%N) diff=$((b-a)) printf "%d.%d seconds passed\n" "${diff:0: -9}" "${diff: -9:3}" It says 1.241 seconds passed. I assumed it would print 1.235. Can you explain what's happening? – Shinebayar G Sep 10 '19 at 12:49
  • Update: After running example script from @derobert 's answer. I noticed that his script also doesn't exactly return 5.000 when running sleep 5, but something like 5.003167. So I assume this is linux OS behaviour. Thanks for the answer! Would you mind If I accept @derobert's answer? Cuz he mentioned other things that could happen. – Shinebayar G Sep 10 '19 at 14:28
  • 1
    @ShinebayarG, well, in both of those cases, running sleep took a few microseconds longer than the interval asked. There can be at least two reasons: first, there's some overhead in running date and sleep (they're full processes, not just system calls), and also the fact that a timed sleep (the system call) can itself take longer than asked, depending on the system load, higher priority processes running, and how the kernel schedules the processes. If you do need (even) microsecond timing, you'll need to use something other than a shell script. – ilkkachu Sep 10 '19 at 15:09
  • @ShinebayarG, and of course, as always, you should choose to accept the answer that answers your question and is most useful to you. – ilkkachu Sep 10 '19 at 15:10
0

Astonished that no-one said to use Python! For example, 5 days ago as yyyymmdd:

MYDATE=$( python -c 'from datetime import date, timedelta; 
    print( ( date.today() - timedelta(days=5) ).strftime("%Y%m%d"))' )

If you want to do things with smaller units than days, import datetime and probably use datetime.now()

nigel222
  • 317