Search code examples
perlglobtypeglob

perl: iterate over a typeglob


Given a typeglob, how can I find which types are actually defined?

In my application, we user PERL as a simple configuration format. I'd like to require() the user config file, then be able to see which variables are defined, as well as what types they are.

Code: (questionable quality advisory)

#!/usr/bin/env perl

use strict;
use warnings;

my %before = %main::;
require "/path/to/my.config";
my %after = %main::;

foreach my $key (sort keys %after) {
    next if exists $before{$symbol}; 

    local *myglob = $after{$symbol};
    #the SCALAR glob is always defined, so we check the value instead
    if ( defined ${ *myglob{SCALAR} } ) {
        my $val = ${ *myglob{SCALAR} };
        print "\$$symbol = '".$val."'\n" ;
    }
    if ( defined *myglob{ARRAY} ) {
        my @val = @{ *myglob{ARRAY} };
        print "\@$symbol = ( '". join("', '", @val) . "' )\n" ;
    }
    if ( defined *myglob{HASH} ) {
        my %val = %{ *myglob{HASH} };
        print "\%$symbol = ( ";
        while(  my ($key, $val) = each %val )  {
            print "$key=>'$val', ";
        }
        print ")\n" ;
    }
}

my.config:

@A = ( a, b, c );
%B = ( b=>'bee' );
$C = 'see';

output:

@A = ( 'a', 'b', 'c' )
%B = ( b=>'bee', )
$C = 'see'
$_<my.config = 'my.config'

Solution

  • In the fully general case, you can't do what you want thanks to the following excerpt from perlref:

    *foo{THING} returns undef if that particular THING hasn't been used yet, except in the case of scalars. *foo{SCALAR} returns a reference to an anonymous scalar if $foo hasn't been used yet. This might change in a future release.

    But if you're willing to accept the restriction that any scalar must have a defined value to be detected, then you might use code such as

    #! /usr/bin/perl
    
    use strict;
    use warnings;
    
    open my $fh, "<", \$_;  # get DynaLoader out of the way
    
    my %before = %main::;
    require "my.config";
    my %after = %main::;
    
    foreach my $name (sort keys %after) {
      unless (exists $before{$name}) {
        no strict 'refs';
        my $glob = $after{$name};
        print "\$$name\n"             if defined ${ *{$glob}{SCALAR} };
        print "\@$name\n"             if defined    *{$glob}{ARRAY};
        print "%$name\n"              if defined    *{$glob}{HASH};
        print "&$name\n"              if defined    *{$glob}{CODE};
        print "$name (format)\n"      if defined    *{$glob}{FORMAT};
        print "$name (filehandle)\n"  if defined    *{$glob}{IO};
      }
    }
    

    will get you there.

    With my.config of

    $JACKPOT = 3_756_788;
    $YOU_CANT_SEE_ME = undef;
    
    @OPTIONS = qw/ apple cherries bar orange lemon /;
    
    %CREDITS = (1 => 1, 5 => 6, 10 => 15);
    
    sub is_jackpot {
      local $" = ""; # " fix Stack Overflow highlighting
      "@_[0,1,2]" eq "barbarbar";
    }
    
    open FH, "<", \$JACKPOT;
    
    format WinMessage =
    You win!
    .
    

    the output is

    %CREDITS
    FH (filehandle)
    $JACKPOT
    @OPTIONS
    WinMessage (format)
    &is_jackpot

    Printing the names takes a little work, but we can use the Data::Dumper module to take part of the burden. The front matter is similar:

    #! /usr/bin/perl
    
    use warnings;
    use strict;
    
    use Data::Dumper;
    sub _dump {
      my($ref) = @_;
      local $Data::Dumper::Indent = 0;
      local $Data::Dumper::Terse  = 1;
      scalar Dumper $ref;
    }
    
    open my $fh, "<", \$_;  # get DynaLoader out of the way
    
    my %before = %main::;
    require "my.config";
    my %after = %main::;
    

    We need to dump the various slots slightly differently and in each case remove the trappings of references:

    my %dump = (
      SCALAR => sub {
        my($ref,$name) = @_;
        return unless defined $$ref;
        "\$$name = " . substr _dump($ref), 1;
      },
    
      ARRAY => sub {
        my($ref,$name) = @_;
        return unless defined $ref;
        for ("\@$name = " . _dump $ref) {
          s/= \[/= (/;
          s/\]$/)/;
          return $_;
        }
      },
    
      HASH => sub {
        my($ref,$name) = @_;
        return unless defined $ref;
        for ("%$name = " . _dump $ref) {
          s/= \{/= (/;
          s/\}$/)/;
          return $_;
        }
      },
    );
    

    Finally, we loop over the set-difference between %before and %after:

    foreach my $name (sort keys %after) {
      unless (exists $before{$name}) {
        no strict 'refs';
        my $glob = $after{$name};
        foreach my $slot (keys %dump) {
          my $var = $dump{$slot}(*{$glob}{$slot},$name);
          print $var, "\n" if defined $var;
        }
      }
    }
    

    Using the my.config from your question, the output is

    $ ./prog.pl 
    @A = ('a','b','c')
    %B = ('b' => 'bee')
    $C = 'see'