5

I'm writing a tool that must import a number of other perl config files. The files are not wrapped w/packages and may have similar or conflicting variables/functions. I don't have the ability to change the format of these files, so I must work around what they are. What I was thinking to do was import each into a unique name space, but I've not found a way to do that using do, require, or use. If I don't use dynamic names, just a hardcoded name, I can do it.

Want something like this:

sub sourceTheFile {
  my ($namespace, $file) = @_;
  package $namespace;
  do $file;
  1;
  return;
}

That doesn't work because the package command requires a constant for the name. So then I try something like this:

sub sourceTheFile {
  my ($namespace, $file) = @_;
  eval "package $namespace;do $file;1;"
  return;
}

But the contents of the file read by do are placed in the main:: scope not the one I want. The target scope is created, just not populated by the do. (I tried require, and just a straight cat $file inside the eval as well.)

I'm using Devel::Symdump to verify that the namespaces are built correctly or not.

example input file:

my $xyz = "some var";
%all_have_this = ( common=>"stuff" );

ADDITIONAL CHALLENGE

Using the answer that does the temp file build and do call, I can make this work dynamically as I require. BUT, big but, how do I now reference the data inside this new namespace? Perl doesn't seem to have the lose ability to build a variable name from a string and use that as the variable.

7
  • 2
    Please post a minimal reproducible example. Commented Aug 25, 2017 at 7:21
  • It is a little more up front work, but using a hook in @INC would be a more robust way to do this. Commented Aug 25, 2017 at 14:05
  • You realize if you have my $xyz = '...' and you put that in a package, you cannot access it from outside the package, right? Commented Aug 25, 2017 at 15:18
  • 1
    @SinanÜnür - yes, I know the my vars will not be accessible. The unqualified vars (no my/our/local) are the ones I really want. Commented Aug 25, 2017 at 16:04
  • Would this not be better if the configuration were data rather than code? Commented Aug 25, 2017 at 20:48

2 Answers 2

3

I am not sure why the eval did not work. Maybe a bug? Here is a workaround using a temp file. This works for me:

use strict;
use warnings;

use Devel::Symdump;
use File::Temp;

my $file = './test.pl';
my $namespace = 'TEST';
{
    my $fh = File::Temp->new();
    print $fh "package $namespace;\n";
    print $fh "do '$file';\n";
    print $fh "1;\n";
    close $fh;
    do $fh->filename;
}
Sign up to request clarification or add additional context in comments.

1 Comment

So far this seems the most dynamic option (can be done on the fly as more data is read in).
3

Perl's use and require facilities make use of any hooks you might have installed in @INC. You can simply install a hook which looks in a specific location to load modules with a prefix you choose:

package MyIncHook;

use strict;
use warnings;

use autouse Carp => qw( croak );

use File::Spec::Functions qw( catfile );

sub import {
    my ($class, $prefix, $location) = @_;
    unshift @INC, _loader_for($prefix, $location);
    return;
}

sub _loader_for {
    my $prefix = shift;
    my $location = shift;

    $prefix =~ s{::}{/}g;

    return sub {
        my $self = shift;
        my $wanted = shift;

        return unless $wanted =~ /^\Q$prefix/;

        my $path = catfile($location, $wanted);
        my ($is_done);

        open my $fh, '<', $path
            or croak "Failed to open '$path' for reading: $!";

        my $loader = sub {
            if ($is_done) {
                close $fh
                    or croak "Failed to close '$path': $!";
                return 0;
            }
            if (defined (my $line = <$fh>)) {
                $_ = $line;
                return 1;
            }
            else {
                $_ = "1\n";
                $is_done = 1;
                return 1;
            }
        };

        (my $package = $wanted) =~ s{/}{::}g;
        $package =~ s/[.]pm\z//;

        my @ret = (\"package $package;", $loader);
        return @ret;
    }
}

__PACKAGE__;
__END__

Obviously, modify the construction of $path according to your requirements.

You can use it like this:

#!/usr/bin/env perl

use strict;
use warnings;

use MyIncHook ('My::Namespace', "$ENV{TEMP}/1");

use My::Namespace::Rand;

print $My::Namespace::Rand::settings{WARNING_LEVEL}, "\n";

where $ENV{TEMP}/1/My/Namespace/Rand.pm contains:

%settings = (
    WARNING_LEVEL => 'critical',
);

Output:

C:\Temp> perl t.pl
critical

You can, obviously, define your own mapping from made up module names to file names.

3 Comments

Interesting answer. Not sure I understood the sentence "... because it is the do that is looking for the true value at the end of $file". Why is do looking for a true value, isn't it require that should be looking for a true value? According to the documentation: do EXPR only executes the content of the filename EXPR as a Perl script.
@HåkonHægland Thank you for the observation. I removed that part from my answer. It is possible that there is another problem with the code the OP is not showing us.
How would you make this more dynamic? I start with a list of files to read (command line). As I parse that list, those files may reference others (as file paths in strings) that I have to dynamically read in as well.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.