0

How to wrap a command to measure its elapsed time?

Currently I do it using eval:

do_cmd_named()
{
  local name=$1
  local cmd=$2

echo "$name" local start_time=$(date +%s) eval "$cmd 2>&1" local exit_status=$? local end_time=$(date +%s) local elapsed_time_sec=$((end_time-start_time)) local elapsed_time_min_sec=$(date -ud "@$elapsed_time_sec" +'%M:%S') if [[ $exit_status -ne 0 ]] then echo "$name failed with exit status $exit_status (elapsed time $elapsed_time_min_sec)" return $exit_status else echo "$name done (elapsed time $elapsed_time_min_sec)" fi }

job() { sleep 1 }

do_cmd_named "do job" "job"

which leads to:

do job
do job done (elapsed time 00:01)

For my cases this approach almost works. However, this approach is considered bad because it violates some rules from BashFAQ. For example, "don't put code inside variables" from BashFAQ #50 (see also BashFAQ #48).

So, the question is: how to do it correctly?

pmor
  • 599

2 Answers2

1
  1. capture the command (and arguments) into an array and then you can avoid eval
  2. use the bash builtin variable $SECONDS
do_cmd_named() {
    local name=$1
    shift
    local -a cmd=("$@")
echo "$name"
SECONDS=0

"${cmd[@]}"

local status=$?
local elapsed_time_sec=$SECONDS

# print messages ...

}

do_cmd_named "job name" job arg1 arg2


The issue with $SECONDS is that in case of nested invocations the $SECONDS is reset to 0.

Yes that's true.

An alternate approach is at the start of the function, capture the current value of SECONDS and do some arithmetic at the end.

An example:

rectest() {
    local n=$1 delay=$2 start=$SECONDS
    ((n == 3)) && return
    sleep $delay
    rectest $((n + 1)) $((delay + 3))
    echo "at level $n, delay is $delay and duration is $((SECONDS - start))"
}

and running it, all output appears after a 15 second duration:

$ rectest 0 2
at level 2, delay is 8 and duration is 8
at level 1, delay is 5 and duration is 13
at level 0, delay is 2 and duration is 15
glenn jackman
  • 85,964
  • Thanks. How to correctly add support for redirections? Say, job arg1 arg2 needs to write to job.log. Note that extra printed info (such as echo "$name") needs to be on stdout. – pmor Nov 22 '22 at 10:31
  • 1
    We can't pass around syntax like >. I'd do something like do_cmd_named -o output.log -n name job arg arg ... and then get the function to parse the arguments using getopts. Search around, there are tons of examples here and on stackoverflow – glenn jackman Nov 22 '22 at 13:21
  • What to do in case of arg && arg? Put it in a string ("arg && arg") or escape & (arg \&\& arg)? – pmor Nov 23 '22 at 12:15
  • UPD: it seems that both don't work. Does it mean that we can't pass around syntax like &&? The same for |. In eval-based approach "arg1 && arg2" works as well as "arg1 | arg2". – pmor Nov 23 '22 at 12:29
  • 1
    that is correct. bash has to parse the command line before expanding variables. If you need to do that, put the code in a function or script. – glenn jackman Nov 23 '22 at 13:45
  • In local -a cmd=("$@") do we really need the -a? – pmor Nov 23 '22 at 15:56
  • The issue with $SECONDS is that in case of nested invocations the $SECONDS is reset to 0. – pmor Nov 23 '22 at 16:13
  • "do we really need the -a?" No. – glenn jackman Nov 23 '22 at 16:19
0

If switching to zsh is an option:

$ TIMEFMT='%J done (elapsed time: %E)'
$ time sleep 1
sleep 1 done (elapsed time: 1.00s)