5

Based on this answer, to measure the time in my bash scripts I can use:

start_time="$(date -u +%s.%N)"
sleep 5
end_time="$(date -u +%s.%N)"

elapsed="$(bc <<<"$end_time-$start_time")" echo "Total of $elapsed seconds elapsed for process"

And it works great. However, since I have a lot of script files, I would love to simplify and DRY code. For example I want to achieve this code:

. /time.sh

start_time=now sleep 5 end_time=now

measure_time

In other words, I want to have another script named time.sh that can be included anywhere and it should have two functions. One to replace "$(date -u +%s.%N)" part, and one to replace "$(bc <<<"$end_time-$start_time")" part.

I tried to create these functions, but then I realized that in the shell, we couldn't return values from functions. We can only return numbers.

Is there a way to simplify the code above, to the code below?

  • 3
    You can "return" values in a function by using echo and assign the output of that function to a variable. For example if you have: hello() { sleep 1; echo hi; } And you use: value=$(hello) ; echo $value The output will be hi – Edgar Magallon Sep 11 '22 at 07:28
  • 1
    Or you can try passing a function as parameter to another function: https://stackoverflow.com/questions/5672289/bash-pass-a-function-as-parameter – Edgar Magallon Sep 11 '22 at 07:37
  • In bash, you can get the elapsed time more directly by manipulating the builtin SECONDS variable - see for example Time elapsed of a bash script (days/minutes/seconds) – steeldriver Sep 11 '22 at 12:41
  • @steeldriver but not the nanoseconds the OP seems to want – Chris Davies Sep 11 '22 at 13:41
  • Have you thought about using time inside a script (also on a function)? See type time, help time and man time. General use time MyExecutableScript.sh or time /bin/bash MyScript.sh. In your script can be something like MyFunction(){....} then time MyFunction or A="$((time MyFunction) 2>&1)". Note that you need a subshell (...) to redirect the output of time. – Hastur Sep 11 '22 at 15:53
  • One huge simplification would be to use seconds without nanoseconds. Collect the seconds-since-the epoch timestamp with date -u +%s just before the start and just after the end of a command, and the shell can do the integer subtraction to give you the elapsed time. No need to call bc to do floating point operations. But if sub-second precision is what you need, it's what you need. – Sotto Voce Sep 11 '22 at 19:01
  • @SottoVoce Another huge simplification would be to use the shell SECONDS variable. No external executable to call, less cpu time wasted. – QuartzCristal Sep 12 '22 at 05:50

4 Answers4

5

In latest versions of bash (5.0+), there is a variable that give the time with precision of microseconds (6 fractional digits). If that is enough precision for your uses, you may try this script:

#!/bin/bash --
time_diff(){ 
    local elapsed="$(bc <<<"${send}-${start}")"
    echo "Total of $elapsed seconds elapsed for process";
 }

start=$EPOCHREALTIME sleep 5 end=$EPOCHREALTIME

time_diff

This script is faster to get time values than calling an external executable (date) to do the same.

5

What is wrong with time ./script.sh ?

Simple example:-

time sleep 5

real    0m5.001s
user    0m0.001s
sys 0m0.000s
Jeremy Boden
  • 1,320
2

In its simplest form, this sounds like what you're after.

now(){
  date -u +%s.%N
}

measure_time(){ local elapsed="$(bc <<<"$end_time-$start_time")" echo "Total of $elapsed seconds elapsed for process" }

start_time="$(now)" sleep 5 end_time="$(now)"

measure_time

You could make the now function accept a named variable and adapt the measure_time function to handle that, but it would be an unnecessary complication if your use case is to track one process at a time.

Having said that, here's an iteration in that direction. start and end are arbitrary labels, so you could call them what ever suits, if you were tracking multiple events.

stamp(){
  time[$1]="$(date -u +%s.%N)"
}

measure_time(){ local elapsed="$(bc <<<"${time[$2]}-${time[$1]}")" echo "Total of $elapsed seconds elapsed for process" }

declare -A time=()

stamp start sleep 5 stamp end

measure_time start end

EDIT:

Caveat

As the figures in roaima's answer show, this is not going to be high precision, because each action (particularly so sub shells and external processes) will pad the reported time out by some fraction of a second. This probably doesn't matter for multi-minute processes (in which case why measure nano seconds?) but will become more pronounced at lower run times, to the point of being almost meaningless. Though the problem was rather less severe in my tests as compared to roaima's. It rather depends on what you are using that data for.

ANOTHER EDIT:

Responding to QuartzCrystal's fair comment, herein lies a further iteration which, it turns out, is converging on roaima's answer -- probably indicating they are on the "best" track.

_stamp(){
  time[${1:?}]="$(date -u +%s.%N)"
}

start() { _stamp "start${1:+${1}}" }

end() { _stamp "end${1:+${1}}" }

measure_time(){ local elapsed="$(bc <<<"${time[end${1:+_${1}}]}-${time[start${1:+_${1}}]}")" echo "Total of $elapsed seconds elapsed${1:+ for process ${1}}" }

declare -A time=()

_start sleep 5 _end

measure_time

_start foo sleep 1 _start bar sleep 2 _end foo _end bar

measure_time foo measure_time bar

bxm
  • 4,855
2

Here is a version that requires associative arrays (bash, etc.)

To be placed in an example timers.sh

#!/bin/bash
########################################################################
declare -A _timers

Declare and start a timer. Optional name

startTimer() { timers[${1:-}_s]=$(date -u +'%s.%N') }

End a timer. Optional name

endTimer() { timers[${1:-}_e]=$(date -u +'%s.%N') }

Report on the duration of a timer. Optional name

diffTimer() { local s=${timers[${1:-}s]} e=${_timers[${1:-}_e]} bc <<< "${e:-$(date -u +'%s.%N')} - ${s:-0}" }

Main code

#!/bin/bash
. timers.sh

startTimer sleep 2 startTimer sub sleep 0.1 endTimer sub endTimer

echo "Elapsed $(diffTimer) and for sub $(diffTimer sub)"

Output

Elapsed 2.321831200 and for sub .183521000

Without a parameter the functions act on a default timer. Otherwise they act on the named timer. If you don't call startTimer or endTimer the default values of 0 and now are used. Timers are stored in the global associative array _timers[]

Chris Davies
  • 116,213
  • 16
  • 160
  • 287