88

In order to find out how long certain operations within a Bash (v4+) script take, I would like to parse the output from the time command "separately" and (ultimately) capture it within a Bash variable (let VARNAME=...).

Now, I am using time -f '%e' ... (or rather command time -f '%e' ... because of the Bash built-in), but since I already redirect the output of the executed command I'm really lost as to how I would go about to capture the output of the time command. Basically the problem here is to separate the output of time from the output of the executed command(s).

What I want is the functionality of counting the amount of time in seconds (integers) between starting a command and its completion. It doesn't have to be the time command or the respective built-in.


Edit: given the two useful answers below, I wanted to add two clarifications.

  1. I do not want to throw away the output of the executed command, but it will not really matter whether it ends up on stdout or stderr.
  2. I would prefer a direct approach over an indirect one (i.e. catching output directly as opposed to store it in intermediate files).

The solution using date so far comes closes to what I want.

2
  • The most direct way to get the data and handle it while still letting it run normally would be to do it in a C program using fork(), execvp() and wait3()/wait4(). This is ultimately what time and friends are doing. I'm not aware of a simle way to do it in bash/perl without redirecting to a file or similar approach.
    – penguin359
    Apr 26, 2011 at 22:20
  • There is a related question that you might find interesting going on here.
    – Caleb
    Apr 26, 2011 at 23:09

9 Answers 9

105

To get the output of time into a var use the following:

usr@srv $ mytime="$(time ( ls ) 2>&1 1>/dev/null )"
usr@srv $ echo "$mytime"

real    0m0.006s
user    0m0.001s
sys     0m0.005s

You can also just ask for a single time type, e.g. utime:

usr@srv $ utime="$( TIMEFORMAT='%lU';time ( ls ) 2>&1 1>/dev/null )"
usr@srv $ echo "$utime"
0m0.000s

To get the time you can also use date +%s.%N, so take it before and after execution and calculate the diff:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF
7
  • 4
    I didn't want to throw away the output from the command though. So I guess your third code block is closest to what I had in mind. Although I'd write the last one as DIFF=$((END-START)), making use of the arithmetic expressions. :) ... thanks for the answer. +1 Apr 26, 2011 at 20:00
  • 2
    @STATUS_ACCESS_DENIED: Bash doesn't do floating point arithmetic, so if you want better than second resolution (.%N in binfalse's code), you either need bc or fancier calculations. Apr 26, 2011 at 21:11
  • @Gilles: I know. As I wrote in my question integers are fine. No need to have a higher resolution than second. But thanks, the date invocation would have to be changed. I had realized that, though. Apr 26, 2011 at 23:25
  • 11
    FYI, the date formatting %N doesn't seem to work on Mac OS X, it just returns "N". Ok on Ubuntu.
    – David
    Oct 19, 2015 at 21:24
  • 1
    If you find yourself in a situation where there's dc but no bc (probably rare but I just did), you can use DIFF=$(echo "$END $START - p" | dc). Oct 2, 2018 at 12:42
23

If you are in bash (and not sh) and you DO NOT need sub-second accuracy, you can skip the call to date entirely and do it without spawning any extra processes, without having to separate the combined output, and without having to capture and parse output from any commands:

# SECONDS is a bash special variable that returns the seconds since set.
SECONDS=0
mycmd <infile >outfile 2>errfile
DURATION_IN_SECONDS=$SECONDS
# Now `$DURATION_IN_SECONDS` is the number of seconds.
0
19

In bash, the output of the time construct goes to its standard error, and you can redirect the standard error of the pipeline it affects. So let's start with a command that writes to its output and error streamas: sh -c 'echo out; echo 1>&2 err'. In order not to mix up the command's error stream with the output from time, we can temporarily divert the command's error stream to a different file descriptor:

{ time -p sh -c 'echo out; echo 1>&2 err' 2>&3; }

This writes out to fd 1, err to fd 3, and the times to fd 2:

{ time -p sh -c 'echo out; echo 1>&2 err' 2>&3; } \
    3> >(sed 's/^/ERR:/') 2> >(sed 's/^/TIME:/') > >(sed 's/^/OUT:/')

It would be more pleasant to have err on fd 2 and the times on fd 3, so we swap them, which is cumbersome because there's no direct way to swap two file descriptors:

{ { { time -p sh -c 'echo out; echo 1>&2 err' 2>&3; } 3>&2 2>&4; } 4>&3; } 3> >(sed 's/^/TIME:/') 2> >(sed 's/^/ERR:/') > >(sed 's/^/OUT:/')

This shows how you can postprocess the output of the command, but if you want to capture both the output of the command and its times, you need to work harder. Using a temporary file is one solution. In fact, it's the only reliable solution if you need to capture both the command's standard error and its standard output. But otherwise, you can capture the whole output and take advantage of the fact that time has a predictable format (if you use time -p to get the POSIX format or the bash-specific TIMEFORMAT variable).

