Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perl changing value within a conditional before entering the conditional?

I am working on a Perl script that is to help with the automation of scanning of machines on our network. I am not a programmer by trade, but none-the-less this project has been assigned to me, and I am quite stumped. Before I explain the nature of what is stumping me, let me explain the outline of what I am doing.

Basically, this script will be run every n hours. When run, it will check a file that holds a log of active IPs and check them against a DHCP log to single out only those that are static. These then are put into a hash (a new one if flagged to initialize, loaded using Storable otherwise), with the key being the IP and within an array their MAC [0] and a "last scanned" date [1] initially set to 19700101. The next part of the script compares the date between today's date and the "last scanned" date - and if it under a certain threshold, it sends a query to our scanner.

The issue that has me so lost is that when the date is being checked, it seems to me that the date value (the updated "last scanned") is being set before entering the conditional. While this doesn't seem likely to me, it is the only possibility I can think of. Here is the relevant chunks of code:

The code that adds IP/MACs to the hash

 if(init == 1){
            %SCAN = ();

            @data = ();

            foreach $key (keys %IPS){

                    $unsavedDB = 1;

                    $data[0] = $IPS{$key};
                    $data[1] = 19700101;

                    print $data[1];

                    $SCAN{$key} = \@data;
            }
 }else{
            #repeat of the above code, but with a if(exists...) to prevent duplicates from being added to the hash that is loaded via storables.
 }

The code that checks the date (which is set previously, and would be 20120726 for today). Between the above code and the following there is nothing but comments

    $scanned = 0;

    foreach $key (keys %SCAN){

            $lastScanned = $SCAN{$key}[1];

            if(($date - $lastScanned) > $threshold){
                    $unsavedDB = 1;

                    $toScan = ${$key}[0];

                    #omitted data for security reasons, just basically forms a string to send to a scanner

                    $SCAN{$key}[1] = $date;

                    $scanned++;
            }
    }

    print "finished. $scanned hosts queued\n";

Now, the reason that I believe that the value is being changed before entering the loop is when I add a 'print $lastScanned' statement right before the 'if(($date...){' the date printed the whatever is assigned to $date earlier - but if I to comment out the '$SCAN{$key}[1] = $date;' statement, the print statements will print the '19700101' date and everything functions as it should. What is happening? $SCAN{$key}[1] is never being touched except in the two places shown above.

Sorry if this is very badly phrased, or doesn't make sense. I tried my best to explain something that has been stumping me for hours.

Thank you!

like image 782
Zachary Yamada Avatar asked Jul 26 '12 21:07

Zachary Yamada


2 Answers

Because your @data array is global, every time your execute the statement

$SCAN{$key} = \@data;

you're assigning to $SCAN{$key} a reference to the same @data array. Thus, all the values in %SCAN end up pointing to the same array, which is presumably not what you want.

There are several ways in which you could fix that. Perhaps the simplest would be to make the code assign a reference to a copy of the @data array to $SCAN{$key}, by changing the above line to

$SCAN{$key} = [ @data ];

Alternatively, you could rewrite the entire loop to use a lexical array declared with my inside the loop — that way you create a new separate array on each iteration:

foreach $key (keys %IPS) {
        $unsavedDB = 1;

        my @data;  # <--- this line is new!

        $data[0] = $IPS{$key};
        $data[1] = 19700101;

        print $data[1];

        $SCAN{$key} = \@data;
}

However, what you really should do, instead of just fixing the symptoms of this particular bug, is learn how variable scoping works in Perl and how it should be used, and rewrite your code accordingly.

In particular, looking at your code, I very much suspect that you're not using the strict pragma in your code. If you want to write clean Perl code, the first thing you really should do is prepend the following two lines to all your scripts, immediately after the #! line:

use strict;
use warnings;

The strict pragma forces you to avoid certain bad and error-prone habits, such as using symbolic references or undeclared global variables, while the warnings pragma make the interpreter warn you about various other silly, risky, ambiguous or otherwise undesirable things (which you really should treat as errors and fix until you get no more warnings).

Of course, this doesn't mean you should just declare all your variables at the beginning of the script with my (or our) just to make strict happy. Instead, what you should do is look at each variable, see where it's actually used, and declare it in the innermost scope it's needed in. (If you're reusing the same variable name in different parts of the code, treat those as separate variables and declare each of them separately.) Remember that you can declare loop variables in the loop statement, as in

foreach my $key (keys %IPS) {

or

while (my $line = <>) {

Ps. I also noticed a worrying comment in the code you showed us:

# repeat of the above code, but with ...

Generally, that kind of code duplication should be a big flashing signal that you're probably doing something wrong — the golden rule of programming is "Don't repeat yourself."

Of course, there are those few, very very rare occasions where you do need to do essentially the same thing in two different ways, but with so many small and arbitrary differences peppered throughout that it's cleaner to write the whole thing twice. But I'd be very surprised if that was the case here — I bet you could write that code only once, and just maybe insert an

if (not $init and exists ...) {

check at a suitable location.

like image 199
Ilmari Karonen Avatar answered Nov 06 '22 14:11

Ilmari Karonen


As Ilmari says, your problem is that every element of %SCAN points to the same two-element array that was @data in the first code block, so $SCAN{<anything>}[1] is the same variable for all the IP addresses.

To fix this, my preference would be to forget about @data and write

$SCAN{$key} = [ $IPS{$key}, '19700101' ];

which generates a new anonymous array each time the statement is executed and assigns a reference to it as the hash's value.

Note also that I have used a string for the date, as you cannot write things like $date - $lastScanned: date arithmetic is more complex than that. Subtracting 31-JAN-2012 from 1-FEB-2012 would become 20120201 - 20120131 or 70!

Fortunately there are modules to make this easier and you can use the module Time::Piece, which is a core module (i.e. it gets installed with standard Perl since Perl v5.9) and will let you do just this sort of arithmetic.

At the top of your program, after use strict and use warnings, you write

use Time::Piece;

and then later on, for your initial time, write

my $initial = localtime(0);

and then

my $date = localtime;

You can see the dates the two values correspond to by just printing them

print $initial, "\n";
print $date, "\n";

which will show something like

Thu Jan  1 00:00:00 1970
Fri Jul 27 01:40:53 2012

and a simple subtraction gives you the real difference, in seconds

print $date - $initial;

So if $threshold is in days you can check the interval by writing

if ( $date - $lastScanned > $threshold * 24 * 60 * 60 ) { ... }

I hope I haven't scared you here, but it needed changing and I thought you should know. The module will do a lot more than this, and if you want to look at the documentation it's here. And please ask another question if you get stuck.

like image 27
Borodin Avatar answered Nov 06 '22 13:11

Borodin