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

Hey fellow perl monks! I recently discovered the use of the following loop control statements:
next last redo
However, I don't think I've quite understood their use. Say, I have a set I wish to iterate through. I'd like to iterate through a particular block while i = 0, and have the loop go through a 2nd block for values i >= 1.
#!/bin/perl use strict; use warnings; sub newl { print "\n"; print "\n"; } my @a; my @b; my $na; my $Na; $na = $#a; $Na = ( 0, 1..$na ); @a = ( /, /home, /var, /tmp, /var/tmp ); for my $i (@Na) { $b[$i] = `ls $a[$i]`; } for my $i (@Na) { my @c1 = split ("\n", $b[$i]); my $nc; my @Nc; $nc = $#c1; @Nc = ( 0, 1..$nc ); for my $w (@Nc) { print "$a[$i]$c1[$w]"; newl; } last if $i == 1; @c1 = split ("\n", $b[$i]); $nc = $#c1; @Nc = ( 0, 1..$nc ); for my $w (@Nc) { print "$a[$i]/$c1[$w]"; newl; } }
However, this goes through the first block at i=0 once, then goes through the second block at i=0, and stops again right before i=1. What is the best way for me to accomplish me going through the first block in the loop for i=0, and iterate through the second block for all values of i>=1 ?

Replies are listed 'Best First'.
Re: Loop Control
by Athanasius (Archbishop) on Jul 04, 2012 at 03:23 UTC

    Hello slugman, welcome to the Monastery!

    The code you posted has a number of issues:

    my @a; ... my $na; my $Na; $na = $#a; $Na = ( 0, 1..$na ); @a = ...; for my $i (@Na)...

    $Na needs to be @Na here. However, even when this is fixed the code can never work as you want, because $na is initialised before @a is populated, so it will always be 0. The simplest solution is to forego both these variables and code the loop directly:

    for my $i (0 .. $#a)

    Update: And in the assignment to @a, the elements /, /home, etc. are strings which need to be quoted.

    In fact, the whole code snippet can benefit greatly from being simplified:

    use strict; use warnings; my @a = qw( / /home /var /tmp /var/tmp ); my @b; $b[$_] = `ls $a[$_]` for (0 .. $#a); for my $i (0 .. $#a) { my @c = split("\n", $b[$i]); printf("%s%s%s\n\n", $a[$i], ($i ? '/' : ''), $c[$_]) for (0 .. $# +c); }

    This does what (I think) you want, in about half the lines and using only 4 variables. Yes, it may appear cryptic due to its heavy use of Perl idioms — but these idioms are well worth learning, as they make the code simpler (and, the simpler the code, the easier it is to debug and maintain).

    The loop controls next, last, and redo are often useful, but in this case the ternary operator ? : seems a better fit to the task at hand. (I see aaron_baugher got in ahead of me on this one.)

    HTH,

    Athanasius <°(((><contra mundum

      the whole code snippet can benefit greatly from being simplified:
      use strict; use warnings; my @a = qw! / /home /var /tmp /var/tmp !; my @b = map { chomp( my @x = `ls $_` ); [ @x ] } @a; for my $i ( 0 .. $#a ) { print $a[ $i ], $i ? '/' : '', $b[ $i ][ $_ ], "\n\n" for 0 .. $#b +; }

      Or perhaps better as:

      use strict; use warnings; my @a = qw! / /home /var /tmp /var/tmp !; my @b = map [ m!/$! ? <$_*> : <$_/*> ], @a; print map "$_\n\n", @$_ for @b;
      Thanks Athanasius, I am fond of the simplification you provided! Also, I apologize for the sloppy code, that's what I get for copying and pasting snippets from the original without paying atttention! Also, thats the 2nd time that ternary operator has been metioned
      ? :
      This operator is new to me.. I've been heavily relying on the O'reilly Perl bible. Although ternary operators are defined, there is no concrete definition /or examples for ? :, in the book!
Re: Loop Control
by davido (Cardinal) on Jul 04, 2012 at 01:56 UTC
    if( $i == 0 ) { for my $w ( @Nc ) { } } else { for my $w ( @Nc ) { } }

    Or since the loops themselves are the same, differing only in their content:

    for my $w ( @Nc ) { if( $i == 0 ) { # First pass actions } else { # Subsequent pass actions. } }

    By the way, the code you posted doesn't compile.


    Dave

      Thank you Dave! For some reason, I forgot that I could use a if statement nested in the for loop. I'll need to remember that for the future. Also, I apologize that the code I posted didn't run -- thats what I get for copying and pasting from the original! Please take a look at my latest comment to the thread for the latest, as this actually runs! :)
Re: Loop Control
by aaron_baugher (Curate) on Jul 04, 2012 at 03:19 UTC

    It looks to me like the only difference between your two "blocks" is that when $i == 0 you want your print statement done without a slash, and otherwise it has a slash. Correct? In that case, I'd limit the test to that particular line:

    print "$a[$i]", ( $i ? '/' : '' ), "$c1[$w]";

    Then the rest of your block doesn't need to be duplicated at all.

    Aaron B.
    Available for small or large Perl jobs; see my home node.

      Yes, that is true Aaron. Thank you for the clever answer! I'm not quite sure how it works, so I'll have to run this in a few test scripts.. Your feedback is greatly appreciated :)

        You're welcome. The confusing part there might be the ternary operator, which you can learn more about in perlop, under Conditional Operator. It's more-or-less an if-then-else conditional, but since it's a single expression, it's easier to plug into a print statement like that. If Perl didn't have it, I'd probably do the same thing with something like this:

        { # start a block to limit the va +riable's scope my $maybe_a_slash = ''; # default to no slash $maybe_a_slash = '/' if $i; # make it a slash if counter ha +s incremented print "$a[$i]$maybe_a_slash$c1[$w]"; } # end the block

        Aaron B.
        Available for small or large Perl jobs; see my home node.

Re: Loop Control
by xyzzy (Pilgrim) on Jul 04, 2012 at 11:18 UTC

    You can also rework this to completely eliminate the conditional

    Never add a slash to the print statement:

    ... @a = qw| / /home/ /var/ /tmp/ /var/tmp/ |; # afaik ls doesn't care if there is a trailing slash for directories ...

    Or if you think that trailing slashes look ugly in the qw, you can have it always added to the print (and ls/glob) statement:

    ... @a = ('','/home','/var','/tmp','/var/tmp'); @b = map { chomp(my @x = `ls $_/`);[@x]} @a; # $_ interpolates to `ls /` for @a[0] # do the same for the print statement

    As an aside, I don't want to insult/piss off anyone (oh who the fuck am I kidding, this is the internet...) but after staring at this for 5 minutes my first thought was "wow... someone really has nothing better to do than troll PM with BBC". Maybe this really is the kind of perl code that someone would write after hacking extremely low-level C for however long, I wouldn't know, I am far less proficient in C than I would like to be. I know that pre-declaring a variable for every single value that might ever be needed and iterating through arrays by index is C-mentality. Still, things like print "\n";print "\n" are mind-boggling at 5 am, and I should hope that would be true in any language. Anyway... if you really are just learning perl you will find that many things are a lot simpler than you might expect with proper use of map and grep. and if not, then thank you for my daily dose of wtf did i just read?


    $,=qq.\n.;print q.\/\/____\/.,q./\ \ / / \\.,q.    /_/__.,q..
    Happy, sober, smart: pick two.
Re: Loop Control
by slugman (Novice) on Jul 05, 2012 at 23:52 UTC
    Hey guys, thanks so much for the productive feedback! Im still very much a newbie with lots to learn. So far I am enjoying my inductance to the monastary very much :) Also, I apologize if the code didn't run. Thats what I get for copying and pasting selective statements from my original... oh well. Also, I know it seems like a lot of work for printing subdirectories, but the reason I went throught the trouble is because I wanted to manipulate the strings in such a way as so that I could eventually populate these strings into a separate array, and then pass them to the shell to perform commands on. After doing some reading, I finally figured out that the c-like for loop is the answer to what I was looking for. Here is what my real code looks like now:
    #!/bin/perl use strict; use warnings; sub newl { print "\n"; print "\n"; } my @a; my @b; my @c; my @d; my @e; my $na; my @Na; #@a = `df -h | grep mapper | cut -c 48-`; # My original code uses the above statement, but because # this may look different on different shells, I used the # array below @a = ( "/", "/home", "/var", "/tmp", "/var/tmp", "/boot" ) $na = $#a; @Na = ( 0, 1..$na ); for my $i (@Na) { $b[$i] = `ls $a[$i]`; } print @a; newl; for my $i (@Na) { print "\$b[$i] = $b[$i]"; newl; } for (my $i = 0; $i < 1; $i++ ) { my @c1 = split ("\n", $b[$i]); my $nc = $#c1; for (my $w = 0; $w <= $nc; $w++ ) { my $z0; my $z1; my $z2; $z0 = $a[$i]; chomp $z0; $z1 = $c1[$w]; $z2 = $z0 . $z1; #print $z2; newl; push (@c, $z2); }} for (my $i = 1; $i <= 5; $i++ ) { my @c1; @c1 = split ("\n", $b[$i]); my $nc = $#c1; for (my $w = 0; $w <= $nc; $w++ ) { my $z0; my $z1; my $z2; $z0 = "$a[$i]"; chomp $z0; $z1 = "\/$c1[$w]"; $z2 = $z0 . $z1; #print $z2; newl; push (@c, $z2); }} print "\$#c = $#c"; newl; for (my $i = 0; $i <= $#c; $i++ ) { my $y = `du -B MB -d0 $c[$i]`; push (@d, $y); } print "\$#d = $#d"; newl; for (my $i = 0; $i <= $#d; $i++ ) { print "$d[$i]"; newl; } for (my $i = 0; $i < 1; $i++ ) { my @c1 = split ("\n", $b[$i]); my $nc = $#c1; for (my $w = 0; $w <= $nc; $w++ ) { my $z0; my $z1; my $z2; $z0 = $a[$i]; chomp $z0; $z1 = $c1[$w]; $z2 = `ls -last $z0 | grep $z1`; chomp $z2; push (@e, $z2); }} for (my $i = 1; $i <= 5; $i++ ) { my @c1; @c1 = split ("\n", $b[$i]); my $nc = $#c1; for (my $w = 0; $w <= $nc; $w++ ) { my $z0; my $z1; my $z2; $z0 = "$a[$i]"; chomp $z0; $z1 = "$c1[$w]"; $z2 = `ls -last $z0 | grep $z1`; chomp $z2; push (@e, $z2); }} print "\$#e = $#e"; newl; for (my $i = 0; $i <= $#e; $i++ ) { print "$e[$i]"; newl; }
    Basically this is my attempt to create a perl script to list size and usage data on subdirectories in my system. This is the template script, my goal is to use Text::CSV to output the data in the arrays @c, @d, & @e into a csv file. Now that I got my first part working, I can go onto part two! I had such a hard time intitially, becuase I was using the output from du -B MB, which would just create variable length strings. If I wanted to I guess I could go that route, but I figured I'd just set the max-depth to 0, to get the values only for the directory i'm evaluating. I know this is a *nix oriented script, eventually I'd like to create something like this to run on windowz. :)

      Here are some pointers to help you assimilate to a more Perlish way of thinking (RESISTANCE IS FUTILE)

      • I can see the point of the newl() subroutine if you want to be able to globally change what is printed after each thing of interest, but here are some ways you can make it look nicer:
        • The most obvious is combine the two prints into print "\n\n";
        • Since you seem to always use it immediately after a print statement, why not just declare it as a variable? $NEWL="\n\n"; print "Pass it after the string like so", $NEWL; print "Or you can just interpolate$NEWL"
        • If you read through perlvar, you will find a section about $\, the output record separator. This is a special variable that is printed after every print statement. Instead of making a call to newl every time, just let Perl do all the work. local $\ = "\n\n"; print "No more newls!" You can always unset it with undef $\;
      • my (and similar operators like our and local) can be applied to any number of variables at once: my (@a, @b, @c, @d, @e);. You can even assign things this way: my ($z1, $z2) = ($a[$i],$c1[$w]);
      • assignment returns the lvalue (not the rvalue!), which means chomp(my $a = $b); is equivalent to my $a = $b;chomp $a;
      • the range operator can use any integer (or even string) values, providing the left one is smaller than the right. (0, 1..$n) can be written as (0..$n) with no ill effects.
      • qw// is often preferred when making lists of strings that do not contain spaces. Like most operators in the quote-like family, you can use any delimiters you want: @a = qw(/ /home /var /tmp /var/tmp)
      • Off the top of my head, I can't think of an instance where $a = $b would give a different result than $a = "$b". I'm sure someone out there knows enough voodoo to provide some rare case where the latter would be preferred (perhaps if you were overloading stringification?), but in general Perl doesn't care if the scalar you give it is a number of a string because it will convert freely between the two. Maybe if you wanted to clean up a number stored as a string you could do something like $a = "05.230000"; $b = $a+0; # $b is 5.23 (number), but interpolating variables by themselves is a little redundant.

      There are other things that could be greatly simplified as well, but those are just some of the more critical stylistic faux pas that jumped out at me. Honestly, if your code works exactly as expected, then there's not really anything wrong with it :) And if you can understand why and how it works, then it's Good Enough(tm). When I was just starting out with Perl, map confused the hell out of me, so I never used it and I always avoided solutions that people gave me that used it. Then when it clicked I thought "Holy llama balls, this shit is The Shit" and tried using it for everything. Neither ways are necessarily the best approach. In the end, all that matters is that you're constantly learning how and when to use the unique features of Perl that make it so slackfully elegant. Just lurk around and look at code examples passed around and you'll start to get it.

      But seriously, do some experimenting with map, because it will greatly simplify any operation where you have to turn one set of values into another.


      $,=qq.\n.;print q.\/\/____\/.,q./\ \ / / \\.,q.    /_/__.,q..
      Happy, sober, smart: pick two.
        Off the top of my head, I can't think of an instance where $a = $b would give a different result than $a = "$b".
        This will explode horribly if $b is a reference. Compare the following:
        [ateague@dingbat mod]$ perl -MData::Dumper -E '$b = [qw/1 2 3 4 5/]; $ +a = $b; say Dumper $a;' $VAR1 = [ '1', '2', '3', '4', '5' ]; [ateague@dingbat mod]$ perl -MData::Dumper -E '$b = [qw/1 2 3 4 5/]; $ +a = qq|$b|; say Dumper $a;' $VAR1 = 'ARRAY(0x12609e4)';
        The first one prints the array referenced by the reference. The second one simply prints a string containing the memory address of the array reference.