File Coverage

blib/lib/Shell/GetEnv.pm
Criterion Covered Total %
statement 153 165 92.7
branch 43 72 59.7
condition 4 15 26.6
subroutine 18 18 100.0
pod 4 4 100.0
total 222 274 81.0


line stmt bran cond sub pod time code
1             # --8<--8<--8<--8<--
2             #
3             # Copyright (C) 2007 Smithsonian Astrophysical Observatory
4             #
5             # This file is part of Shell::GetEnv
6             #
7             # Shell::GetEnv is free software: you can redistribute it and/or modify
8             # it under the terms of the GNU General Public License as published by
9             # the Free Software Foundation, either version 3 of the License, or (at
10             # your option) any later version.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15             # GNU General Public License for more details.
16             #
17             # You should have received a copy of the GNU General Public License
18             # along with this program. If not, see .
19             #
20             # -->8-->8-->8-->8--
21              
22             package Shell::GetEnv;
23              
24             require 5.008000;
25 8     8   167687 use strict;
  8         14  
  8         189  
26 8     8   27 use warnings;
  8         8  
  8         175  
27              
28 8     8   25 use Carp;
  8         13  
  8         451  
29              
30 8     8   4212 use File::Temp;
  8         102639  
  8         443  
31 8     8   2777 use Shell::GetEnv::Dumper;
  8         13  
  8         13107  
