GeneQ
GeneQ

Reputation: 7595

How can I execute Perl code stored inside a shell script variable?

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?

Upvotes: 6

Views: 11575

Answers (5)

pizza
pizza

Reputation: 7630

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

Upvotes: 0

Khalil Khamlichi
Khalil Khamlichi

Reputation: 161

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

Upvotes: 1

bdonlan
bdonlan

Reputation: 231093

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.

Upvotes: 3

Mat
Mat

Reputation: 206689

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.

Upvotes: 7

ikegami
ikegami

Reputation: 385655

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

Upvotes: 1

Related Questions