Subject: Far More Than Everything You Ever Wanted To Know About Switch/Case Statements and do{} blocks (was: How to do "Shell case" in perl?)
From: Tom Christiansen <[email-masked]>
Cc: merlyn,[email-masked],[email-masked]
Newsgroups: comp.lang.perl

In comp.lang.perl, [email-masked] (Hal Wine) writes:
:In article <[email-masked]>, [email-masked] wrote:
:
:>I'm a newbie in perl.

First, a pet peeve:

    s/newbie/neophyte/
    s/beginner/neophyte/
    s/a newbie in/new to/

I find "Newbie" to be a grating, overly-cutsie buzzword (read: obsequious
neologism) used by people who are either trying to be overly-cutsie or
else who don't know that "neophyte", "beginner" or "I'm new to" work just
fine.  It sounds like something you'd be likely to find in that
offensively entitled line of books called "XXX for Dummies".

Anyway, enough of these useless, idiosyncratic word things, or Larry will
yell at me. :-)

:>I wrote a shell script with
:>for $xx in $@
:>  do
:>    case $xx in
:>       a) do..;;
:>       b) do..;;
:>       *) do..;;
:>    esac
:>  done
:
:Based on the Camel book (p 98):
:
:foreach $xx ($@) {
:    CASE: {
:        $_ = $xx;
:        /^a$/ && do { ...; last CASE; };
:        /^b$/ && do { ...; last CASE; };
:        ... # *) default case code
:    }
:}
:Yes, there could be optimizations, but this is the closest mechanical
:change (IMO) to get you going.  You'll have to change the regular
:expressions from shell style to perl style, too.

Um... not quite.  Not $@ but @ARGV.    

But the essential point is correct: since you don't have an official case
or switch statement in Perl, you have to construct one on the fly,
generally using do{} as a term rather than as a do{}while/until.

For a simple operation, you can often in-line it:

    $FILE = do {
        if    ($opt_f)   { $opt_f    }
        elsif (-f STDIN) { '<&STDIN' }
        else             { $DEF_FILE }
    };  

Although plenty of other things are possible, like the 
plain ole:

    if    ($a == 1)  { f1() }
    elsif ($a == 2)  { f2() }
    elsif ($a == 3)  { f3() }
    elsif ($a == 4)  { f4() }
    else             { f5() }

Or via a jump table:

    @codes = ( \&f1, \&f2, \&f3, \&f4 );
    $c = $a - 1;
    if (defined &{ $codes[$c] } ) {
        &{ $codes[$c] }();
    } else {
        f5();
    } 

Or even as an associative array with anonymous subroutines. 
This is a much better approach than Randal's case statement
>from  chapter 5 of the Camel:

    %cmd_table = (
        key1 => sub {
                    # stuff here
                },
        key2 => sub {
                    # stuff here
                },
        key3 => sub {
                    # stuff here
                },
    );

And then to call it 

    if (defined &{ $cmd_table{$cmd} } ) {
        &{ $cmd_table{$cmd} }();
    }

Or maybe you want to find the first match:

    for (keys %cmd_table) {
        if ( /^$cmd/i ) {
            &{ $cmd_table{$_} }();
        } 
    } 

Although that has a performance issue with the run-time interpolation of
the pattern, and I'm going to avoid discussing it since this article isn't
called "Far More Than Everything You Ever Wanted To Know About Regular
Expressions".:-)

To replicate your example there using the command-line arguments, we often
use something like this:

   SWITCH: for (@ARGV) {
       /^abc/      && do {
                           $abc = 1;
                           last SWITCH;
                      };

       /^def/      && do {
                           $def = 1;
                           last SWITCH;
                      };

       /^xyz/      && do {
                           $xyz = 1;
                           last SWITCH;
                       };
       $nothing = 1;
   }

Of if $_ already has what you want, and you want to 
do some fancy stuff:

    SWITCH: {
        /foo/       && do {
                        something();
                        last SWITCH;
                    };  # semi is required
        /bar/       && do {
                        something_else();   
                        # FALLTHRU!
                    }; 
        /glarch/    && do {
                        if ($xyzzy) {
                            $_ = bizarre();
                            redo SWITCH;
                        } 
                    }; 
        # default code here
    } 

Of course, if that kind of thing make you nervous, there's no
reason you can't write it with "real" if's:

    SWITCH: {
        if (/foo/) {
            something();
            last SWITCH;
        }
        if (/bar/) {
            something_else();   
            # FALLTHRU to next test
        }
        if (/glarch/) {
            if ($xyzzy) {
                $_ = bizarre();
                redo SWITCH;
            } 
        }
        # default code here
    } 

This is a strategy people sometimes use with just plain if's, in fact:

    if ($cond) {{  # kinda confusing
        # code here
        last if $i > 10;
        # other code here
    }} 

Which could have been written:

    IF_BLOCK: {
        if ($cond) {  
            # code here
            last IF_BLOCK if $i > 10;
            # other code here
        }
    } 

Or even:

    IF: {
        if ($cond) { 
            # code here
            last IF if $i > 10;
            # other code here
        }
    } 

Or even:

    {
        if ($cond) { 
            # code here
            last if $i > 10;
            # other code here
        }
    } 

In fact, you absolutely must do such a thing if you're trying to get
out of a do{}while/until, since last/next/redo don't work there.

    FIND_J: {       
        do {
            # code here
            last FIND_J if $cond;
        } until $j > 100;
    } 

Of course, that doesn't help on the next or redo:

        do {{       
                # code here
                next if $cond;
        }} until $j > 100;

Or more clearly as:

        do {
            FIND_J: {       
                # code here
                next FIND_J if $cond;
            } 
        } until $j > 100;

Or dealing with both of them:

        OUTER_J: { 
                    do { 
        INNER_J:           {       
                                # code here
                                redo INNER_J if $cond1;
                                # code here
                                next INNER_J if $cond2;
                                # code here
                                last OUTER_J if $cond3;
                                # code here
                            }   
                    } until $j > 100;
                 }

Hmmm...  maybe we might as well just use goto's. :-) It's certainly more
than disgusting enough to make you think that just maybe next/last/redo
ought to be made to work inside do{}. :-(

In parting, I'd like to draw your attention to the trouble I've (usually)
gone through to line up parallel constructs throughout this article.  It
makes things a lot easier to read than if you jamming it all together.

Never let the GNU-Emacs/OS's pretty-printer touch this code!  :-)

--tom