nl=$'\n'
output=$(TIMEFORMAT='%R %U %S %P'; mycommand)
set ${output##*$nl}; real_time=$1 user_time=$2 system_time=$3 cpu_percent=$4
output=${output%$nl*}

If you only care about wall clock time, running date before and after is a simple solution (if slightly more imprecise due to the extra time spent loading the external command).

0
7

With time, the command output comes out on stdout and the time comes out on stderr. So, to separate them, you can do:

command time -f '%e' [command] 1>[command output file] 2>[time output file]

But, now the time is in a file. I don't think Bash is able to put stderr in a variable directly. If you don't mind redirecting the command's output somewhere, you can do:

FOO=$((( command time -f '%e' [command]; ) 1>outputfile; ) 2>&1; )

When you do this, the command's output will be in outputfile and the time it took to run will be in $FOO.

2
  • 1
    ooh, I didn't realize. I knew time writes to stderr, but I didn't realize that it would merge stdout and stderr of the executed command into its stdout. But generally there is no direct method (i.e. without intermediate file)? +1. Apr 26, 2011 at 19:57
  • 3
    @STATUS_ACCESS_DENIED: time does not merge its command's stdout and stderr. The way I showed assumed that you only needed the command's stdout output. Since bash will only store what comes out of stdout, you have to redirect. In case you can safely drop your command's stderr: the time will always be on the last line of stderr. If you do need both your command's output streams, I'd suggest wrapping it in another script.
    – marinus
    Apr 26, 2011 at 20:27
5

In Putting all together previous responses, when in OSX

ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31+

you can do like

microtime() {
    python -c 'import time; print time.time()'
}
compute() {
    local START=$(microtime)
    #$1 is command $2 are args
    local END=$(microtime)
    DIFF=$(echo "$END - $START" | bc)
    echo "$1\t$2\t$DIFF"
}
4

For that purpose it's probably better to use times (than time) in bash which prints the accumulated user and system times for the shell and for processes run from the shell, example use:

$ (sleep 2; times) | (read tuser tsys; echo $tuser:$tsys)
0m0.001s:0m0.003s

See: help -m times for more info.

1

try this, it runs a simple command with arguments and puts the times $real $user $sys and preserves the exit code.

It also does not fork subshells or trample on any variables except real user sys, and does not otherwise interfere with the running of the script

timer () {
  { time { "$@" ; } 2>${_} {_}>&- ; } {_}>&2 2>"/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  set -- $?
  read -d "" _ real _ user _ sys _ < "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  rm -f "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  return $1
}

e.g.

  timer find /bin /sbin /usr rm /tmp/
  echo $real $user $sys

note: it only times the simple command not a pipeline, whose components are run in a subshell

This version allows you to specify as $1 the name of the variables that should receive the 3 times:

timer () {
  { time { "${@:4}" ; } 2>${_} {_}>&- ; } {_}>&2 2>"/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  set -- $? "$@"
  read -d "" _ "$2" _ "$3" _ "$4" _ < "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  rm -f "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  return $1
}

e.g.

  timer r u s find /bin /sbin /usr rm /tmp/
  echo $r $u $s

and may be useful if it ends up being called recursively, to avoid trampling on times; but then r u s etc should be declared local in their use.

http://blog.sam.liddicott.com/2016/01/timeing-bash-commands.html

1

Install /bin/time (e.g. pacman -S time)

So instead of error when trying -f flag:

$ time -f %e sleep 0.5
bash: -f: command not found

real    0m0.001s
user    0m0.001s
sys     0m0.001s

You can actually use it:

$ /bin/time -f %e sleep 0.5
0.50

And get what you want - time in variable (example uses %e for real elapsed time, for other options check man time):

#!/bin/bash
tmpf="$(mktemp)"
/bin/time -f %e -o "$tmpf" sleep 0.5
variable="$(cat "$tmpf")"
rm "$tmpf"

echo "$variable"
2
  • /usr/bin/time on many systems Sep 19, 2017 at 11:58
  • If you look closely I pointed that out in my question. time is a shell builtin in Bash (and presumably other shells) but as long as the path to the time executable is in PATH, we can use command time to ensure we're running the external command as opposed to the builtin. Sep 19, 2017 at 12:34
1

Just as a heads up when working with the above statements and especially taking Grzegorz's answer in mind. I was surprised to see on my Ubuntu 16.04 Xenial system these two results:

$ which time
/usr/bin/time

$ time -f %e sleep 4
-f: command not found
real    0m0.071s
user    0m0.056s
sys     0m0.012s

$ /usr/bin/time -f %e sleep 4
4.00

I don't have any aliases set and so I don't know why this is happening.

3
  • 1
    time is also a builtin shell. Sep 19, 2017 at 11:57
  • If you want to ensure you're running the command, use command, if you want to ensure you're running the builtin, use builtin. There's no magic here. Use help to figure out the commands available or type <command> to figure out what type <command> is (e.g. a builtin, external command, function or alias). Sep 19, 2017 at 12:29
  • Basile, you are quite right. I hadn't spotted the significance of the command command. Which is fact ensures that /usr/bin/time is called. This means that the following two statements are compatible: $ command time -f %e sleep 4 and $ /usr/bin/time -f %e sleep Sep 20, 2017 at 6:38

You must log in to answer this question.

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