http://www.perlmonks.org?node_id=1024253

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

I'm currently writing a collection of scripts for my company - currently a few projects with inter-dependencies. Since everything is in development - and I can't expect to have particularly priviliged access to the resulting server - dependant projects are stored in subdirectories of the owning projects rather than in the proper Perl Module paths (Nothing Too Special here, I hope).

For example, three separate projects "Checkout", "Utilities" and "Transfer" can have the following heirarchy:

Checkout
Checkout/Utilities
Checkout/Transfer
Checkout/Transfer/Utilities (Obviously not necessary in this particular setup, but shown for demonstration)

 

So, I have two queries I humbly submit to you, enlightened Monks of the Monastery.

Firstly: use lib 'Utilities' and use lib 'Transfer' (in Checkout scripts) both work fine if I'm running the script from the directories they are stored in, but not elsewhere because for some raisin - assumedly to do with @INC -, Perl doesn't consider a script's home to be the obvious search directory. I'm using FindBin in the root project and while it works, it feels a bit cludgy. Is this more-or-less correct or am I missing some sage wisdom here?

Secondly: More importantly, though, is that some of these packages need to call some non-Perl programs. Even though I keep them in their relative project directories, FindBin is powerless to assist in this general sense. For example, the following will fail because './rdiff' is not in the current working directory:

---./checkout.pl use lib 'Transfer'; use DiffScript; DiffScript::generateDiff(); ---Transfer/DiffScript.pm package DiffScript; sub generateDiff { eval('./rdiff args1 args2 etcargs'); }

What would I do in this instance? The application (i.e. 'rdiff') is tied to the Transfer project, and I don't want to introduce extra coupling by forcing every higher-up project to supply a path to every module just for cases like this. The idea is to keep the projects as decoupled as possible, with the top-most project in an arbitrary root directory.

 

I've searched for a while and have turned up perilously little in this regard! I really hope any of you can shed some light on this :)

  • Comment on Executing a program from within a Perl Module in a non-standard path
  • Download Code

Replies are listed 'Best First'.
Re: Executing a program from within a Perl Module in a non-standard path
by tobyink (Canon) on Mar 19, 2013 at 13:41 UTC

    This is quite cute - a subclass of System::Sub that croaks at compile time if the command cannot be found:

    use v5.10; use strict; use warnings; package System::Sub::Env; use Carp qw(croak); use File::Which qw(which); use base qw(System::Sub); sub import { my $class = shift; my @super; while (@_) { my $cmd = shift; my %opt = @{ ref($_[0]) ? shift : [] }; my $env = uc("PATH_TO_$cmd"); $opt{'$0'} //= ($ENV{$env} // which($cmd)); -x $opt{'$0'} or croak("Could not find '$cmd'; please set '$env' environ +ment variable; stopped"); push @super, $cmd => [%opt]; } @_ = ($class, @super); goto \&System::Sub::import; } 1;

    You can use it like this:

    use System::Sub::Env "mv"; mv("oldname.txt" => "newname.txt");

    At compile time, it will check to see if there's a $ENV{PATH_TO_MV environment variable set, and use mv from there (it should be the full path to the binary). If that's not set, it will fall back to a normal $ENV{PATH} search. And otherwise it will croak, asking them to set the environment variable.

    package Cow { use Moo; has name => (is => 'lazy', default => sub { 'Mooington' }) } say Cow->new->name

      Ahh, this is pretty cool. I like the idea that System::Sub represents, but alas, it requires that I know where the executable (or script!) will be at compile time (at least, relative to the cwd).

      As an aside, I can't understand how import doesn't get itself in an infinite loop! Where's the exit?!

       

      But thanks; I'll definitely keep this class in mind for later projects

        "As an aside, I can't understand how import doesn't get itself in an infinite loop!"

        System::Sub::Env::import is going to System::Sub::import. Different package; different sub.

        The reason for the goto is to hide System::Sub::Env::import from the call stack - System::Sub's import method looks at caller.

        package Cow { use Moo; has name => (is => 'lazy', default => sub { 'Mooington' }) } say Cow->new->name
Re: Executing a program from within a Perl Module in a non-standard path ($PATH/%PATH%)
by Anonymous Monk on Mar 19, 2013 at 11:49 UTC

      Hmmm. It feels a little heavy-handed to modify the PATH for each potential script/executable after a recursive search, considering I technically know where they are stored (in the directory of the calling project/script, given a heirarchy of projects).

      On top of that, since I don't know it explicitly (that's pretty much the entire problem here!), $startdir will always be the cwd, too (or at least $FindBin::Bin), which would be redundant if PathStuffer is used multiple times (the search is probably a negligable impact in this day and age, but still. ;) ).

      Also, it's not always the case that a file will be a script (note that 'rdiff' is an executable binary), so just searching for ".pl" doesn't quite cover all use cases. Since *nix binaries don't really have naming conventions (and hence I'd have to search for ".*"), I'd necessarily be including every directory into the path to include it, which just makes me feel dirty. (It probably wasn't obvious that I'm using *nix boxes here, but I'd like to keep the code portable either way)

       

      That said, this is certainly still a viable option that I wouldn't have considered otherwise :)

Re: Executing a program from within a Perl Module in a non-standard path
by GoldElite (Beadle) on Mar 20, 2013 at 12:14 UTC

    Alright, I found a solution that isn't perfect, but suits my needs thus far. Since the Utilities project includes a logger package that is used by practically every other script (and which already has tenuously appropriate functions ;) ), I managed to put a "get_script_dir" function into it that other scripts can use to figure out where they are (it is exported along with multiple other things for convenience):

    sub get_script_dir { #Get the calling script's path my $sc_path = (caller)[1]; #Parse the (possibly relative) directory out of the filename $sc_path =~ m/^(.*[\/\\]+)[^\/\\]*$/; $sc_path = $1 || "."; #Append a trailing slash if there isn't one, for consistency $sc_path .= "/" unless m/[\/\\]$/; return $sc_path; }

     

    Then, in every script that isn't a Package/Module, I start with the following:

    #!/usr/bin/perl use strict; use warnings; use FindBin; use lib "$FindBin::Bin"; use lib "$FindBin::Bin/Utilities"; ...etc...

     

    This way, scripts will call packages from their working directories regardless of the cwd (any script executed via 'system', etc - rather than just used - will inherit its own, relative, FindBin value). Any Package/Module/Script that feels the need to execute something in its own path can then call something like system(get_script_dir() . "some_executable"); which should™ always work.

    It's not amazing, and probably slightly hacky, but it's the most efficient method to overcome Perl's trouble with script-relative paths that I could produce so far. I'm still open to any better solutions, but at this point I'm not expecting any :)

     

    As an adendum, something that I completely failed to mention in the orignal post, is that most of the servers should be expected to be only running core versions of Perl 5.8.8 on RedHat. Not that's reeaally changed anything, but still, I'm noting this now, for reference.