LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Perl - Looping through a file, changing behaviour by 'heading' (https://www.linuxquestions.org/questions/programming-9/perl-looping-through-a-file-changing-behaviour-by-heading-761558/)

Lordandmaker 10-13-2009 04:53 AM

Perl - Looping through a file, changing behaviour by 'heading'
 
I have a series of files (one per site) that are structured so:
Code:

Servers:
scan-svr01        10.45.0.1
scan-svr02        10.45.0.2
scan-svr05        10.45.0.5

Services:

dhcp        scan-svr01
dns1        scan-svr01
dns2        scan-svr02
ftp       
gateway        10.45.0.40
job bag        scan-svr05
mail        jup-svr09
print        scan-svr01
prod-route        scan-svr02
rdp        scan-svr07
shared        scan-svr01
users        scan-svr01

And I'm trying to loop through it to produce a pretty webpage. I'm fine with reading files line-by-line, but I want to change what I'm doing depending on which was the last 'heading' I passed. Servers should be put into a hash $servers{$hostname}=$ip, while Services into one $services{$service}=[$site, $server]
There's reasonable chance of adding more headings (when I decide what else needs documenting this way), too.

In short, how do I do that?


I'm assuming it's something I can append to
Code:

while (<FILE>)
only because I can't see anywhere else to do it. My previous method has included effectively
Code:

while (<FILE>){
  if($_ =~ /^Servers:$/){$servers=1;}
  if($_ =~ /^Services:$/){$services=1;$servers=0;}
[...]
  if($services == 1){
    do stuff
  }elsif($servers==1){
    do other stuff
  }
}

Which strikes me as incredibly inefficient and really not the right way.


I'm sure this is a pretty common thing to need to do, but all I can find are tutorials on reading files line-by-line, and treating each line the same.

acid_kewpie 10-13-2009 05:20 AM

That's exactly what I'd do TBH, what angle do you think that there is for improvement? I suppose you could have a separate function for each header type, and pass the file handle into each different function, with that function checking for a change of header each line, doesn't seem worth it, and probably end up with extra code.

Sure you can condense the code down:

$services ? do something;
$servers ? do something_else;

or actually rejig slightly to have a single variable:

Code:

while(<file>) {
  /^(\w+):$/ ? $header = $1;
  if( $header == "Servers" ) {
    ...
  }
  ...
}

But it's only saving a few lines really... logic remains. Only other thing is to use a switch / case block instead of a series of if's...

Code:

while(<file>) {
  /^(\w+):$/ ? $header = $1;
  switch ( $header ) {
    case "Servers" {
      ...
    }
    ...
  }
  ...
}

I wouldn't hire or fire you over a difference in style / skill as narrow as that though (esp as my code is kinda pseudo and probably wrong in some way anyway!)

Lordandmaker 10-13-2009 05:25 AM

Quote:

Originally Posted by acid_kewpie (Post 3717453)
That's exactly what I'd do TBH, what angle do you think that there is for improvement?

I don't know, really. My thinking was a construct similar to:

Code:

while($line !~ /^Services$/){
  do stuff on lines before the 'Services' heading
}

I know the above wouldn't work, I was wanting a sort-of analog. Maybe I'm going a little over-the-top in presuming the way that first appears to work must be inefficient...

Cheers!

acid_kewpie 10-13-2009 05:33 AM

Well unless you were using more advanced input, e.g. XML, you're going to have to be a bit repetative. I would personally stay within the simple constraints of the file format you've got, I'd not preempt anything at all, i.e. "do this until we see this" as maybe you won't see that header.

Hmm, I suppose (similar to my intial thoughts about going OTT with separate functions) you could make the while loop more conditional in the first instance, but whilst it'd condense the code, you're not going to make it more efficient per se.

BTW I kept edited my reply, you might have missed some later bits...

Lordandmaker 10-13-2009 06:09 AM

Ah, yeah, I'm already completely sold on plain text. Also, efficiency's not really of massive concern here - it's to run on a high-powered workstation and serve the pages to four people) - I'm just trying to get into the habit of doing things sensibly rather than assuming the first thing I come up with is the correct way.

The case statement is probably more sensible, though. Certainly closer to the way I'd do it in my head (I seem to have a mental blindspot where case is concerned).

I'll carry on with it as I started, then.

Lordandmaker 10-13-2009 06:25 AM

Hm, apparently case statements are less than straightforward in perl (might be where the mental block comes from...), but there's a similarly straightforward way to do it here:
http://www.perlmonks.org/?node=How%2...20statement%3F

zhjim 10-13-2009 06:30 AM

Total overkill but interesting none the less. You could use function pointers. Just always call the same function and just point it to different one depend on the heading you found.
Here is a link how function pointers work in perl

http://www.mikey.com/mikey/2008/04/p...-pointers.html

acid_kewpie 10-13-2009 06:38 AM

Quote:

Originally Posted by zhjim (Post 3717501)
Total overkill but interesting none the less. You could use function pointers. Just always call the same function and just point it to different one depend on the heading you found.
Here is a link how function pointers work in perl

http://www.mikey.com/mikey/2008/04/p...-pointers.html

Heh, yeah that's the OTT gibberish I was trying to come up with! Actually quite elegant as long as you can work out what the code is doing.

acid_kewpie 10-13-2009 06:39 AM

Quote:

Originally Posted by Lordandmaker (Post 3717497)
Hm, apparently case statements are less than straightforward in perl (might be where the mental block comes from...), but there's a similarly straightforward way to do it here:
http://www.perlmonks.org/?node=How%2...20statement%3F

well you can "use Switch" to get a proper switch block available. Obviously as far as using libraries are concerned, you're just going to be hiding extra coding behind the scenes. Same for an XML library, might be able to traverse is elegantly, but loading the models and all is work in itself.

Telemachos 10-13-2009 07:36 AM

Quote:

Originally Posted by zhjim (Post 3717501)
You could use function pointers. Just always call the same function and just point it to different one depend on the heading you found.
Here is a link how function pointers work in perl

http://www.mikey.com/mikey/2008/04/p...-pointers.html

That tutorial recommends symbolic references which are generally considered a terrible, terrible idea and require you not to use the strict pragma (which is generally considered an essential part of well-written Perl programs).

For some ideas about why symbolic references are bad, see the following:

http://perl.plover.com/varvarname.html
http://perl.plover.com/varvarname2.html
http://perl.plover.com/varvarname3.html

Perl 5.10.1 (the current branch) supports given/when, which is like case on steroids. If you're using an earlier version of Perl, there are many good modules to get you case blocks.

zhjim 10-13-2009 09:07 AM

Quote:

Originally Posted by Telemachos (Post 3717546)
That tutorial recommends symbolic references which are generally considered a terrible, terrible idea and require you not to use the strict pragma (which is generally considered an essential part of well-written Perl programs).

Sad but true that nearly the only thing I know about perl programms is that you're better of use(ing) strict.

When I reread the Op something came to my mind I did in PHP. I check on it when I'm at home but was something like this

Code:

while ( ! feof($fh) ){
# see if line is heading
    if ( substr($line, 0, 1) == '[' ){
        $$array = array();
        next;
    }
# else assign value
    $parts = split( $line, ':' );
    $$array[$$parts[0]] = $parts[1];
}

something like this. The purpose is to create an array for every heading. And assign values to a named key within this array.
Dunno if this possible in perl and can be used within strict sense (I would wonder about this)

zhjim 10-13-2009 12:21 PM

Just for completion heres the code I used in php
Code:

while ( $line = fgets($hFile) ){
                $line = trim($line);
                # See if its a section
                $first = substr($line, 0, 1 );
                if ( $first == '[' ){                       
                        $section = section($line);                       
                        #echo "new section $section <br />";
                } else { # its a variable
                        $parts = split( ':', $line, 2);
                        $key = trim($parts[0]);
                        $value = ltrim($parts[1]);
                        $new = array( $key => $value );                       
                        $$section = array_merge( $$section, $new );                       
                }
        }


Telemachos 10-13-2009 03:11 PM

I had written a script, but then I realized that there's some key information I don't yet have. So, some questions:
  • How closely does your sample model the real data? For example, do all the entries under Servers: really begin with scan-something and do the Services: and Servers: entries always start with a capital letter? (If so, you're done. Don't get fancy: just use those facts. Ignore blank lines, ignore lines starting with a capital letter. If the initial bit of the line matches scan, it's a server. Otherwise, it's a service entry. Easy peasy.) Also, is your ftp line right? It has no associated server. That can be a problem (if you have to deal with bad splits, empty fields, etc.).
  • Do the files switch only once (a block of Server: entries, followed by a block of Services: entries) or do they go back and forth.

Edit: Since I started writing it, here's the draft. It's probably not a real solution for you, but it may help some.

Code:

#!/usr/bin/env perl
use strict;
use warnings;

my(%servers, %services);

open my $input_fh, '<', 'server_file'
        or die "Bad open on [server_file]: $!";

while (<$input_fh>) {
        next if $_ =~ /^\s+$/;
        next if $_ =~ /^[A-Z]/;
        if ($_ =~ m/^scan/) {
                my($server, $ip) = split /\s+/, $_;
                $servers{$server} = $ip
        }
        else {
                my($service, $server) = split /\s+/, $_;
                # Deal with wonky ftp entry
                if ($server eq '') {
                        $services{$service} = [undef];
                }
                else {
                        $services{$service} = [$server, $servers{$server}];
                }
        }
}

use Data::Dumper;
print Dumper \%servers, \%services;


chrism01 10-14-2009 12:05 AM

Actually, the data rules appear to be

1. starts in 1st char pos (ie '^')
2, starts with capital (eg [A-Z] )
3. one word (regex that ?)
4. ends with colon (ie ':' ) eg /:$/

so something like

/^[A-Z] [a-z].* :$/

not exact Perl regex, but you get the idea.

Edit; after some playing around I get

/^[A-Z][a-z]{1,}:$/

Telemachos 10-14-2009 06:47 AM

Quote:

Originally Posted by chrism01 (Post 3718439)
Actually, the data rules appear to be

1. starts in 1st char pos (ie '^')
2, starts with capital (eg [A-Z] )
3. one word (regex that ?)
4. ends with colon (ie ':' ) eg /:$/

so something like

/^[A-Z] [a-z].* :$/

not exact Perl regex, but you get the idea.

Edit; after some playing around I get

/^[A-Z][a-z]{1,}:$/

I hear you, but based on the data he showed, it seems even simpler: the headings are the only items that begin with a capital letter. Based on that, I left it at /^[A-Z]/.

This is partly why I called it "cheating" (since I was not being especially careful there), but also why I asked for more information about the data.


All times are GMT -5. The time now is 05:54 PM.