par
par

Reputation: 17724

Conversion of list context result to array in one line in perl?

I wrote this code in perl:

shift( @interfaces = qx'ifconfig -s' );

And got this error:

Type of arg 1 to shift must be array (not list assignment)

When I write it this way:

@interfaces = qx'ifconfig -s';
shift @interfaces;

It does what I want, which is to get the output of the ifconfig command as an array of lines and remove the first element in the array (which is a header, not an actual interface).

My personal preference is to write this as a one liner. It seems to me the parentheses in the first example should cause the assignment to be resolved fully, therefore allowing shift to see @interfaces as an array, but clearly perl thinks it's a "list assignment."

This is surely an easy question for the perl gurus but I've googled and googled and haven't found enlightenment.

If someone would please provide the specific semantics to accomplish what I want in one line I would appreciate it. If you would also please take the time to explain why my first version isn't working I would be eternally grateful (teach a man to fish and all that).

Thank you in advance for your help.

Upvotes: 10

Views: 3898

Answers (6)

Joel Berger
Joel Berger

Reputation: 20280

Starting with Perl version 5.10 you can use the state variable declaration to manage the persistance of a variable without having to predeclare with my outside the loop.

use 5.10.0; 
my @interfaces = grep {state $i++} qx'ifconfig -s'; 

yes you can do it without state, but its a perfect use case for it. Here is the analogous code without state and same lexicality w.r.t $i.

my @interfaces;
{
  my $i;
  @interfaces = grep {$i++} qx'ifconfig -s';
}

or

my @interfaces = do { my $i; grep {$i++} qx'ifconfig -s' };

of course you can not care about $i's lexicality and simply do

my $i;
my @interfaces = grep {$i++} qx'ifconfig -s';

or you can cheat

my @interfaces = grep {$|++} qx'ifconfig -s'

but that breaks if you rely on $| somewhere else. Never mind all that, just use state.

Upvotes: 2

mob
mob

Reputation: 118605

shift @{EXPR} is valid syntax, so

shift @{$interfaces = [qx'ifconfig -s']}

will give you an array reference that has the first element removed.

I discovered this from the diagnostics output about calling shift from list assignment:

$ perl -Mdiagnostics -e 'print shift(@a = (2,3,4))'
Type of arg 1 to shift must be array (not list assignment) at -e line 1, at end of line
 Execution of -e aborted due to compilation errors (#1)
    (F) This function requires the argument in that position to be of a
    certain type.  Arrays must be @NAME or @{EXPR}.  Hashes must be
    %NAME or %{EXPR}.  No implicit dereferencing is allowed--use the
    {EXPR} forms as an explicit dereference.  See perlref.

Perl enforces this behavior on any user-defined subroutine or builtin that is prototyped with \@ or \% characters. The prototype is a clue for the interpreter that Perl should treat an array or hash function argument as an array or hash type, and not try to unroll the list into several arguments.

One way to think about it (though I'm not sure if this is accurate for the builtins) is that Perl will read the array or hash variable from your list of arguments to a function call, but actually pass a reference to that variable to the prototyped function. So the interpreter needs to identify an array or hash in your argument list, and it needs to be able to get a reference to that array or hash. Perl doesn't or can't (hands waving here) do that with a list assignment expression -- note that the result of

\(@a = (1,2,3))

is a list of 3 references to scalars, not a reference to a list with 3 scalars.

You can see the prototypes (if any) for most of the Perl builtins with the prototype function:

$ perl -e 'print prototype("CORE::shift")'      ===>   \@
$ perl -e 'print prototype("CORE::each")'       ===>   \%
$ perl -e 'print prototype("CORE::push")'       ===>   \@@

Upvotes: 7

Eric Strom
Eric Strom

Reputation: 40152

As you saw, shift requires a literal array, not the result of an assignment. This is because when perl parses shift @interfaces it is actually rewriting it into something like &CORE::shift(\@interfaces) and you can not take a reference of an assignment and get an array ref.

You could break it into two lines as you have found, you could hide the assignment inside a bracketed dereference as mob shows, or you could simply throw away the first value:

(undef, @interfaces) = qx'ifconfig -s';

undef in an lvalue position is a placeholder for values that you don't need.

(parsing of shift has changed a bit in perl 5.14+, but the argument above still holds)


a few more ways that you probably shouldn't use, ordered only by increasing length :)

my @interfaces = sub {shift; @_}->(qx'ifconfig -s');

my @interfaces = sub {@_[1..$#_]}->(qx'ifconfig -s');

my @interfaces = map {@$_[1..$#$_]} [qx'ifconfig -s'];

my @interfaces = map {shift @$_; @$_} [qx'ifconfig -s'];

our @interfaces; shift @{*interfaces = [qx'ifconfig -s']};

my @interfaces = sub {*_ = [qx'ifconfig -s']; shift; @_}->();

Upvotes: 13

David W.
David W.

Reputation: 107040

Try this:

shift qw(this that the other);

You'll get the same error message. The shift command must take a list variable and not a list itself. After all, there are two major affects with the shift command.

  1. It returns the value of the first element of the list
  2. It also removes the value of the first element from the list variable. If there is no list variable, shifting it wouldn't make any sense.

In your example, the (@interfaces = qx 'ifconfig -s') is setting @interfaces and is returning the value of the list @interfaces and not the variable itself to the shift command.

Mob's answer helps you a bit. You'll get a list reference, but then, you'll either have to keep dereferencing it, or set an actual list variable:

shift @{$interfaces = [qx'ifconfig -s']}
foreach my $entry (@{$interfaces}) {   #Have to dereference
   say "Interface: $entry";
}
@interfaces = @{$interfaces};          #This will also work.

If the whole purpose was to save some typing, setting an actual list variable from the dereferece reference variable doesn't save anything. And, using a reference instead of an actual list in the rest of your program is just going to add a layer of complexity and obfication.

If you are really just setting @interfaces not to include the first element of the returned list, you can do something like this:

(my $garbage, @interfaces) = qw(ifconfig -s);

The first value of the list will be returned into $garbage, a throw away variable. The rest of the list will be slurped up by @interfaces. This is clean, and fairly easy to understand what is going on.

Eric Strom I now see has done it even better:

(undef, @interfaces) = qw(ifconfig -s);

You don't even have a variable to throw out.

Now I'm going to stay up all night worrying what changes Perl 5.14 has made in the parsing of the shift command.

Upvotes: 1

TLP
TLP

Reputation: 67918

Perl might not be the easiest way (or most readable way, I should say) to do it, if you want to restrict yourself to using just one line. Here's a solution that fixes your shell command instead:

@interfaces = qx(ifconfig -s | tail -n +2);

Upvotes: 3

MisterEd
MisterEd

Reputation: 1735

The closest I could get to getting this into a one liner:

perl -e '@interfaces = (qx|ifconfig -s|)[1 .. 1000]; print @interfaces'

This is taking a slice from index 1 to index 1000 and assumes your ifconfig output is no more than 1000 lines. Obviously this is horrible programming practice, but is does work for most cases and does what the question asks.

Upvotes: 3

Related Questions