marctwo has asked for the wisdom of the Perl Monks concerning the following question:

Please help me! I am just starting out in Perl and am totally confused by what is happening. I am trying to build an array of arrays in one function, pass it out in to the main scope of the script, then back into another function to print it's contents.

The array of arrays is filled correctly but when I come to print it it apparently contains the value of the last element repeated throughout the array!

Please tell me what I am doing wrong (apart from everything!

@datalist = GetLogFilesAndDatesIntoArray($logdirectory); ExtractData(@datalist); sub GetLogFilesAndDatesIntoArray { my @filenames; my @filesanddates; my @fileanddate; my @sortedfilesanddates; my $dev, my $ino, my $mode, my $nlink, my $uid, my $gid, my $rdev, + my $size, my $atime, my $mtime, my $ctime, my $blksize, my $blocks; my $filecounter; my $filename; my $logdirectory = $_[0]; #Get passed log direc +tory name chdir($logdirectory) || die "Cannot open " . $logdirectory; #Ch +ange to log file directory @filenames = <post*.*>; #Get list of UP log + files in directory $filecounter = 0; foreach $filename (@filenames) { #Get file info on every file and store name and date in array print "Checking ".$filename."\n"; ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, +$mtime, $ctime, $blksize, $blocks) = stat $filename; @fileanddate = ($filename, $mtime); $filesanddates[$filecounter] = \@fileanddate; print $filesanddates[$filecounter][0]."\n"; $filecounter++; } #Sort by date #@sortedfilesanddates = sort { $a->[1] <=> $b->[1] } @filesanddate +s; return(@filesanddates); #Return array conta +ining the data } sub ExtractData { my @filesanddates = @_; #Get the array pass +ed into the function my $logfilename; my $filedate; my $status = "Success"; my @errormessage; my $errorline = 0; my $message; my $service; my $comment; my $firstline = "true"; my $listcount; my @header; my $theline; chdir($logdirectory) || die "Cannot open " . $logdirectory; #Ch +ange to log file directory open(REPORT, ">" . $report) || die "Cannot open ".$report; #Ope +n the report file print "Checking the list\n Length is ".$#filesanddates."\n"; #then process all other logs for that day for($listcount = 0; $listcount < $#filesanddates; $listcount++) { $logfilename = $filesanddates[$listcount][0]; open(LOGFILE, $logfilename) || die "Cannot open ".$logfilename +; #*** THE LOGFILENAME IS THE LAST ELEMENT OF THE ORIGINAL ARRAY R +EPEATED *** print "Searching ".$logfilename." for errors\n"; print "listcount: ".$listcount."\n\n"; .....etc

Replies are listed 'Best First'.
Re: Passing Array of Arrays
by arturo (Vicar) on Mar 11, 2003 at 13:07 UTC

    First thought on reading this is "wow, you're doing a lot that you don't have to." There are a few stylistic issues that boil down to making your code a little more readable. But a big thing might be to forego the idea of using an array of arrays. You're dealing with filenames, which, in a given directory, must be unique. You want to record a certain bit of information about each file, so why not have the keys be the filenames and the values be those bits of information?

    For example, to get the mtimes on all your files, which is the only bit from the stat call you want to keep, you can do the following:

    my %filenames =(); foreach( <post*.*> ) { $filenames{$_} = -M $_; }
    This uses the -M filetest operator (see perldoc -f -X on your system for more info on the filetest operators) instead of that long stat call and is more mnemonic than using, say, a list slice on the stat call. ( as an aside you can ignore for the moment, you could compress to a single line with my %filenames = map { $_ => -M $_ } <post*.*>; this does pretty much the same thing as the foreach loop, but is shorter). Anyhow, with a hash, you would sort it by the mod time via something like the following:
    my @sorted_keys = sort { $filenames{$a} <=> $filenames{$b}} keys %file +names;
    (you don't need to name the array, but I just wanted to illustrate how you get the set of keys of the filename hash sorted according to values).

    Now, for your original question (finally!). I think what's going on is a scoping issue. You define @fileanddate outside the loop, and put a reference to it into the array you're returning each time you process a file. But there is only one @fileanddate, so you're filling up your returned array with a bunch of references to that same thing. A better approach, keeping your current code for the most part, would have your loop that constructs the array look like this:

    foreach my $filename ( @filenames ) { # create an ANONYMOUS array (basically a reference to an array) my $record = [ $filename, -M $filename ]; push @filesanddates, $record; }
    This ought to do what you want with much less fuss. An absolutely minimal fix would just move the declaration of @fileanddate into the subroutine (update NO! array building loop ... ooops. ), so you get a "fresh" lexically-scoped variable on each iteration through the loop.

    HTH!

    If not P, what? Q maybe?
    "Sidney Morgenbesser"

Re: Passing Array of Arrays
by broquaint (Abbot) on Mar 11, 2003 at 12:53 UTC
    I am trying to build an array of arrays in one function, pass it out in to the main scope of the script, then back into another function to print it's contents.
    Something like this perhaps
    sub ret_arr { return( ["foo"],["bar"],["baz"] ) } sub print_arr { print @$_ for @_ } my @arr = ret_arr(); print_arr(@arr); __output__ foobarbaz
    Could I also suggest a refactoring of your first function (now just returns a list of files ordered by mtime)
    use IO::Dir; sub files_by_mtime { my $d = IO::Dir->new($_[0]) or die "ack: $!"; return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [ $_, (stat "$_[0]/$_")[9] ] } grep { /^post.*\../ } $d->read; }

    HTH

    _________
    broquaint

Re: Passing Array of Arrays
by robartes (Priest) on Mar 11, 2003 at 13:07 UTC
    First you do, in sub GetLogFilesAndDatesIntoArray :
    my @fileanddate;
    --> you define this as a lexical array for this sub.

    Then, in the foreach loop, you do:

    @fileanddate = ($filename, $mtime); $filesanddates[$filecounter] = \@fileanddate;
    --> After setting it, you push a reference to @fileanddate onto @filesanddates.

    That's your error. All of the references you push onto @filesanddates point to the same array, @fileanddate. Hence, all of these point to the same piece of data. This piece of data is the one you last put into it - the last set of (filename, mtime).

    To solve this, just make sure that the reference you push onto @filesanddates is to something that is scoped locally to the foreach block. For example:

    foreach my $filename (@filenames) { my $mtime=(stat ($filename))[9]; my @fileanddate=($filename, $mtime); # Note lexical scoping of @fileanddate push @filesanddates, \@fileanddate; }
    Or better yet:
    foreach my $filename (@filenames) { push @filesanddates, [ ( $filename, (stat ($filename))[9] ) ]; }

    For more information on scoping, have a look at this tutorial.

    CU
    Robartes-

      Thank you thank you thank you all!! I can't believe the speed and quality of these responses. This is truely an enlightened forum :)

      I followed Robartes suggestion of lexical scoping and it works now. This seemed to be the simplest modification to my existing code and something I understood right away. However, I shall certainly read through all the excellent suggestions and try to understand and incorporate them soon.

Re: Passing Array of Arrays
by Tanalis (Curate) on Mar 11, 2003 at 12:56 UTC

    When you're passing arrays and hashes into and out of subs, it's (normally) necessary to pass a reference to the array, rather than the array itself.

    If you don't pass a reference, the array is flattened, and passed out as a simple list - not something you want in this case.

    You can correct this by having the GetLogFilesAndDatesIntoArray sub return a reference to the AoA, rather than trying to flatten the AoA and then return it. This is easy to do - just change the return to return \@filesanddates;.

    Once you have the sub returning a reference, you should assign it to a scalar variable, rather than an array (a reference is always a scalar, regardless of what it refers to), and you can then use this variable to pass the array into the ExtractData sub - like this:

    # note: $datalist now, it's a reference $datalist = GetLogFilesAndDatesIntoArray($logdirectory); ExtractData($datalist); # passes the reference into the sub

    Finally, once you have the reference going into the ExtractData sub, you can deference it to retrieve the original array like this:

    my $_filesanddates = shift; @filesanddates = @$_filesanddates; # dereferences the reference

    There's a whole load of stuff on references and subroutines in perlsub and perlref.

    Update: Thanks to broquaint for pointing out that I'm talking absolute rubbish .. oh well, eh. References are still a Good Thing in my book, but I guess that's a preference now, rather than a requirement...

    Hope that helps a little ...
    -- Foxcub
    A friend is someone who can see straight through you, yet still enjoy the view. (Anon)
    All code is untested unless explicitly stated otherwise.

      If you don't pass a reference, the array is flattened, and passed out as a simple list - not something you want in this case.
      Er, wha? This is a perfectly valid case to pass a list back and forth. Since the code is only dealing with a single array this is not an issue, it would be an issue however if multiple arrays were being passed about e.g
      sub f { print "got: ", @_, $/ } my @a = qw( one two three ); my @b = qw( four five six ); ## @a & @b will be flattened into a single list f(@a, @b); ## pass references to keep array integrity f(\@a, \@b); ## only @a is passed, so only @a is received f(@a); __output__ got: onetwothreefourfivesix got: ARRAY(0x8107e50)ARRAY(0x8107f34) got: onetwothree

      HTH

      _________
      broquaint

        Eh?

        I was always told that "complex" structures (anything other than a "simple" array or hash - and the OP is talking about an AoA) got flattened, and that its integrity wasn't necessarily preserved when it was passed into and out of a sub.

        I'm quite happy to be wrong, though .. that's been a pet peeve of mine with Perl for *ages* ... *grin* ..

        My thinking this (probably) comes from perlsub ..

        Perl sees all arguments as one big, long, flat parameter list in @_. Like the flattened incoming parameter list, the return list is also flattened on return.

        I read this as saying that if I have a crazily bizarre structure, such as an AoA or AoH or whatever, its integrity and structure will not necessarily be preserved - epecially not if it becomes a simple list of scalars.

        Can anyone clarify this? I'm gonna play with some code, see what happens ..

        Update: Ok, so code playing proves I'm completely wrong ... all kudos to broquaint - I've learnt something completely new today .. and got rid of a pet hate :)

        -- Foxcub
        A friend is someone who can see straight through you, yet still enjoy the view. (Anon)

Re: Passing Array of Arrays
by nite_man (Deacon) on Mar 11, 2003 at 13:30 UTC
    You pushed in result array a array ref:
    $filesanddates[$filecounter] = \@fileanddate;
    and you should use following:
    $logfilename = $filesanddates->[$listcount][0];
    instead of
    $filesanddates[$filecounter] = \@fileanddate;
    But I would like to offer you another way:
    !/usr/bin/perl -w use strict; # You can use this module for dumping your data. use Data::Dumper; # Function which forms an files array sub one { my $dir = shift; # Dir name opendir(DIR, $dir) or die "Can't open $dir: $!"; my @files = grep { !(/^\./) && /^post/ && -f } readdir(DIR); closedir(DIR); my $res; # Array reference which will consist results @$res = map { [$_, (stat($_))[9]] } @files; return $res; } # Function which processes result array sub two { my $array = shift; for(@$array) { print $_->[0] .': '.$_->[1]."\n"; <do something> ... } } # Call our functions two(one('/var/logs'));

    ========================
    { firstname => 'Michael',
    quote => 'Mice were crying and stinging but went on nibbling the cactus', }

Re: Passing Array of Arrays
by SheridanCat (Pilgrim) on Mar 11, 2003 at 22:15 UTC
    You've gotten plenty of good replies. Here's a tip. Try using Data::Dumper to inspect your data structure if you get confused about what's going on.

    Just

    use Data::Dumper;

    and then pass your data structure to it:

    print Dumper( \@my_complicated_array );

    and you'll get a nicely formatted view of your data structure on screen or wherever STDOUT is pointing. One caveat: if your structure is too large it can take a long time to dump. Regards, Cat