How can I conditionally pass a subshell through 'time'?

To be able to time a subshell, you need the time keyword, not command.

The time keyword, part of the language, is only recognised as such when entered literally and as the first word of a command (and in the case of ksh93, then the next token doesn't start with a -). Even entering "time" won't work let alone $TIME (and would be taken as a call to the time command instead).

You could use aliases here which are expanded before another round of parsing is performed (so would let the shell recognise that time keyword):

shopt -s expand_aliases
alias time_or_not=
TIMEFORMAT=%E

MEASURE_TIME=true
[[ $MEASURE_TIME = true ]] && alias time_or_not=time

time_or_not (apt-get update > /tmp/last.log 2>&1)

The time keyword doesn't take options (except for -p in bash), but the format can be set with the TIMEFORMAT variable in bash. (the shopt part is also bash-specific, other shells generally don't need that).


While an alias is one way to do it, this can be done with eval as well - it's just that you don't so much want to eval the command execution as you want to eval the command declaration.

I like aliases - I use 'em all the time, but I like functions better - especially their ability to handle parameters and that they needn't necessarily be expanded in command position as is required for aliases.

So I thought maybe you'd want to try this, too:

_time() if   set -- "${IFS+IFS=\$2;}" "$IFS" "$@" && IFS='
';      then set -- "$1" "$2" "$*"; unset IFS
             eval "$1 $TIME ${3#"$1"?"$2"?}"
        fi

The $IFS bit is mainly about $*. It's important that the ( subshell bit ) is also the result of a shell expansion and so in order to expand the arguments into a parsable string I use "$*" (don't eval "$@", by the way, unless you're certain all of the args can be joined on spaces). The split delimiter between args in "$*" is the first byte in $IFS, and so it could be dangerous to proceed without making certain its value. So the function saves $IFS, sets it to a \newline long enough to set ... "$*" into "$3", unsets it, then resets its value if it previously had one.

Here's a little demo:

TIME='set -x; time'
_time \( 'echo "$(echo any number of subshells)"' \
         'command -V time'                        \
         'hash time'                              \
      \) 'set +x'

You see I put two commands in the value of $TIME there - any number is fine - even none - but be sure it is escaped and quoted properly - and the same goes for the arguments to _time(). They will all be concatenated into a single command string when they are executed - but each arg gets its own \newline and so they can be spread out relatively easily. Or else you can lump them all in one, if you like, and separate them on \newlines or semi-colons or what-have-you. Just be sure that a single argument represents a command you'd feel comfortable putting on its own line in a script when you call it. \(, for example is fine, so long as it is eventually followed with \). Basically the normal stuff.

When eval gets the above snippet fed it it looks like:

+ eval 'IFS=$2;set -x; time (
echo "$(echo any number of subshells)"
command -V time
hash time
)
set +x'

And, its results look like...

OUTPUT

+++ echo any number of subshells
++ echo 'any number of subshells'
any number of subshells
++ command -V time
time is a shell keyword
++ hash time
bash: hash: time: not found

real    0m0.003s
user    0m0.000s
sys     0m0.000s
++ set +x

The hash error indicates that I don't have a /usr/bin/time installed (because I don't) and command let's us know which time is running. The trailing set +x is another command executed after time (which is possible) - it is important to be careful with input commands when evaling anything.