File Coverage

blib/lib/App/Env.pm
Criterion Covered Total %
statement 251 262 95.8
branch 111 124 89.5
condition 30 36 83.3
subroutine 37 41 90.2
pod 16 17 94.1
total 445 480 92.7


line stmt bran cond sub pod time code
1             package App::Env;
2              
3             # ABSTRACT: manage application specific environments
4              
5 27     27   6896262 use v5.10;
  27         109  
6 27     27   184 use strict;
  27         60  
  27         888  
7 27     27   139 use warnings;
  27         69  
  27         2167  
8              
9             our $VERSION = '1.05';
10              
11 27     27   196 use Scalar::Util;
  27         60  
  27         1531  
12 27     27   13638 use Storable ();
  27         99878  
  27         997  
13 27     27   180 use List::Util 1.33;
  27         660  
  27         1758  
14 27     27   16325 use IO::File ();
  27         239665  
  27         905  
15              
16 27     27   16595 use Params::Validate ();
  27         312826  
  27         1155  
17              
18 27     27   16099 use App::Env::_Util;
  27         104  
  27         1198  
19 27     27   22330 use App::Env::_app;
  27         149  
  27         1780  
20              
21             use overload
22             '%{}' => '_envhash',
23             q{""} => 'str',
24 0     0   0 bool => sub { 1 },
25 27     27   233 fallback => 1;
  27         47  
  27         344  
