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   156358 use strict;
  8         12  
  8         185  
26 8     8   24 use warnings;
  8         10  
  8         159  
27              
28 8     8   26 use Carp;
  8         10  
  8         428  
29              
30 8     8   4019 use File::Temp;
  8         97390  
  8         433  
31 8     8   2586 use Shell::GetEnv::Dumper;
  8         11  
  8         12885  
32              
33             our $VERSION = '0.09_01';
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 42777 my $class = shift;
122 21         36 my $shell = shift;
123              
124             croak( __PACKAGE__, "->new: unsupported shell: $shell\n" )
125 21 50       71 unless defined $shells{$shell};
126              
127 21 50       26 my %opt = %{ 'HASH' eq ref( $_[-1] ) ? pop : {} };
  21         145  
128              
129             # now want lc, but keep backwards compat
130 21         159 $opt{lc $_} = delete $opt{$_} for keys %opt;
131              
132 21         50 my @notvalid = grep { ! exists $Opts{$_} } keys %opt;
  87         123  
133 21 50       63 croak( __PACKAGE__, "->new: illegal option(s): @notvalid\n" )
134             if @notvalid;
135              
136 21         199 my $self = bless { %Opts, %opt,
137             cmds => [@_],
138             shell => $shell
139             } , $class;
140              
141             # needed to get correct hash key for %shells
142 21         69 $self->{nostartup} = ! $self->{startup};
143              
144 21         50 $self->_getenv;
145              
146 21         5416 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   26 my $self = shift;
154              
155             # file to hold the environment
156 21 50       127 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         95 push @{$self->{cmds}},
161             $shells{$self->{shell}}{save_status},
162 21         8005 $self->_dumper_script( $fh_e->filename ),
163             'exit' ;
164              
165             # construct list of command line options for the shell
166 21         63 $self->_shell_options;
167              
168             # redirect i/o streams
169 21 50       83 $self->_stream_redir if $self->{redirect};
170              
171              
172 21 50       58 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         50 eval {
181 21 100       40 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         58760 my $error = $@;
191              
192             # reset i/o streams
193 21 50       198 $self->_stream_reset if $self->{redirect};
194              
195 21 50       149 if ( $error )
196             {
197 0         0 local $Carp::CarpLevel = 1;
198 0         0 croak $error;
199             }
200              
201              
202             # retrieve environment
203 21         192 $self->_retrieve_env( $fh_e->filename );
204             }
205              
206              
207             sub _dumper_script
208             {
209 21     21   125 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         97 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   405 my ( $self ) = @_;
224              
225             # redirect STDERR & STDOUT to either /dev/null or somewhere the user points
226             # us to.
227              
228 22   33     59 my $stdout = $self->{stdout} || File::Spec->devnull();
229 22   33     47 my $stderr = $self->{stderr} || File::Spec->devnull();
230              
231 22 50       261 open( $self->{oSTDOUT}, ">&STDOUT" )
232             or croak( __PACKAGE__, ': error duping STDOUT' );
233 22 50       154 open( $self->{oSTDERR}, ">&STDERR" )
234             or croak( __PACKAGE__, ': error duping STDERR' );
235              
236 22 50       1240 open( STDERR, '>', $stderr ) or
237             croak( __PACKAGE__, ": unable to redirect STDERR to $stderr" );
238 22 50       927 open( STDOUT, '>', $stdout ) or
239             croak( __PACKAGE__, ": unable to redirect STDOUT to $stdout" );
240              
241 22         56 select STDERR; $| = 1;
  22         70  
242 22         31 select STDOUT; $| = 1;
  22         55  
243             }
244              
245             # reset STDOUT and STDERR
246             sub _stream_reset
247             {
248 22     22   1580 my ( $self ) = @_;
249              
250 22         215 close STDOUT;
251 22         110 close STDERR;
252              
253 22         205 open STDOUT, '>&', $self->{oSTDOUT};
254 22         91 open STDERR, '>&', $self->{oSTDERR};
255              
256 22         98 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   30 my ( $self, $scriptfile ) = @_;
264              
265 21         41 my $shell = $shells{$self->{shell}};
266              
267             ## no critic (ProhibitAccessOfPrivateData)
268              
269             my @options =
270 24         64 map { $shell->{$_} }
271 21 100       39 grep { exists $shell->{$_} && $self->{$_} }
  105         392  
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       61 : ();
    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     131 && @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         72  
291 21         48 my @bundled = grep{ ! /^[-+]/ } keys %options;
  24         101  
292 21         43 delete @options{@bundled};
293              
294 21 50       63 my $bundled = @bundled ? '-' . join( '', @bundled ) : undef;
295              
296             # long options; bash treats these differently
297 21         34 my @longopts = grep{ /^--/ } keys %options;
  3         13  
298 21         26 delete @options{@longopts};
299              
300             # everything else
301 21         35 my @otheropts = keys %options;
302              
303             $self->{shelloptions} =
304             [
305             # long options go first (bash complains)
306 21 50       118 @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   105 my ( $self ) = @_;
317              
318 18         24 local $" = ' ';
319 18 50       63 open( my $pipe, '|-' , $self->{shell}, @{$self->{shelloptions}} )
  18         21222  
320             or die( __PACKAGE__, ": error opening pipe to $self->{shell}: $!\n" );
321              
322 18         122 print $pipe ( join( "\n", @{$self->{cmds}}), "\n");
  18         326  
323 18 50       282191 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   21 my ( $self, $filename ) = @_;
331              
332 3         20 require Expect;
333 3         21 my $exp = Expect->new;
334 3         1506 $exp->raw_pty(1);
335 3 50       78 $exp->spawn( $self->{shell}, @{$self->{shelloptions}} )
  3         11  
336             or die( __PACKAGE__, ": error spawning $self->{shell}\n" );
337 3         6952 $exp->send( map { $_ . "\n" } @{$self->{cmds}} );
  12         36  
  3         12  
338 3         249 $exp->expect( $self->{timeout} );
339             }
340              
341             # extract environmental variables from a dumped file
342             sub _retrieve_env
343             {
344 21     21   219 my ( $self, $filename ) = @_;
345              
346 21         114 $self->{envs} = Shell::GetEnv::Dumper::read_envs( $filename );
347 21         188 $self->{status} = delete $self->{envs}{$status_var};
348             }
349              
350             # return variables
351             sub envs
352             {
353 24     24 1 8910 my ( $self, %iopt ) = @_;
354              
355 24         135 my %opt = ( diffsonly => 0,
356             exclude => [],
357             envstr => 0,
358             zapdeleted => 0,
359             );
360              
361             # now want lc, but keep backwards compat
362 24         110 $iopt{lc $_} = delete $iopt{$_} for keys %iopt;
363              
364 24         47 my @unknown = grep { !exists $opt{$_} } keys %iopt;
  24         51  
365 24 50       71 croak( __PACKAGE__, "->envs: unknown options: @unknown\n" )
366             if @unknown;
367              
368 24         90 %opt = ( %opt, %iopt );
369              
370 24         38 my %env = %{$self->{envs}};
  24         279  
371              
372              
373             ###
374             # filter out excluded variables
375              
376             # ensure that scalars are handled correctly
377             $opt{exclude} = [ $opt{exclude} ]
378 24 100       93 unless 'ARRAY' eq ref $opt{exclude};
379              
380 24         27 foreach my $exclude ( @{$opt{exclude}} )
  24         70  
381             {
382 6         3 my @delkeys;
383              
384 6 100       23 if ( 'Regexp' eq ref $exclude )
    100          
385             {
386 1         3 @delkeys = grep { /$exclude/ } keys %env;
  23         37  
387             }
388             elsif ( 'CODE' eq ref $exclude )
389             {
390 1         3 @delkeys = grep { $exclude->($_, $env{$_}) } keys %env;
  23         55  
391             }
392             else
393             {
394 4         11 @delkeys = grep { $_ eq $exclude } keys %env;
  92         71  
395             }
396              
397 6         14 delete @env{@delkeys};
398             }
399              
400              
401             # return only variables which are new or differ from the current
402             # environment
403 24 100       52 if ( $opt{diffsonly} )
404             {
405             my @delkeys =
406 2 100       6 grep { exists $ENV{$_} && $env{$_} eq $ENV{$_} } keys %env;
  46         125  
407              
408 2         10 delete @env{@delkeys};
409             }
410              
411              
412              
413 24 100       47 if ( $opt{envstr} )
414             {
415 2         6 my @set = map { _shell_escape("$_=" . $env{$_}) } keys %env;
  24         35  
416 2         3 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         13 return join( ' ', @unset, @set );
428             }
429              
430 22         58 return \%env;
431             }
432              
433 6     6 1 3302 sub status { $_[0]->{status} }
434              
435             sub _shell_escape
436             {
437 24     24   24 my $str = shift;
438              
439             # empty string
440 24 50       43 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         7 $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         102 $str =~ s/(\W)/\\$1/go;
465             }
466              
467 24         43 $str;
468             }
469              
470              
471             sub import_envs
472             {
473 8     8 1 2617 my ( $self, %iopt ) = @_;
474              
475 8         36 my %opt = ( Exclude => [],
476             ZapDeleted => 1,
477             );
478              
479 8         22 my @unknown = grep { !exists $opt{$_} } keys %iopt;
  2         9  
480 8 50       27 croak( __PACKAGE__, "->import_envs: unknown options: @unknown\n" )
481             if @unknown;
482              
483 8         28 %opt = ( %opt, %iopt );
484 8         26 my $env = $self->envs( %opt );
485              
486             # store new values
487 8         30 while( my ( $key, $val ) = each %$env )
488             {
489 210         673 $ENV{$key} = $val;
490             }
491              
492              
493             # remove deleted ones, if requested
494 8 100       29 if ( $opt{ZapDeleted} )
495             {
496 7 50       29 delete @ENV{grep { exists $ENV{$_} && ! exists $self->{envs}{$_} }
  188         554  
497             keys %ENV };
498             }
499             }
500              
501              
502             1;
503             __END__