http://qs321.pair.com?node_id=697542

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

I am trying to create a subroutine that will take in an array of MAC addresses and simply return a different format to match that of the device I'm telnetted into. For example, the MAC 001589F453CA needs to be changed to AP0015.89F4.53CA I can get this to work fine with a static scalar, but run into problems when I try to pass an array to the subroutine. I'm not sure what everyone feels is a lot of code, so I'll post this code as this is my first post. I have done extensive rewrites to this over the last several days before asking anyone else for help (I've read about folks just dumping their problems and expecting someone else to find it). You'll see from some of my print statements I'm trying to track the values of my variables within my code to find out where it breaks. But at this point, I'm just getting garbage by the third iteration of my loop.
#!c:/Perl/bin/perl my @mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03C"); # CONVERT THE MAC ADDRESS ############################################ +############## sub convert () { my @array = @_; print "Inside convert: @array\n"; my @hold; my $j = 0; my $length = scalar(@array); #Number of records in the input array print "Length of inner array: $length\n"; for ($j; $j<$length; $j++) { #This loop is setup so that its iteration +s equal the number of elements in the input array print "Value of j is: $j\n"; $element = lc($array[$j]); #Convert uppercase MAC to lowercase print "Inside element: $element\n"; my $i = 0; @hold = (); #Reset the temporary hold array used to create the $ou +t variable while ($i<length($element)) { #for each element of the array, brea +k it up into the 3 groups of 4 letters each push (@hold, substr($element, $i, 4)); print "While substring: " . substr($element, $i, 4) . "\n"; $i += 4; print "Just finished substring: @hold\n"; } my $out = "AP" . $hold[0] . "." . $hold[1] . "." . $hold[2]; #Store th +e results to a variable to be added to output array print "Out: $out\n"; push (@array, $out); #Add the newly converted MAC address to our array shift (@array); #Remove the original MAC addresses one at a time as th +ey're not needed outside the sub } return @array; print @array; } # CONVERT THE MAC ADDRESS ############################################ +##############
Thanks, Scott

Replies are listed 'Best First'.
Re: Corrupt Data?
by friedo (Prior) on Jul 14, 2008 at 19:41 UTC

    The reason you're getting garbage at the end of your loop is because you're pushing stuff onto @array, then shifting stuff off the front, but looping over @array sequentially. You can't alter the thing you're looping over and expect sane results.

    BTW, you don't need to use a C-style for loop to iterate over an array, it's much easier to write it this way:

    my @results; foreach my $addr( @array ) { my $element = lc $addr; # mess with $element here push @results, $element; } return @results;
    You also don't need to reset @hold each time through the loop if you declare it with my, since it will be lexically scoped to the enclosing block. You'll catch a number of other problems if you turn on strict and warnings; put this at the top of your script:
    use strict; use warnings;
    and fix stuff until it stops yelling at you. Finally, the print @array statement at the end of your sub will never get executed because you return on the previous line!

    I think that should be enough to get you well on your way. Good luck!

      You can't alter the thing you're looping over and expect sane results.

      While that's true, it's not the case here. He's not looping over the array.

        But he is accessing it positionally (with the for loop) while pushing onto it and shifting it at the same time. That means he's going to end up running his transformation on output data, plus throwing away data that should have been transformed, etc.
      Thanks for your posts, guys! Almut - I thought that the variable @_ would take whatever was passed to it, even for a subroutine without any explicit arguments? Therefore, I should be able to pass either a scalar or an array, without having to explicitly define arguments. friedo - Thanks for your help in finding out how I was corrupting my own data. Regards, Scott
        I thought that the variable @_ would take whatever was passed to it, even for a subroutine without any explicit arguments?

        Thing is that if you declare sub convert () {...} you're telling Perl that the routine takes no arguments... In other words, if you're then going to call the routine with any arguments, you'll just get the error Too many arguments for main::convert ...

        Without any prototype, however, Perl already does exactly what you want, i.e. you can pass one or several arguments...

        even for a subroutine without any explicit arguments

        I don't know where you got the term explicit or what you want it means in this context, but what almut meant was that the "()" in "sub foo () { ... }" means the subroutine takes no arguements. Drop the "()": "sub foo { ... }".

        Update: Too slow and credited the wrong monk. Fixed the latter.

Re: Corrupt Data?
by almut (Canon) on Jul 14, 2008 at 19:37 UTC

    How are you calling the convert() routine? You're declaring a no-arguments prototype (i.e. the empty parentheses), but are then assigning my @array = @_; (presumed to be arguments) at the beginning of the routine. This doesn't make much sense. Similarly, printing something after the return ... :)

    Update: BTW, you could also use unpack to simplify the function, e.g. something like

    my @mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03C"); sub convert { return map { "AP" . join ".", unpack "(A4)*", lc $_ } @_; } print "$_\n" for convert(@mac_addrs);

    Output:

    AP0015.faa3.f03a AP0015.faa3.f03b AP0015.faa3.f03c