26              
27             #-------------------------------------------------------
28              
29             # Options
30             my %SharedOptions = (
31             Force => { default => 0 },
32             Cache => { default => 1 },
33             Site => { optional => 1 },
34             CacheID => { default => undef },
35             Temp => { default => 0 },
36             SysFatal => { default => 0, type => Params::Validate::BOOLEAN },
37             );
38              
39             my %ApplicationOptions = (
40             AppOpts => { default => {}, type => Params::Validate::HASHREF },
41             %SharedOptions,
42             );
43              
44             my %CloneOptions
45             = %{ Storable::dclone( { map { $_ => $SharedOptions{$_} } qw[ CacheID Cache SysFatal ] } ) };
46             $CloneOptions{Cache}{default} = 0;
47              
48             my %TempOptions = %{ Storable::dclone( { map { $_ => $SharedOptions{$_} } qw[ SysFatal Temp ] } ) };
49              
50             # options for whom defaults may be changed. The values
51             # in %OptionDefaults are references to the same hashes as in
52             # ApplicationOptions & SharedOptions, so modifying them will
53             # modify the others.
54             my @OptionDefaults = qw( Force Cache Site SysFatal );
55             my %OptionDefaults;
56             @OptionDefaults{@OptionDefaults} = @ApplicationOptions{@OptionDefaults};
57              
58             #-------------------------------------------------------
59             #-------------------------------------------------------
60              
61              
62             # import one or more environments. this may be called in the following
63             # contexts:
64             #
65             # * as a class method, i.e.
66             # use App:Env qw( application )
67             # App:Env->import( $application )
68             #
69             # * as a class function (just so as not to confuse folks
70             # App::Env::import( $application )
71             #
72             # * as an object method
73             # $env->import
74              
75             sub import {
76              
77 43     43   628610 my $this = $_[0];
78              
79             # object method?
80 43 100 66     361 if ( Scalar::Util::blessed $this && $this->isa( __PACKAGE__ ) ) {
81 11         44 my $self = shift;
82 11 50       47 App::Env::_Util::croak( __PACKAGE__, "->import: too many arguments\n" )
83             if @_;
84              
85 11         65 $ENV{$_} = $self->{$_} for keys %$self; ## no critic (Variables::RequireLocalizedPunctuationVars)
86             }
87              
88             else {
89              
90             # if class method, get rid of class in argument list
91 32 100 66     257 shift if !ref $this && $this eq __PACKAGE__;
92              
93             # if no arguments, nothing to do. "use App::Env;" will cause this.
94 32 100       454026 return unless @_;
95              
96             # if the only argument is a hash, it sets defaults
97 14 100 100     84 if ( @_ == 1 && 'HASH' eq ref $_[0] ) {
98 2         12 config( @_ );
99 2         2531 return;
100             }
101              
102 12         75 App::Env->new( @_ )->import;
103             }
104             }
105              
106              
107             # class method
108             # retrieve a cached environment.
109             sub retrieve {
110              
111 4     4 1 275 my ( $cacheid ) = @_;
112 4         6 my $self;
113              
114 4 100       16 if ( defined( my $app = App::Env::_Util::getCacheEntry( $cacheid ) ) ) {
115 2         10 $self = __PACKAGE__->new();
116 2         6 $self->_var( app => $app );
117             }
118              
119 4         13 return $self;
120             }
121              
122             #-------------------------------------------------------
123              
124             sub config {
125 2     2 1 116 my %default = Params::Validate::validate( @_, \%OptionDefaults );
126 2         24 $OptionDefaults{$_}{default} = $default{$_} for keys %default;
127 2         12 return;
128             }
129              
130             #-------------------------------------------------------
131              
132             sub new {
133 83     83 1 5343319 my $class = shift;
134              
135 83 100       410 my $opts = 'HASH' eq ref $_[-1] ? pop : {};
136              
137             # %{} is overloaded, so an extra reference is required to avoid
138             # an infinite loop when doing things like $self->{}. instead,
139             # use $$self->{}
140 83         1360 my $self = bless \{}, $class;
141              
142 83 100       630 $self->_load_envs( @_, $opts ) if @_;
143              
144 82         374 return $self;
145             }
146              
147             #-------------------------------------------------------
148              
149             sub clone {
150 2     2 1 587 my $self = shift;
151              
152 2         75 my %nopt = Params::Validate::validate( @_, \%CloneOptions );
153              
154 2         346 my $clone = Storable::dclone( $self );
155 2         8 delete ${$clone}->{id};
  2         9  
156              
157             # create new cache id
158             $clone->_app->mk_cacheid(
159             CacheID => defined $nopt{CacheID}
160             ? $nopt{CacheID}
161 2 50       7 : $self->lobject_id,
162             );
163              
164 2         5 my %opt = ( %{ $clone->_opt }, %nopt );
  2         6  
165 2         10 $clone->_opt( \%opt );
166              
167 2         10 $clone->cache( $opt{Cache} );
168              
169 2         10 return $clone;
170             }
171              
172             #-------------------------------------------------------
173              
174             sub _load_envs {
175 81     81   160 my $self = shift;
176 81         279 my @opts = ( pop );
177 81         260 my @apps = @_;
178              
179             # most of the following logic is for the case where multiple applications
180             # are being loaded in one call. Checking caching requires that we generate
181             # a cacheid from the applications' cacheids.
182              
183             # if import is called as import( [$app, \%opts], \%shared_opts ),
184             # this is equivalent to import( $app, { %shared_opts, %opts } ),
185             # but we still validate %shared_opts as SharedOptions, just to be
186             # precise.
187              
188             # if there's a single application passed as a scalar (rather than
189             # an array containing the app name and options), treat @opts as
190             # ApplicationOptions, else SharedOptions
191              
192 81 100 100     3150 my %opts = Params::Validate::validate( @opts, @apps == 1 && !ref( $apps[0] )
193             ? \%ApplicationOptions
194             : \%SharedOptions );
195              
196              
197 81 100       768 $opts{Cache} = 0 if $opts{Temp};
198              
199             # iterate through the applications to ensure that any application specific
200             # options are valid and to form a basis for a multi-application
201             # cacheid to check for cacheing.
202 81         194 my @cacheids;
203             my @Apps;
204 81         258 for my $app ( @apps ) {
205             # initialize the application specific opts from the shared opts
206 87         376 my %app_opt = %opts;
207              
208             # special filtering of options if this is part of a multi-app
209             # merge
210 87 100       335 if ( @apps > 1 ) {
211             # don't use the shared CacheID option
212 12         23 delete $app_opt{CacheID};
213              
214             # don't cache individual apps in a merged environment,
215             # as the cached environments will be polluted.
216 12         19 delete $app_opt{Cache};
217              
218             # ignore a Force option. This will be turned on later;
219             # if set now it will prevent proper error checking
220 12         25 delete $app_opt{Force};
221             }
222              
223             # handle application specific options.
224 87 100       322 if ( 'ARRAY' eq ref( $app ) ) {
225 2         6 ( $app, my $opts ) = @$app;
226 2 50       7 App::Env::_Util::croak( "$app: application options must be a hashref\n" )
227             unless 'HASH' eq ref $opts;
228              
229 2         11 %app_opt = ( %app_opt, %$opts );
230              
231 2 100       9 if ( @apps > 1 ) {
232 1         2 for my $iopt ( qw( Cache Force ) ) {
233 2 50       6 if ( exists $app_opt{$iopt} ) {
234 0         0 App::Env::_Util::croak(
235             "$app: do not specify the $iopt option for individual applications in a merge\n" );
236 0         0 delete $app_opt{$iopt};
237             }
238             }
239             }
240             }
241              
242             # set forced options for apps in multi-app merges, otherwise
243             # the defaults will be set by the call to validate below.
244 87 100       297 if ( @apps > 1 ) {
245 12         25 $app_opt{Force} = 1;
246 12         22 $app_opt{Cache} = 0;
247             }
248              
249             # validate possible application options and get default
250             # values. Params::Validate wants a real array
251 87         311 my ( @app_opts ) = %app_opt;
252              
253             # return an environment object, but don't load it. we need the
254             # module name to create a cacheid for the merged environment.
255             # don't load now to prevent unnecessary loading of uncached
256             # environments if later it turns out this is a cached
257             # multi-application environment
258 87         2867 %app_opt = ( Params::Validate::validate( @app_opts, \%ApplicationOptions ) );
259 87         910 my $appo = App::Env::_app->new(
260             pid => $self->lobject_id,
261             app => $app,
262             NoLoad => 1,
263             opt => \%app_opt,
264             );
265 86         293 push @cacheids, $appo->cacheid;
266 86         444 push @Apps, $appo;
267             }
268              
269              
270             # create a cacheid for the multi-app environment
271 80   33     554 my $cacheid = $opts{CacheId} // join( $;, @cacheids );
272 80         184 my $App;
273              
274             # use cache if possible
275 80 100 100     496 if ( !$opts{Force} && defined( my $app = App::Env::_Util::getCacheEntry( $cacheid ) ) ) {
    100          
276             # if this is a temporary object and a cached version exists,
277             # clone it and assign a new cache id.
278 18 100       53 if ( $opts{Temp} ) {
279 2         267 $App = Storable::dclone( $app );
280              
281             # should really call $self->cacheid here, but $self
282             # doesn't have an app attached to it yet so that'll fail.
283 2         13 $App->cacheid( $self->lobject_id );
284              
285             # update Temp compatible options
286 2         4 $App->opt( { %{ $App->opt }, map { $_ => $opts{$_} } keys %TempOptions } );
  2         7  
  4         18  
287             }
288              
289             else {
290 16         30 $App = $app;
291             }
292             }
293              
294             # not cached; is this really just a single application?
295             elsif ( @Apps == 1 ) {
296 57         117 $App = shift @Apps;
297 57         296 $App->load;
298             }
299              
300             # ok, create new environment by iteration through the apps
301             else {
302             # we don't want to merge environments, as apps may
303             # modify a variable that we don't know how to merge.
304             # PATH is easy, but we have no idea what might be in
305             # others. so, let the apps handle it.
306              
307             # apps get loaded in the current environment.
308 5         336 local %ENV = %ENV;
309              
310 5         22 my @modules;
311 5         14 foreach my $app ( @Apps ) {
312 10         43 push @modules, $app->module;
313              
314             # embrace new merged environment
315 10         16 %ENV = %{ $app->load }; ## no critic (Variables::RequireLocalizedPunctuationVars)
  10         31  
316             }
317              
318 5         105 $App = App::Env::_app->new(
319             ref => $self,
320             env => {%ENV},
321             module => join( $;, @modules ),
322             cacheid => $cacheid,
323             opt => \%opts,
324             );
325              
326 5 50       51 if ( $opts{Cache} ) { $App->cache; }
  5         17  
327             }
328              
329             # record the final things we need to know.
330 80         266 $self->_var( app => $App );
331             }
332              
333              
334             #-------------------------------------------------------
335              
336             # simple accessors to reduce confusion because of double reference in $self
337              
338             sub _var {
339 822     822   1513 my $self = shift;
340 822         1446 my $var = shift;
341              
342 822 100       2007 ${$self}->{$var} = shift if @_;
  164         602  
343              
344 822         1405 return ${$self}->{$var};
  822         14504  
345             }
346              
347 0     0 1 0 sub module { $_[0]->_app->module }
348 6     6 1 19 sub cacheid { $_[0]->_app->cacheid }
349 0     0   0 sub _cacheid { my $self = shift; $self->app->cacheid( @_ ) }
  0         0  