32              
33             our $VERSION = '0.10';
34              
35             my $status_var = 'p5_SHELL_GETENV_STATUS';
36              
37             # a compendium of shells
38             my %shells = (
39             bash => {
40             interactive => 'i',
41             nostartup => '--noprofile',
42             verbose => 'v',
43             echo => 'x',
44             login => 'l',
45             save_status => qq[export $status_var=\$?],
46             },
47              
48             zsh => {
49             interactive => 'i',
50             nostartup => 'p',
51             verbose => 'v',
52             echo => 'x',
53             login => 'l',
54             save_status => qq[export $status_var=\$?],
55             },
56              
57             dash => {
58             interactive => 'i',
59             verbose => 'v',
60             echo => 'x',
61             login => 'l',
62             save_status => qq[export $status_var=\$?],
63             },
64              
65             sh => {
66             interactive => 'i',
67             verbose => 'v',
68             echo => 'x',
69             login => 'l',
70             save_status => qq[$status_var=\$?; export $status_var],
71             },
72              
73             ksh => {
74             interactive => 'i',
75             nostartup => 'p',
76             verbose => 'v',
77             echo => 'x',
78             login => 'l',
79             save_status => qq[export $status_var=\$?],
80             },
81              
82             csh => {
83             interactive => 'i',
84             nostartup => 'f',
85             echo => 'x',
86             verbose => 'v',
87             login => 'l',
88             save_status => qq[setenv $status_var \$status],
89             },
90              
91             tcsh => {
92             interactive => 'i',
93             nostartup => 'f',
94             echo => 'x',
95             verbose => 'v',
96             login => 'l',
97             save_status => qq[setenv $status_var \$status],
98             },
99             );
100              
101              
102             my %Opts = (
103             startup => 1,
104             login => 0,
105             debug => 0,
106             echo => 0,
107             login => 0,
108             verbose => 0,
109             interactive => 0,
110             redirect => 1,
111             stderr => undef,
112             stdout => undef,
113             expect => 0,
114             timeout => 10,
115             shellopts => undef,
116             );
117              
118              
119             sub new
120             {
121 21     21 1 45898 my $class = shift;
122 21         36 my $shell = shift;
123              
124             croak( __PACKAGE__, "->new: unsupported shell: $shell\n" )
125 21 50       88 unless defined $shells{$shell};
126              
127 21 50       29 my %opt = %{ 'HASH' eq ref( $_[-1] ) ? pop : {} };
  21         180  
128              
129             # now want lc, but keep backwards compat
130 21         172 $opt{lc $_} = delete $opt{$_} for keys %opt;
131              
132 21         53 my @notvalid = grep { ! exists $Opts{$_} } keys %opt;
  87         146  
133 21 50       74 croak( __PACKAGE__, "->new: illegal option(s): @notvalid\n" )
134             if @notvalid;
135              
136 21         223 my $self = bless { %Opts, %opt,
137             cmds => [@_],
138             shell => $shell
139             } , $class;
140              
141             # needed to get correct hash key for %shells
142 21         93 $self->{nostartup} = ! $self->{startup};
143              
144 21         59 $self->_getenv;
145              
146 21         6402 return $self;
147             }
148              
149             # use temporary script files and output files to get the environment
150             # requires that a shell have a '-i' flag to act as an interactive shell
151             sub _getenv
152             {
153 21     21   33 my $self = shift;
154              
155             # file to hold the environment
156 21 50       166 my $fh_e = File::Temp->new( )
157             or croak( __PACKAGE__, ": unable to create temporary environment file" );
158              
159             # create script to dump environmental variables to the above file
160 21         122 push @{$self->{cmds}},
161             $shells{$self->{shell}}{save_status},
162 21         9403 $self->_dumper_script( $fh_e->filename ),
163             'exit' ;
164              
165             # construct list of command line options for the shell
166 21         74 $self->_shell_options;
167              
168             # redirect i/o streams
169 21 50       100 $self->_stream_redir if $self->{redirect};
170              
171              
172 21 50       59 if ( $self->{debug} )
173             {
174             warn( "Shell: $self->{shell}\n",
175 0         0 "Options: ", join( ' ', @{$self->{shelloptions}} ), "\n",
176 0         0 "Cmds: \n", join( "\n", @{$self->{cmds}}), "\n" );
  0         0  
177             }
178              
179              
180 21         27 eval {
181 21 100       51 if ( $self->{expect} )
182             {
183 3         13 $self->_getenv_expect( $fh_e->filename);
184             }
185             else
186             {
187 18         58 $self->_getenv_pipe( $fh_e->filename);
188             }
189             };
190 21         383402 my $error = $@;
191              
192             # reset i/o streams
193 21 50       250 $self->_stream_reset if $self->{redirect};
194              
195 21 50       163 if ( $error )
196             {
197 0         0 local $Carp::CarpLevel = 1;
198 0         0 croak $error;
199             }
200              
201              
202             # retrieve environment
203 21         249 $self->_retrieve_env( $fh_e->filename );
204             }
205              
206              
207             sub _dumper_script
208             {
209 21     21   134 my ( $self, $filename ) = @_;
210              
211             # this invokes the module directly, using the Perl which was
212             # used to invoke the parent process. It uses the fact that we
213             # use()'d Shell::GetEnv::Dumper and Perl stored the absolute path
214             # to it in %INC;
215 21         124 return qq{$^X '$INC{'Shell/GetEnv/Dumper.pm'}' $filename};
216             }
217              
218              
219              
220             # redirect STDOUT and STDERR
221             sub _stream_redir
222             {
223 22     22   835 my ( $self ) = @_;
224              
225             # redirect STDERR & STDOUT to either /dev/null or somewhere the user points
226             # us to.
227              
228 22   33     62 my $stdout = $self->{stdout} || File::Spec->devnull();
229 22   33     58 my $stderr = $self->{stderr} || File::Spec->devnull();
230              
231 22 50       286 open( $self->{oSTDOUT}, ">&STDOUT" )
232             or croak( __PACKAGE__, ': error duping STDOUT' );
233 22 50       155 open( $self->{oSTDERR}, ">&STDERR" )
234             or croak( __PACKAGE__, ': error duping STDERR' );
235              
236 22 50       1338 open( STDERR, '>', $stderr ) or
237             croak( __PACKAGE__, ": unable to redirect STDERR to $stderr" );
238 22 50       943 open( STDOUT, '>', $stdout ) or
239             croak( __PACKAGE__, ": unable to redirect STDOUT to $stdout" );
240              
241 22         65 select STDERR; $| = 1;
  22         72  
242 22         35 select STDOUT; $| = 1;
  22         49  
243             }
244              
245             # reset STDOUT and STDERR
246             sub _stream_reset
247             {
248 22     22   10104 my ( $self ) = @_;
249              
250 22         275 close STDOUT;
251 22         126 close STDERR;
252              
253 22         263 open STDOUT, '>&', $self->{oSTDOUT};
254 22         121 open STDERR, '>&', $self->{oSTDERR};
255              
256 22         134 close delete $self->{oSTDOUT};
257 22         129 close delete $self->{oSTDERR};
258             }
259              
260             # create shell options
261             sub _shell_options
262             {
263 21     21   38 my ( $self, $scriptfile ) = @_;
264              
265 21         45 my $shell = $shells{$self->{shell}};
266              
267             ## no critic (ProhibitAccessOfPrivateData)
268              
269             my @options =
270 24         76 map { $shell->{$_} }
271 21 100       51 grep { exists $shell->{$_} && $self->{$_} }
  105         412  
272             qw( nostartup echo verbose interactive login )
273             ;
274              
275             my @shellopts
276             = defined $self->{shellopts}
277             ? 'ARRAY' eq ref( $self->{shellopts} )
278 0         0 ? @{ $self->{shellopts} }
279             : $self->{shellopts}
280 21 0       69 : ();
    50          
281              
282             ## use critic
283              
284             croak( "cannot combine 'login' with any other options for $self->{shell}\n" )
285             if ( $self->{shell} eq 'csh' or $self->{shell} eq 'tcsh' )
286             && $self->{login}
287 21 0 33     160 && @options + @shellopts > 1;
      33        
      0        
288              
289             # bundled options are those without a leading hyphen or plus
290 21         31 my %options = map { ( $_ => 1 ) } @options;
  24         82  
291 21         50 my @bundled = grep{ ! /^[-+]/ } keys %options;
  24         109  
292 21         49 delete @options{@bundled};
293              
294 21 50       88 my $bundled = @bundled ? '-' . join( '', @bundled ) : undef;
295              
296             # long options; bash treats these differently
297 21         41 my @longopts = grep{ /^--/ } keys %options;
  3         10  
298 21         21 delete @options{@longopts};
299              
300             # everything else
301 21         47 my @otheropts = keys %options;
302              
303             $self->{shelloptions} =
304             [
305             # long options go first (bash complains)
306 21 50       134 @longopts,
307             ( $bundled ? $bundled : () ),
308             @otheropts,
309             @shellopts,
310             ];
311             }
312              
313             # communicate with the shell using a pipe
314             sub _getenv_pipe
315             {
316 18     18   114 my ( $self ) = @_;
317              
318 18         31 local $" = ' ';
319 18 50       67 open( my $pipe, '|-' , $self->{shell}, @{$self->{shelloptions}} )
  18         26125  
320             or die( __PACKAGE__, ": error opening pipe to $self->{shell}: $!\n" );
321              
322 18         149 print $pipe ( join( "\n", @{$self->{cmds}}), "\n");
  18         346  
323 18 50       324340 close $pipe
324             or die( __PACKAGE__, ": error closing pipe to $self->{shell}: $!\n" );
325             }
326              
327             # communicate with the shell using Expect
328             sub _getenv_expect
329             {
330 3     3   36 my ( $self, $filename ) = @_;
331              
332 3         22 require Expect;
333 3         19 my $exp = Expect->new;
334 3         1554 $exp->raw_pty(1);
335 3 50       94 $exp->spawn( $self->{shell}, @{$self->{shelloptions}} )
  3         13  
336             or die( __PACKAGE__, ": error spawning $self->{shell}\n" );
337 3         8449 $exp->send( map { $_ . "\n" } @{$self->{cmds}} );
  12         39  
  3         14  
338 3         283 $exp->expect( $self->{timeout} );
339             }
340              
341             # extract environmental variables from a dumped file
342             sub _retrieve_env
343             {
344 21     21   269 my ( $self, $filename ) = @_;
345              
346 21         150 $self->{envs} = Shell::GetEnv::Dumper::read_envs( $filename );
347 21         211 $self->{status} = delete $self->{envs}{$status_var};
348             }
349              
350             # return variables
351             sub envs
352             {
353 24     24 1 12001 my ( $self, %iopt ) = @_;
354              
355 24         173 my %opt = ( diffsonly => 0,
356             exclude => [],
357             envstr => 0,
358             zapdeleted => 0,
359             );
360              
361             # now want lc, but keep backwards compat
362 24         126 $iopt{lc $_} = delete $iopt{$_} for keys %iopt;
363              
364 24         62 my @unknown = grep { !exists $opt{$_} } keys %iopt;
  24         61  
365 24 50       70 croak( __PACKAGE__, "->envs: unknown options: @unknown\n" )
366             if @unknown;
367              
368 24         101 %opt = ( %opt, %iopt );
369              
370 24         43 my %env = %{$self->{envs}};
  24         326  
371              
372              
373             ###
374             # filter out excluded variables
375              
376             # ensure that scalars are handled correctly
377             $opt{exclude} = [ $opt{exclude} ]
378 24 100       106 unless 'ARRAY' eq ref $opt{exclude};
379              
380 24         30 foreach my $exclude ( @{$opt{exclude}} )
  24         70  
381             {
382 6         5 my @delkeys;
383              
384 6 100       29 if ( 'Regexp' eq ref $exclude )
    100          
385             {
386 1         4 @delkeys = grep { /$exclude/ } keys %env;
  23         37  
387             }
388             elsif ( 'CODE' eq ref $exclude )
389             {
390 1         4 @delkeys = grep { $exclude->($_, $env{$_}) } keys %env;
  23         79  
391             }
392             else
393             {
394 4         19 @delkeys = grep { $_ eq $exclude } keys %env;
  92         102  
395             }
396              
397 6         22 delete @env{@delkeys};
398             }
399              
400              
401             # return only variables which are new or differ from the current
402             # environment
403 24 100       67 if ( $opt{diffsonly} )
404             {
405             my @delkeys =
406 2 100       8 grep { exists $ENV{$_} && $env{$_} eq $ENV{$_} } keys %env;
  46         170  
407              
408 2         19 delete @env{@delkeys};
409             }
410              
411              
412              
413 24 100       63 if ( $opt{envstr} )
414             {
415 2         6 my @set = map { _shell_escape("$_=" . $env{$_}) } keys %env;
  24         43  
416 2         5 my @unset;
417              
418 2 50       6 if ( $opt{zapdeleted} )
419             {
420 0         0 my @deleted;
421 0 0       0 @deleted = grep { exists $ENV{$_} && ! exists $self->{envs}{$_} }
  0         0  
422             keys %ENV;
423              
424 0         0 @unset = map { "-u $_" } @deleted;
  0         0  
425             }
426              
427 2         21 return join( ' ', @unset, @set );
428             }
429              
430 22         69 return \%env;
431             }
432              
433 6     6 1 4019 sub status { $_[0]->{status} }
434              
435             sub _shell_escape
436             {
437 24     24   22 my $str = shift;
438              
439             # empty string
440 24 50       52 if ( $str eq '' )
    100          
441             {
442 0         0 $str = "''";
443             }
444              
445             # if there's white space, single quote the entire word. however,
446             # since single quotes can't be escaped inside single quotes,
447             # isolate them from the single quoted part and escape them.
448             # i.e., the string a 'b turns into 'a '\''b'
449             elsif ( $str =~ /\s/ )
450             {
451             # isolate the lone single quotes
452 4         6 $str =~ s/'/'\\''/g;
453              
454             # quote the whole string
455 4         7 $str = "'$str'";
456              
457             # remove obvious duplicate quotes.
458 4         5 $str =~ s/(^|[^\\])''/$1/g;
459             }
460              
461             # otherwise, quote all of the non-word characters
462             else
463             {
464 20         112 $str =~ s/(\W)/\\$1/go;
465             }
466              
467 24         43 $str;
468             }
469              
470              
471             sub import_envs
472             {
473 8     8 1 2588 my ( $self, %iopt ) = @_;
474              
475 8         56 my %opt = ( Exclude => [],
476             ZapDeleted => 1,
477             );
478              
479 8         29 my @unknown = grep { !exists $opt{$_} } keys %iopt;
  2         9  
480 8 50       28 croak( __PACKAGE__, "->import_envs: unknown options: @unknown\n" )
481             if @unknown;
482              
483 8         32 %opt = ( %opt, %iopt );
484 8         27 my $env = $self->envs( %opt );
485              
486             # store new values
487 8         34 while( my ( $key, $val ) = each %$env )
488             {
489 210         655 $ENV{$key} = $val;
490             }
491              
492              
493             # remove deleted ones, if requested
494 8 100       57 if ( $opt{ZapDeleted} )
495             {
496 7 50       34 delete @ENV{grep { exists $ENV{$_} && ! exists $self->{envs}{$_} }
  188         553  
497             keys %ENV };
498             }
499             }
500              
501              
502             1;
503             __END__