Re: Corrupt Data?
by ikegami (Patriarch) on Jul 14, 2008 at 20:36 UTC
    Aside from what's already been said,
    my $j = 0; for ($j; $j<$length; $j++)
    could be better written as
    for (my $j = 0; $j<$length; $j++)
    and even better as
    for my $j (0..$#array)
      Thanks for the discussion everyone...I always do my homework first, but the web has just as many bad examples and poorly written documentation as it does good ones. In any case, the goal here is for all of us, particularly myself, to become better programmers. To that end, since I didn't find any examples as unique as Almut's use of the unpack function, do I understand its use thusly:

      return - we want to return a value
      map - we're going to use the map function to evaluate each array node
      "AP" . join "." - I understand what this is doing, but I don't understand from any documentation what the first argument to map is, generally speaking (everything before the first comma)
      unpack - I see from documentation that the capital 'A' for unpack refers to any ASCII character with its whitespace, but again I don't understand from documentation that you can add the ordinal and/or * along with it, and how that modifies the query
      lc $_ - perform the lowercase operation on the current scalar/node of the array
      @_ - Store the results to the unnamed array @_

      If anyone has any additional comments and/or suggestions regarding my understanding, please feel free. In addition, any suggestions for comprehensive documentation is appreciated. I currently use search engines to find as many examples of a particular piece of code until it clicks with me and I understand it.

      Regards, Scott
        I will (too) try to explain and detail almut's answer:
        my @mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03C"); sub convert { return map { "AP" . join ".", unpack "(A4)*", lc $_ } @_; } print "$_\n" for convert(@mac_addrs);
        Well:
        •  @answer = map { do_something_with("$_") } @array transforms one @array into another array @answer where each element of @answer is the result of the transform do_something_with(). Try this:
          $ perl -le '@a = (2, 3, 4, 5); @b = map { 2 * $_ } @a; print for @b' 4 6 8 10
          Notice that @b has each of the elements of @a, doubled. The result of the block given to map can be an array, too, so:
          $ perl -le '@a = (2, 3, 4, 5); @b = map { $_ % 2 ? () : ($_, $_ / 2) } + @a; print for @b' 2 1 4 2
          Notice that it printed the number and its half -- for the even numbers in @a.
        • So, let's try the knowledge of map:
          $ perl -le '@mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03 +C"); @a = map { lc $_ } @mac_addrs; print for @a' 0015faa3f03a 0015faa3f03b 0015faa3f03c
        • Now, let's add unpack"(A4)*" to the equation (unpack any number of groups of four alphanumeric chars):
          $ perl -le '@mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03 +C"); @a = map { unpack "(A4)*", lc $_ } @mac_addrs; print for @a' 0015 faa3 f03a 0015 faa3 f03b 0015 faa3 f03c
        • Now, let's join the parts with ".":
          $ perl -le '@mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03 +C"); @a = map { join ".", unpack "(A4)*", lc $_ } @mac_addrs; print f +or @a' 0015.faa3.f03a 0015.faa3.f03b 0015.faa3.f03c
        • And then, add the "AP" in the beginning:
          @mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03C"); @a = ma +p { "AP" . join ".", unpack "(A4)*", lc $_ } @mac_addrs; print for @a +' AP0015.faa3.f03a AP0015.faa3.f03b AP0015.faa3.f03c
        • voilą!! you already have your answer. Now, package everything in a neat subroutine:
          $ perl -le ' > use strict; > my @mac_addrs = ("0015FAA3F03A", "0015FAA3F03B", "0015FAA3F03C"); > sub convert { > map { "AP" . join ".", unpack "(A4)*", lc $_ } @_ > } > print for convert @mac_addrs > ' AP0015.faa3.f03a AP0015.faa3.f03b AP0015.faa3.f03c
        Got it?
        []s, HTH, Massa

        The first argument of map is code executed for every other argument of map.

        print map uc($_), qw( a b c d ); # Prints ABCD print map { uc($_) } qw( a b c d ); # Same thing

        unpack '(A4)*', $val splits the value into as many 4 byte blocks as possible. I would have used "W" instead of "A", though. unpack '(W4)*', $val splits the value into as many 4 character blocks as possible.

        @_ is not unamed —its name is @_— and nothing is being stored in it —its contents are being passed to map.

        @_ holds the arguments of the current function (convert).

        "AP" . join "." - I understand what this is doing, but I don't understand from any documentation what the first argument to map is, generally speaking (everything before the first comma)
        map this, that will do this to each element of that, with $_ being aliased to each of the elements

        For example

        my @in = 1 .. 5; my @out = map $_ * 2, @in; # doubles each element of @in @out = map $_ * 2, 1 .. 5; # same thing - any list will do

        Unless I state otherwise, all my code runs with strict and warnings