1

I am working on a program which uses different subroutines in separate files.

There are three parts

  • A text file with the name of the subroutine

  • A Perl program with the subroutine

  • The main program which extracts the name of the subroutine and launches it

The subroutine takes its data from a text file.

I need the user to choose the text file, the program then extracts the name of the subroutine.

The text file contains

cycle.name=cycle01

Here is the main program :

# !/usr/bin/perl -w

use strict;
use warnings;

use cycle01;

my $nb_cycle = 10;

# The user chooses a text file

print STDERR "\nfilename: ";

chomp($filename = <STDIN>);

# Extract the name of the cycle 

open (my $fh, "<", "$filename.txt") or die "cannot open $filename";

while ( <$fh> ) {

    if ( /cycle\.name/ ) {

        (undef, $cycleToUse) = split /\s*=\s*/;
    }
}

# I then try to launch the subroutine by passing variables.
# This fails because the subroutine is defined as a variable.

$cycleToUse($filename, $nb_cycle);

And here is the subroutine in another file

# !/usr/bin/perl

package cycle01;

use strict;
use warnings;

sub cycle01 {

    # Get the total number of arguments passed
    my ($filename, $nb_cycle) = @_;
    print "$filename, $nb_cycle";
1
  • 2
    Shouldn't the $cycleToUse be assigned to the second item returned from the split call? Like: (undef, $cycleToUse) = split /\s*=\s*/. Note: then you should also chomp the line before you call split to avoid a newline character from creeping into the value... To call a subroutine defined in a variable I would use a dispatch table, e.g. my %dispatch = (cycle01 => \&cycle01::cycle01 ) and then call the sub like this: $dispatch{$cycleToUse}->($filename, $nb_cycle) Commented Jul 12, 2018 at 12:42

2 Answers 2

4

Your code doesn't compile, because in the final call, you have mistyped the name of $nb_cycle. It's helpful if you post code that actually runs :-)

Traditionally, Perl module names start with a capital letter, so you might want to rename your package to Cycle01.

The quick and dirty way to do this is to use the string version of eval. But evaluating an arbitrary string containing code is dangerous, so I'm not going to show you that. The best way is to use a dispatch table - basically a hash where the keys are valid subroutine names and the values are references to the subroutines themselves. The best place to add this is in the Cycle01.pm file:

our %subs = (
  cycle01 => \&cycle01,
);

Then, the end of your program becomes:

if (exists $Cycle01::subs{$cycleToUse}) {
  $Cycle01::subs{$cycleToUse}->($filename, $nb_cycle);
} else {
  die "$cycleToUse is not a valid subroutine name";
}

(Note that you'll also need to chomp() the lines as you read them in your while loop.)

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you very much, and do you know what to do not to use the name of the subroutine cycle01 at the end of the main program ? Because some subroutines cycle02, 03 ... will be added later and the main program must not be changed.
That's not the name of the subroutine. That's the name of the module. The one you load at the top of the program - use Cycle01.
There is also no declaration for $filename.
1

To build on Dave Cross' answer, I usually avoid the hash table, partly because, in perl, everything is a hash table anyway. Instead, I have all my entry-point subs start with a particular prefix, that prefix depends on what I'm doing, but here we'll just use ep_ for entry-point. And then I do something like this:

my $subname = 'ep_' . $cycleToUse;
if (my $func = Cycle01->can($subname))
{
  $func->($filename, $nb_cycle);
}
else
{
  die "$cycleToUse is not a valid subroutine name";
}

The can method in UNIVERSAL extracts the CODE reference for me from perl's hash tables, instead of me maintaining my own (and forgetting to update it). The prefix allows me to have other functions and methods in that same namespace that cannot be called by the user code directly, allowing me to still refactor code into common functions, etc.

If you want to have other namespaces as well, I would suggest having them all be in a single parent namespace, and potentially all prefixed the same way, and, ideally, don't allow :: or ' (single quote) in those names, so that you minimise the scope of what the user might call to only that which you're willing to test.

e.g.,

die "Invalid namespace $cycleNameSpaceToUse"
  if $cycleNameSpaceToUse =~ /::|'/;
my $ns = 'UserCallable::' . $cycleNameSpaceToUse;
my $subname = 'ep_' . $cycleToUse;
if (my $func = $ns->can($subname))
# ... as before

There are definitely advantages to doing it the other way, such as being explicit about what you want to expose. The advantage here is in not having to maintain a separate list. I'm always horrible at doing that.

3 Comments

Your odd habit of adding a prefix to your subroutine names is irrelevant to the question and rather confuses things.
@Borodin The point here is using can to get the code reference, the odd prefix habit is just how I ensure that untrusted data doesn't invoke arbitrary code (as the accepted answer also ensures).
This method is tainted. How do you know that a module will not use the prefix for its imported subroutines? Better to use a dispatch table and minimize possible bugs.

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.