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   154702 use strict;
  8         14  
  8         473  
26 8     8   25 use warnings;
  8         8  
  8         169  
27              
28 8     8   24 use Carp;
  8         11  
  8         441  
29              
30 8     8   3943 use File::Temp;
  8         100188  
  8         474  
31 8     8   2580 use Shell::GetEnv::Dumper;
  8         12  
  8         13555  
32              
33             our $VERSION = '0.09';
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 \$?],
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 \$?],
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 50897 my $class = shift;
122 21         38 my $shell = shift;
123              
124             croak( __PACKAGE__, "->new: unsupported shell: $shell\n" )
125 21 50       82 unless defined $shells{$shell};
126              
127 21 50       29 my %opt = %{ 'HASH' eq ref( $_[-1] ) ? pop : {} };
  21         181  
128              
129             # now want lc, but keep backwards compat
130 21         191 $opt{lc $_} = delete $opt{$_} for keys %opt;
131              
132 21         57 my @notvalid = grep { ! exists $Opts{$_} } keys %opt;
  87         172  
133 21 50       77 croak( __PACKAGE__, "->new: illegal option(s): @notvalid\n" )
134             if @notvalid;
135              
136 21         239 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         6722 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   32 my $self = shift;
154              
155             # file to hold the environment
156 21 50       171 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         135 push @{$self->{cmds}},
161             $shells{$self->{shell}}{save_status},
162 21         9706 $self->_dumper_script( $fh_e->filename ),
163             'exit' ;
164              
165             # construct list of command line options for the shell
166 21         72 $self->_shell_options;
167              
168             # redirect i/o streams
169 21 50       110 $self->_stream_redir if $self->{redirect};
170              
171              
172 21 50       61 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         31 eval {
181 21 100       50 if ( $self->{expect} )
182             {
183 3         12 $self->_getenv_expect( $fh_e->filename);
184             }
185             else
186             {
187 18         61 $self->_getenv_pipe( $fh_e->filename);
188             }
189             };
190 21         74571 my $error = $@;
191              
192             # reset i/o streams
193 21 50       252 $self->_stream_reset if $self->{redirect};
194              
195 21 50       162 if ( $error )
196             {
197 0         0 local $Carp::CarpLevel = 1;
198 0         0 croak $error;
199             }
200              
201              
202             # retrieve environment
203 21         262 $self->_retrieve_env( $fh_e->filename );
204             }
205              
206              
207             sub _dumper_script
208             {
209 21     21   142 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         135 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   670 my ( $self ) = @_;
224              
225             # redirect STDERR & STDOUT to either /dev/null or somewhere the user points
226             # us to.
227              
228 22   33     67 my $stdout = $self->{stdout} || File::Spec->devnull();
229 22   33     52 my $stderr = $self->{stderr} || File::Spec->devnull();
230              
231 22 50       350 open( $self->{oSTDOUT}, ">&STDOUT" )
232             or croak( __PACKAGE__, ': error duping STDOUT' );
233 22 50       174 open( $self->{oSTDERR}, ">&STDERR" )
234             or croak( __PACKAGE__, ': error duping STDERR' );
235              
236 22 50       1429 open( STDERR, '>', $stderr ) or
237             croak( __PACKAGE__, ": unable to redirect STDERR to $stderr" );
238 22 50       1043 open( STDOUT, '>', $stdout ) or
239             croak( __PACKAGE__, ": unable to redirect STDOUT to $stdout" );
240              
241 22         71 select STDERR; $| = 1;
  22         79  
242 22         41 select STDOUT; $| = 1;
  22         56  
243             }
244              
245             # reset STDOUT and STDERR
246             sub _stream_reset
247             {
248 22     22   2279 my ( $self ) = @_;
249              
250 22         295 close STDOUT;
251 22         151 close STDERR;
252              
253 22         303 open STDOUT, '>&', $self->{oSTDOUT};
254 22         138 open STDERR, '>&', $self->{oSTDERR};
255              
256 22         140 close delete $self->{oSTDOUT};
257 22         144 close delete $self->{oSTDERR};
258             }
259              
260             # create shell options
261             sub _shell_options
262             {
263 21     21   41 my ( $self, $scriptfile ) = @_;
264              
265 21         51 my $shell = $shells{$self->{shell}};
266              
267             ## no critic (ProhibitAccessOfPrivateData)
268              
269             my @options =
270 24         75 map { $shell->{$_} }
271 21 100       51 grep { exists $shell->{$_} && $self->{$_} }
  105         435  
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       75 : ();
    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     151 && @options + @shellopts > 1;
      33        
      0        
288              
289             # bundled options are those without a leading hyphen or plus
290 21         42 my %options = map { ( $_ => 1 ) } @options;
  24         91  
291 21         52 my @bundled = grep{ ! /^[-+]/ } keys %options;
  24         147  
292 21         52 delete @options{@bundled};
293              
294 21 50       89 my $bundled = @bundled ? '-' . join( '', @bundled ) : undef;
295              
296             # long options; bash treats these differently
297 21         39 my @longopts = grep{ /^--/ } keys %options;
  3         13  
298 21         28 delete @options{@longopts};
299              
300             # everything else
301 21         48 my @otheropts = keys %options;
302              
303             $self->{shelloptions} =
304             [
305             # long options go first (bash complains)
306 21 50       143 @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   113 my ( $self ) = @_;
317              
318 18         32 local $" = ' ';
319 18 50       64 open( my $pipe, '|-' , $self->{shell}, @{$self->{shelloptions}} )
  18         29374  
320             or die( __PACKAGE__, ": error opening pipe to $self->{shell}: $!\n" );
321              
322 18         161 print $pipe ( join( "\n", @{$self->{cmds}}), "\n");
  18         503  
323 18 50       332716 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   33 my ( $self, $filename ) = @_;
331              
332 3         27 require Expect;
333 3         33 my $exp = Expect->new;
334 3         2165 $exp->raw_pty(1);
335 3 50       116 $exp->spawn( $self->{shell}, @{$self->{shelloptions}} )
  3         19  
336             or die( __PACKAGE__, ": error spawning $self->{shell}\n" );
337 3         9832 $exp->send( map { $_ . "\n" } @{$self->{cmds}} );
  12         53  
  3         18  
338 3         423 $exp->expect( $self->{timeout} );
339             }
340              
341             # extract environmental variables from a dumped file
342             sub _retrieve_env
343             {
344 21     21   258 my ( $self, $filename ) = @_;
345              
346 21         139 $self->{envs} = Shell::GetEnv::Dumper::read_envs( $filename );
347 21         236 $self->{status} = delete $self->{envs}{$status_var};
348             }
349              
350             # return variables
351             sub envs
352             {
353 24     24 1 13303 my ( $self, %iopt ) = @_;
354              
355 24         170 my %opt = ( diffsonly => 0,
356             exclude => [],
357             envstr => 0,
358             zapdeleted => 0,
359             );
360              
361             # now want lc, but keep backwards compat
362 24         132 $iopt{lc $_} = delete $iopt{$_} for keys %iopt;
363              
364 24         71 my @unknown = grep { !exists $opt{$_} } keys %iopt;
  24         56  
365 24 50       88 croak( __PACKAGE__, "->envs: unknown options: @unknown\n" )
366             if @unknown;
367              
368 24         124 %opt = ( %opt, %iopt );
369              
370 24         43 my %env = %{$self->{envs}};
  24         355  
371              
372              
373             ###
374             # filter out excluded variables
375              
376             # ensure that scalars are handled correctly
377             $opt{exclude} = [ $opt{exclude} ]
378 24 100       133 unless 'ARRAY' eq ref $opt{exclude};
379              
380 24         31 foreach my $exclude ( @{$opt{exclude}} )
  24         87  
381             {
382 6         3 my @delkeys;
383              
384 6 100       25 if ( 'Regexp' eq ref $exclude )
    100          
385             {
386 1         4 @delkeys = grep { /$exclude/ } keys %env;
  23         33  
387             }
388             elsif ( 'CODE' eq ref $exclude )
389             {
390 1         4 @delkeys = grep { $exclude->($_, $env{$_}) } keys %env;
  23         56  
391             }
392             else
393             {
394 4         14 @delkeys = grep { $_ eq $exclude } keys %env;
  92         80  
395             }
396              
397 6         18 delete @env{@delkeys};
398             }
399              
400              
401             # return only variables which are new or differ from the current
402             # environment
403 24 100       74 if ( $opt{diffsonly} )
404             {
405             my @delkeys =
406 2 100       7 grep { exists $ENV{$_} && $env{$_} eq $ENV{$_} } keys %env;
  46         128  
407              
408 2         11 delete @env{@delkeys};
409             }
410              
411              
412              
413 24 100       67 if ( $opt{envstr} )
414             {
415 2         5 my @set = map { _shell_escape("$_=" . $env{$_}) } keys %env;
  24         42  
416 2         3 my @unset;
417              
418 2 50       5 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         15 return join( ' ', @unset, @set );
428             }
429              
430 22         84 return \%env;
431             }
432              
433 6     6 1 3929 sub status { $_[0]->{status} }
434              
435             sub _shell_escape
436             {
437 24     24   21 my $str = shift;
438              
439             # empty string
440 24 50       56 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         6 $str = "'$str'";
456              
457             # remove obvious duplicate quotes.
458 4         4 $str =~ s/(^|[^\\])''/$1/g;
459             }
460              
461             # otherwise, quote all of the non-word characters
462             else
463             {
464 20         103 $str =~ s/(\W)/\\$1/go;
465             }
466              
467 24         46 $str;
468             }
469              
470              
471             sub import_envs
472             {
473 8     8 1 2842 my ( $self, %iopt ) = @_;
474              
475 8         41 my %opt = ( Exclude => [],
476             ZapDeleted => 1,
477             );
478              
479 8         21 my @unknown = grep { !exists $opt{$_} } keys %iopt;
  2         9  
480 8 50       25 croak( __PACKAGE__, "->import_envs: unknown options: @unknown\n" )
481             if @unknown;
482              
483 8         27 %opt = ( %opt, %iopt );
484 8         34 my $env = $self->envs( %opt );
485              
486             # store new values
487 8         31 while( my ( $key, $val ) = each %$env )
488             {
489 210         628 $ENV{$key} = $val;
490             }
491              
492              
493             # remove deleted ones, if requested
494 8 100       35 if ( $opt{ZapDeleted} )
495             {
496 7 50       41 delete @ENV{grep { exists $ENV{$_} && ! exists $self->{envs}{$_} }
  188         508  
497             keys %ENV };
498             }
499             }
500              
501              
502             1;
503             __END__