350             sub _opt {
351 24     24   467 my $self = shift;
352 24         91 $self->_app->opt( @_ );
353             }
354 554     554   1379 sub _app { $_[0]->_var( 'app' ) }
355 519     519   1504 sub _envhash { $_[0]->_app->{ENV} }
356              
357             # would rather use Object::ID but it uses Hash::FieldHash which
358             # (through no fault of its own:
359             # http://rt.cpan.org/Ticket/Display.html?id=58030 ) stringify's the
360             # passed reference on pre 5.10 perls, which causes problems.
361              
362             # stolen as much as possible from Object::ID to keep the interface the same
363             {
364             my $Last_ID = 'a';
365              
366             sub lobject_id {
367 93     93 0 178 my $self = shift;
368              
369 93 100       533 return $self->_var( 'id' ) if defined $self->_var( 'id' );
370 82         310 return $self->_var( 'id', ++$Last_ID );
371             }
372             }
373              
374             #-------------------------------------------------------
375              
376             sub cache {
377 3     3 1 10 my ( $self, $cache ) = @_;
378              
379 3 50       9 defined $cache
380             or App::Env::_Util::croak( "missing or undefined cache argument\n" );
381              
382 3 100       10 if ( $cache ) {
383 1         4 $self->_app->cache;
384             }
385             else {
386 2         23 $self->_app->uncache;
387             }
388             }
389              
390             sub uncache {
391 23     23 1 17010 goto \&App::Env::_Util::uncache;
392             }
393              
394              
395             #-------------------------------------------------------
396              
397             sub env {
398 70     70 1 54218 my $self = shift;
399 70 100       280 my @opts = ( 'HASH' eq ref $_[-1] ? pop : {} );
400              
401             # mostly a duplicate of what's in str(). ick.
402 70         1510 my %opt = Params::Validate::validate(
403             @opts,
404             {
405             Exclude => {
406             callbacks => { 'type' => \&App::Env::_Util::exclude_param_check },
407             default => undef,
408             },
409             AllowIllegalVariableNames => {
410             optional => 1,
411             default => !!1,
412             },
413             } );
414              
415             # Exclude is only allowed in scalar calling context where
416             # @_ is empty, has more than one element, or the first element
417             # is not a scalar.
418             App::Env::_Util::croak( "Cannot use Exclude in this calling context\n" )
419 70 100 100     672 if $opt{Exclude} && ( wantarray() || ( @_ == 1 && !ref $_[0] ) ); ## no critic (Community::Wantarray)
      100        
420              
421 68 100       266 my $include = [ @_ ? @_ : qr/.*/ ];
422 68         194 my $env = $self->_envhash;
423              
424             my @exclude
425             = defined( $opt{Exclude} )
426 68 100       207 ? ( 'ARRAY' eq ref $opt{Exclude} ? @{ $opt{Exclude} } : ( $opt{Exclude} ) )
  1 100       4  
427             : ();
428              
429             # exclude any variables with non-word characters
430             push @exclude, qr/\W/
431 68 100       206 unless $opt{AllowIllegalVariableNames};
432              
433 68         208 my @vars = $self->_filter_env( $include, \@exclude );
434              
435 68 100 100     472 if ( wantarray() ) { ## no critic (Community::Wantarray)
    100          
436 1 100       4 return map { exists $env->{$_} ? $env->{$_} : undef } @vars;
  3         18  
437             }
438             elsif ( @_ == 1 && !ref $_[0] ) {
439 35 100 100     508 return @vars && exists $env->{ $vars[0] } ? $env->{ $vars[0] } : undef;
440             }
441             else {
442 32         72 my %env;
443 32 50       63 @env{@vars} = map { exists $env->{$_} ? $env->{$_} : undef } @vars;
  477         1465  
444 32         586 return \%env;
445             }
446             }
447              
448             #-------------------------------------------------------
449              
450             sub setenv {
451 12     12 1 411 my $self = shift;
452 12         29 my $var = shift;
453              
454 12 50       49 defined $var
455             or App::Env::_Util::croak( "missing variable name argument\n" );
456              
457 12 100       40 if ( @_ ) {
458 11         35 $self->_envhash->{$var} = $_[0];
459             }
460             else {
461 1         6 delete $self->_envhash->{$var};
462             }
463             }
464              
465             #-------------------------------------------------------
466              
467             # return an env compatible string
468             sub str {
469 7     7 1 1927409 my $self = shift;
470 7 100       39 my @opts = ( 'HASH' eq ref $_[-1] ? pop : {} );
471              
472             # validate type. Params::Validate doesn't do Regexp, so
473             # this is a bit messy.
474 7         303 my %opt = Params::Validate::validate(
475             @opts,
476             {
477             Exclude => {
478             callbacks => { 'type' => \&App::Env::_Util::exclude_param_check },
479             optional => 1,
480             },
481             AllowIllegalVariableNames => {
482             optional => 1,
483             default => !!0,
484             },
485             } );
486              
487 7 100       102 my $include = [ @_ ? @_ : qr/.*/ ];
488             my @exclude
489             = defined( $opt{Exclude} )
490 7 100       38 ? ( 'ARRAY' eq ref $opt{Exclude} ? @{ $opt{Exclude} } : ( $opt{Exclude} ) )
  1 100       12  
491             : ();
492              
493             push @exclude, 'TERMCAP'
494 7 50   7   88 if List::Util::none { $_ eq 'TERMCAP' } @$include;
  7         57  
495              
496             # exclude any variables with non-word characters
497             push @exclude, qr/\W/
498 7 100       44 unless $opt{AllowIllegalVariableNames};
499              
500 7         32 my $env = $self->_envhash;
501 7         28 my @vars = grep { exists $env->{$_} } $self->_filter_env( $include, \@exclude );
  132         186  
502 7         25 return join( q{ }, map { "$_=" . App::Env::_Util::shell_escape( $env->{$_} ) } @vars );
  132         232  
503             }
504              
505             #-------------------------------------------------------
506              
507             # return a list of included variables, in the requested
508             # order, based upon a list of include and exclude specs.
509             # variable names passed as plain strings are not checked
510             # for existance in the environment.
511             sub _filter_env {
512 75     75   186 my ( $self, $included, $excluded ) = @_;
513              
514 75         243 my @exclude = $self->_match_var( $excluded );
515              
516 75         195 my %exclude = map { $_ => 1 } @exclude;
  22         77  
517 75         231 return grep { !$exclude{$_} } $self->_match_var( $included );
  661         1356  
518             }
519              
520             #-------------------------------------------------------
521              
522             # return a list of variables which matched the specifications.
523             # this takes a list of scalars, coderefs, or regular expressions.
524             # variable names passed as plain strings are not checked
525             # for existance in the environment.
526             sub _match_var {
527 151     151   343 my ( $self, $match ) = @_;
528              
529 151         296 my $env = $self->_envhash;
530              
531 151 50       428 $match = [$match] unless 'ARRAY' eq ref $match;
532              
533 151         241 my @keys;
534 151         323 for my $spec ( @$match ) {
535 108 50       254 next unless defined $spec;
536              
537             ## no critic( ControlStructures::ProhibitCascadingIfElse)
538 108 100       315 if ( !ref $spec ) {
    100          
    100          
    50          
539             # always return a plain name. this allows
540             # @values = $env->env( @names) to work.
541 55         150 push @keys, $spec;
542             }
543             elsif ( 'Regexp' eq ref $spec ) {
544 50         432 push @keys, grep { /$spec/ } keys %$env;
  1042         3517  
545             }
546             elsif ( 'CODE' eq ref $spec ) {
547 2         36 push @keys, grep { $spec->( $_, $env->{$_} ) } keys %$env;
  46         233  
548             }
549             elsif ( 'ARRAY' eq ref $spec ) {
550 1         5 push @keys, $self->_match_var( $spec );
551             }
552             else {
553 0         0 App::Env::_Util::croak( 'match specification is of unsupported type: ', ref $spec, "\n" );
554             }
555             }
556              
557 151         476 return @keys;
558             }
559              
560             #-------------------------------------------------------
561              
562              
563             #-------------------------------------------------------
564              
565              
566             sub system { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
567 7     7 1 11605 my $self = shift;
568              
569 7         16 local %ENV = %{$self};
  7         55  
570 7 100       45 if ( $self->_opt->{SysFatal} ) {
571 4         2276 require IPC::System::Simple;
572 4         55914 return IPC::System::Simple::system( @_ );
573             }
574             else {
575 3         1325768 return CORE::system( @_ );
576             }
577             }
578              
579             #-------------------------------------------------------
580              
581             sub qexec {
582 8     8 1 3496 my $self = shift;
583 8         32 local %ENV = %{$self};
  8         79  
584              
585 8         112 require IPC::System::Simple;
586              
587 8         34 my ( @res, $res );
588              
589 8         31 my $wantarray = wantarray; ## no critic (Community::Wantarray)
590              
591             eval {
592 8 100       69 if ( $wantarray ) {
593 1         13 @res = IPC::System::Simple::capture( @_ );
594             }
595             else {
596 7         61 $res = IPC::System::Simple::capture( @_ );
597             }
598 4         2660560 1;
599 8   66     52 } // do {
600 4 100       1221451 App::Env::_Util::croak( $@ ) if $self->_opt->{SysFatal};
601             # should return false, but need to keep API backwards compat
602 1         175 return undef;
603             };
604              
605 4 100       822 return $wantarray ? @res : $res;
606             }
607              
608             #-------------------------------------------------------
609              
610             sub capture {
611 7     7 1 1144080 my $self = shift;
612 7         45 my @args = @_;
613              
614 7         18 my $redirect;
615 7 100       43 $redirect = pop @args if 'HASH' eq ref $args[-1];
616              
617 7         16 local %ENV = %{$self};
  7         53  
618              
619 7         28 my $wantarray = wantarray; ## no critic (Community::Wantarray)
620              
621 7         2831 require Capture::Tiny;
622 7         60359 require IPC::System::Simple;
623              
624 7         17991 my $exit;
625              
626             my $sub
627             = $self->_opt->{SysFatal}
628 3     3   5562 ? sub { $exit = IPC::System::Simple::system( @args ) }
629 7 100   4   46 : sub { $exit = CORE::system( @args ) };
  4         2283384  
630              
631             # Capture::Tiny::capture is prototyped as (&;@). App::Env
632             # lazy-loads Capture::Tiny and thus nominally avoids the prototype
633             # check. However, if Capture::Tiny is explicitly loaded prior to
634             # App::Env, the prototype check will be performed when App::Env is
635             # compiled. In that case the following calls to capture are
636             # singled out, as while the calls are correct, the prototype
637             # requires an explicit block or sub{}. So, explicitly
638             # ignore prototypes.
639              
640              
641 7         19 my @return;
642 7   66     13 eval {
643             ## no critic (AmpersandSigils,AmpersandSubCalls )
644              
645 7 100       33 if ( $redirect ) {
    100          
646 2         6 my %redirect;
647 2         8 for my $stream ( 'stdout', 'stderr' ) {
648 4 50       13 next unless exists $redirect->{$stream};
649 4         5 my $handle = $redirect->{$stream};
650 4 100       35 $handle = IO::File->new( $handle, '>' )
651             if ref $handle eq q{};
652 4         262 $redirect{$stream} = $handle;
653             }
654 2         68 &Capture::Tiny::capture( $sub, %redirect );
655 2         966 $return[0] = $exit;
656             }
657             elsif ( $wantarray ) {
658 1         41 @return = &Capture::Tiny::capture( $sub );
659             }
660             else {
661 4         126 $return[0] = &Capture::Tiny::capture( $sub );
662             }
663 4         2743 1;
664             } // App::Env::_Util::croak( $@ );
665              
666             ## no critic (EmptyReturn)
667 4 100       178 return if !defined $wantarray;
668 3 100       517 return $wantarray ? @return : $return[0];
669             }
670              
671             #-------------------------------------------------------
672              
673             sub exec { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
674 0     0 1 0 my $self = shift;
675              
676 0         0 local %ENV = %{$self};
  0         0  
677 0         0 exec( @_ );
678             }
679              
680              
681             #-------------------------------------------------------
682              
683             sub which {
684 1     1 1 385 require File::Which;
685 1         992 my $self = shift;
686              
687             {
688 1         2 local %ENV = %{$self};
  1         2  
  1         3  
689 1         4 return File::Which::which( @_ );
690             }
691             }
692              
693             1;
694              
695             #
696             # This file is part of App-Env
697             #
698             # This software is Copyright (c) 2018 by Smithsonian Astrophysical Observatory.
699             #
700             # This is free software, licensed under:
701             #
702             # The GNU General Public License, Version 3, June 2007
703             #
704              
705             __END__