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?

7
  • 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 Commented Sep 11, 2022 at 7:28
  • 1
    Or you can try passing a function as parameter to another function: stackoverflow.com/questions/5672289/… Commented Sep 11, 2022 at 7: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) Commented Sep 11, 2022 at 12:41
  • @steeldriver but not the nanoseconds the OP seems to want Commented Sep 11, 2022 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
    Commented Sep 11, 2022 at 15:53

4 Answers 4

5

What is wrong with time ./script.sh ?

Simple example:-

time sleep 5

real    0m5.001s
user    0m0.001s
sys 0m0.000s
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.

0
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
1
  • 1
    Inside the functions, what if $1 or $2 are empty or unset? Hint: use ${1:-_default} for example. Commented Sep 12, 2022 at 6:40
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[]

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .