6

I've got a script that calls Perl's Time::HiRes module to calculate elapsed time. Basically the script gets the time by passing the following one-liner:

use Time::HiRes qw(time); print time

to the Perl interpreter via backticks and gets back the results.

#/bin/sh

START_TIME=`perl -e 'use Time::HiRes qw(time); print time'`
END_TIME=`perl -e 'use Time::HiRes qw(time); print time'`
ELAPSED_TIME=$(echo "($END_TIME - $START_TIME)" | bc)
echo $ELAPSED_TIME

I tried to rewrite it in a more modular way, but I'm stumped by the quoting rules of the Bash shell.

#/bin/sh
CALCULATE='bc'
NOW="perl -e 'use Time::HiRes qw(time); print time'"
START_TIME=`$NOW`
[Some long running task ...]
ELAPSED_TIME=$(echo "($NOW - $START_TIME)" | $CALCULATE)
echo $ELAPSED_TIME

Bash complains that something is not quoted properly. Why doesn't Bash just expand the command in $NOW and pass it to the backtick to be executed?

I tried various ways to embed Perl code in a shell script variable, but I can't seem to get it right.

How can I quote Perl code inside a shell script correctly?

2
  • @Mat To make it easier for people to answer, I just showed an excerpt from a much larger script. I cut and pasted it wrong. Thanks for pointing out. Fixed. But the issue is still the quoting though. Commented Apr 28, 2012 at 8:37
  • 1
    FYI, you can write your perl one-liner as perl -MTime::HiRes=time -e 'print time' Commented Apr 28, 2012 at 11:34

5 Answers 5

7

Using a function is the most straightforward way to do this, I think:

#! /bin/bash

now() {
    perl -e 'use Time::HiRes qw(time); print time';
}

calc=bc
time1=$(now)
time2=$(now)
elapsed=$(echo $time2 - $time1 | $calc)
echo $elapsed $time1 $time2

Essentially no quoting is required.

Sign up to request clarification or add additional context in comments.

1 Comment

That's it! Silly me. Talk about getting stuck in a box. I sometimes forget that Bash is a full fledge programming language. Thanks.
3

Your problem is that $NOW is just a string with some Perl code in it. You need to tell Bash to execute it, with backticks or $():

ELAPSED_TIME=$(echo "($($NOW) - $START_TIME)" | $CALCULATE)

Also, Bash can do arithmetic natively:

ELAPSED_TIME=$(( $($NOW) - $START_TIME))

There isn't any need to invoke bc.

Finally, starting and stopping perl is likely to take a lot of time, which will add noise to your results. I'd recommend running perl only once, and having perl itself execute the long-running task. You'd then do all the computation within Perl itself as well:

#!/usr/bin/perl

use Time::HiRes qw(time);

my $start = time;
system(@ARGV);
my $end = time;

print "Elapsed: ", ($end - $start), "\n"

Or you could just use the Bash builtin time (or /usr/bin/time) to just do all the timing directly.

3 Comments

Good point there about the perl interpreter loading time. But it's some old code I'm maintaining so I don't want to change it too much. Switching to Perl would be porting it, not refactoring and that's a little bit too much work. I'd prefer to write it in pure Perl any day.
"no need to invoke bc" - my bash (and expr) doesn't do arithmetics with decimal numbers.
@Mat, ah, yes, zsh did, which threw me off a bit there
1

If $NOW is outside of quotes, it gets split on whitespace.

$ perl -E'say 0+@ARGV; say for @ARGV' $NOW
7
perl
-e
'use
Time::HiRes
qw(time);
print
time'

You can surround the variable by double-quotes to avoid this:

$ perl -E'say 0+@ARGV; say for @ARGV' "$NOW"
1
perl -e 'use Time::HiRes qw(time); print time'

But you want to execute that string as a shell command. For that, use eval.

$ eval "$NOW"
1335602750.57325

Finally, to assign it, we use the backticks (or equivalent $( ... )).

$ START_TIME=$(eval "$NOW")
$ echo $START_TIME
1335602898.78472

The previously posted function is obviously cleaner, but you said you wanted help with quoting.


By the way,

perl -e 'use Time::HiRes qw(time); print time'

can be shortened to

perl -MTime::HiRes=time -e'print time'

and even to the following (since the trailing new line is perfectly fine):

perl -MTime::HiRes=time -E'say time'

Or if you really wanted to golf:

perl -MTime::HiRes=time -Esay+time

1 Comment

Added a bit at the end for colour.
1

Below is a modified version of your script. You basically need to understand that some applications have their standard output towards standard error (stderr), so when you don't see their output put in a variable, you just need to redirect it to standard output (stdout):

#/bin/sh
CALCULATE='bc'
echo 'starting'
NOW=$(perl -e 'use Time::HiRes qw(time); print time' 2>&1)
sleep 3
echo 'ending'
END_TIME=$(perl -e 'use Time::HiRes qw(time); print time' 2>&1)
ELAPSED_TIME=$(echo "($NOW - $START_TIME)")
echo $ELAPSED_TIME

Comments

0

I think the benefit of HiRes time is negated by the fact that perl is a relatively heavy external process and it is separately invoked two times.

If you don't need that many decimal places for the value. you can use the time builtin in Bash like

task() {
    [Some long running task ...]
}
TIMEFORMAT=%R
elapse=$({ time task > task.out 2>&1; } 2>&1)
echo $elapse

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.