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 alias
es - 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 alias
es.
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 \n
ewline long enough to set ... "$*"
into "$3"
, unset
s 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 \n
ewline 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 \n
ewlines 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 eval
ing anything.