0

A simple perl script:

#!/usr/bin/perl
$num = `sed "s/\([^ ]*\).*/\1/"`;
print "$num";

$total = `sed "s/[^ ]*\(.*\)/\1/"`;
print "$total";

$div = $num / $total;
print "div = $div \n";

When I run it:

$ echo 4 10 | ./test.pl 
4 10
Illegal division by zero at ./test.pl line 11.
TLP
  • 66,756
  • 10
  • 92
  • 149
Maria Ines Parnisari
  • 16,584
  • 9
  • 85
  • 130
  • `use strict;` is also a good idea – Joe W Nov 01 '13 at 12:30
  • 3
    Using backticks inside Perl to call `sed` to handle stdin sent to Perl must be the most awkward way I've seen in a long while to obfuscate the diamond operator `<>`. – TLP Nov 01 '13 at 13:43

3 Answers3

3

Looking at your code, I am somewhat confused. First by how one would come up with such a strange idea, and second how it works.

I assume that what you want is to parse a string sent to Perl via STDIN. There is a very simple and idiomatic way to do that, that does not involve the strange usage of sed inside backticks: Using the diamond operator <>, which is a file handle that reads from STDIN or ARGV (file name argument list), depending on which is given.

use strict;
use warnings;               # always use these two pragmas

my $input = <>;                         # input is the string "4 10"
my ($num, $total) = split ' ', $input;  # splitting on whitespace
my $div = $num / $total;                # ... etc

Normally, one would chomp input from STDIN to remove the trailing newline, but since we split on whitespace here, the newline is removed anyway.

As to why you get the illegal division by zero error, that is because your $total variable is undefined, which translates to the number zero 0 when used in a numerical context, such as with the division operator /. If you had used use warnings, you would have gotten the error

Use of uninitialized value $total in division (/) at script.pl line 10.

Which would no doubt have told you something about what the error was.

TLP
  • 66,756
  • 10
  • 92
  • 149
2

Your code doesn't work as expected, because you don't understand the escaping rules of strings in Perl, and because you are confused how STDIN is inherited.

In double quoted strings, unknown escapes like \( are ignored by dropping the backslash. Therefore: "\(" eq "(". The \1 is usually not a replacement operator, but a octal escape sequence: "\1" eq "\01" && "\1" eq "\x01" – it is the character 0x01 “Start of Heading”. So your first line is identical to

$num = `sed "s/([^ ]*).*/\x01/"`;  # this won't change your input

As you can see, the regex is actually buggy, and won't even match your input.

The backtick operator threats its contents exactly like a double-quoted string, then passes the contents to the shell. The shell also has escapes, so quite often double escaping has to be done.

The shell is launched by forking from your script. The child process inherits all file descriptors including STDIN. The child execs the shell which then forks off a sed process, which reads all input from STDIN, which is still the same as the STDIN of your original script.

Now when you execute the 2nd command, STDIN is empty, and sed prints nothing. At this point, $total will contain the empty string. When used as a number, that string is interpreted as zero, leading to your error.

The solution

The solution is to stop treating Perl as a glorified shell, because Perl isn't as good for stringing commands together as actual sh. To parse your input, use the built-in regexes of Perl. To read input, use the builtins, not shell idioms.

First, start every script with:

use strict;
use warnings;

For example, this forces us to properly declare all variables. After we do that, running your script emits the following warnings and errors:

Argument "" isn't numeric in division (/) at script.pl line 12.
Argument "4 10\n" isn't numeric in division (/) at script.pl line 12.
Illegal division by zero at script.pl line 12.

So you are actually calculating "4 10 \n" / "", not what you intended.

To read input, we use the <> diamond operator. Each line then lands in the $_ default variable. We could use a regex like /([0-9]+)/ to extract the numbers, but we'll rather split each line at whitespace:

while (<>) {
  my ($num, $total) = split;
  print $num / $total, "\n";
}

We then perform the division, and print out the result along with a newline. If you are using any non-ancient perl, you can also use feature 'say', which enables the say function: Exactly like print, but automatically adds a newline.

Instead of giving the arguments through STDIN etc. you could also specify them on the command line. The command line arguments are accessible through the @ARGV array. Now:

my ($num, $total) = @ARGV;
say $num / $total;

Invocation:

$ perl script.pl 4 10

For some tasks, Perl isn't the fitting tool. If you want to write a shell script, write an actual shell script:

#!/bin/bash
input="4 10"
num=`echo $input | sed 's/\([^ ]*\).*/\1/'`
total=`echo $input | sed 's/[^ ]*\(.*\)/\1/'`
echo `bc <<<"scale = 5; $num / $total"`
amon
  • 57,091
  • 2
  • 89
  • 149
1

Change the code like this and the answer will be obvious:

#!/usr/bin/perl

$num = `sed "s/\([^ ]*\).*/\1/"`;

print "num = $num\n";

$total = `sed "s/[^ ]*\(.*\)/\1/"`;

print "total = $total\n";

$div = $num / $total;

print "div = $div\n";
David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • What do you think `.*` does in a regular expression? – David Schwartz Nov 01 '13 at 12:36
  • @toolic I'm trying to, but I don't understand the reasoning underlying it yet. "The code obviously sets it to that" isn't helpful. I need to figure out why someone would expect it to do something different so I can address the erroneous reasoning behind that expectation. – David Schwartz Nov 01 '13 at 12:37
  • @toolic I might not. I haven't figured out what the question is yet. For some reason, someone is expecting this code to do something other than what it does, but I don't know what the reason is yet. (What's the mystery? By what reasoning would anyone expect anything else?) – David Schwartz Nov 01 '13 at 12:40
  • I know why $num has that value. I don't know why anyone would expect it to have any other value. It has that value because that's the obvious effect of the regex. Presumably, someone has some other understanding of what `.*` does, but I don't know what that understanding is, so I can't point out what's wrong with it. What's the mystery? Do you expect something different? If so, what? Why? – David Schwartz Nov 01 '13 at 12:41
  • 1
    Because the `.*` matches the whole line. The attempt to select the first match doesn't work because the backslash isn't properly escaped in the perl code. (Presumably `sed "s/\\([^ ]*\\).*/\\1/"` was intended.) – David Schwartz Nov 01 '13 at 12:51
  • 1
    Quoting. Perl handles the escapes and ends up calling ```sed "s/([^ ]*).*/1/"```, which never matches, so the original stdin is simply returned. Protect the characters you want to send to the shell from Perl's quoting rules and you will probably get the results you expect. – MidLifeXis Nov 01 '13 at 13:10
  • 4
    The correct answer to this question should contain a recommendation not to use code which includes handling standard input by calling sed through a system call. – TLP Nov 01 '13 at 13:55