File Coverage

blib/lib/Psh/Parser.pm
Criterion Covered Total %
statement 6 353 1.7
branch 0 200 0.0
condition 0 117 0.0
subroutine 2 16 12.5
pod 0 12 0.0
total 8 698 1.1


line stmt bran cond sub pod time code
1             #! /usr/local/bin/perl -w
2             package Psh::Parser;
3              
4 1     1   4 use strict;
  1         2  
  1         1652  
5              
6             require Psh::OS;
7             require Psh::Util;
8             require Psh::Strategy;
9              
10             sub T_END() { 0; }
11             sub T_WORD() { 1; }
12             sub T_PIPE() { 2; }
13             sub T_REDIRECT() { 3; }
14             sub T_BACKGROUND() { 4; }
15             sub T_OR() { 5; }
16             sub T_AND() { 6; }
17              
18             sub T_EXECUTE() { 1; }
19              
20             # ugly, ugly, but makes things faster
21              
22             my %quotehash = qw|' ' " " q( ) qw( ) qq( ) ` `|;
23             my %quotedquotes = ();
24             my $def_quoteexp;
25             my $def_tokenizer= '(\\s+|\\|\\||\\&\\&|\||=>|->|;;|;|\\&|>>|>|<<|<|\\(|\\)|\\{|\\}|\\[|\\])';
26             my $nevermatches = "(?!a)a";
27              
28              
29             $def_quoteexp = $nevermatches;
30             foreach my $opener (keys %quotehash) {
31             $def_quoteexp .= '|' . quotemeta($opener);
32             $quotedquotes{$opener} = quotemeta($quotehash{$opener});
33             }
34              
35             my $stdallinall= "^((?:[^\\\\]|\\\\.)*?)(?:$def_tokenizer|($def_quoteexp))(.*)\$";
36              
37             if ($]>=5.005) {
38             eval {
39             $stdallinall= qr{$stdallinall}s;
40             };
41             }
42              
43             sub decompose {
44 0     0 0   my ($delimexp,$line,$num,$keep,$unmatched) = @_;
45 0           my @matches;
46              
47 0 0         if (!defined($delimexp)) { $delimexp = $def_tokenizer; }
  0 0          
48 0           elsif ($delimexp eq ' ') { $delimexp='(\s+)'; }
49              
50 0 0         if (!defined($num)) { $num = -1; }
  0            
51 0 0         if (!defined($keep)) { $keep = 1; }
  0            
52              
53             # Remember if delimexp came with any parenthesized subexpr, and
54             # arrange for it to have exactly one so we know what each piece in
55             # the match below means:
56              
57 0           my $saveDelimiters = 0;
58 0           @matches = ('x' =~ m/$delimexp|(.)/);
59 0 0         if (@matches > 2) {
60 0           require Carp;
61 0           Carp::carp("Delimiter regexp '$delimexp' in decompose may " .
62             "contain at most 1 ().");
63 0           return undef;
64             }
65 0 0         if (@matches == 2) {
66 0           $saveDelimiters = 1;
67             } else {
68 0           $delimexp = "($delimexp)";
69             }
70              
71 0           return _decompose($line, "^((?:[^\\\\]|\\\\.)*?)(?:$delimexp|($def_quoteexp))(.*)\$", $keep, $num, $unmatched, $saveDelimiters-1);
72             }
73              
74             sub _decompose
75             {
76 0     0     my ( $line, $regexp, $keep, $num, $unmatched, $saveDelimiters)= @_;
77              
78 0           $saveDelimiters++;
79 0           my @pieces = ('');
80 0           my $startNewPiece = 0;
81 0           my $freshPiece = 1;
82 0           my $uquote = 0;
83 0           while ($line) {
84 0 0         if ($startNewPiece) {
85 0           push @pieces, '';
86 0           $startNewPiece = 0;
87 0           $freshPiece = 1;
88             }
89 0 0         if (@pieces == $num) { last; }
  0            
90              
91             # $delimexp is unparenthesized below because we have
92             # already arranged for it to contain exactly one backref ()
93 0           my ($prefix,$delimiter,$quote,$rest) =
94             ($line =~ m/$regexp/s);
95 0 0 0       if (!$keep and defined($prefix)) {
96 0           $prefix= remove_backslash($prefix);
97             }
98 0 0         if (defined($delimiter)) {
    0          
99 0           $pieces[$#pieces] .= $prefix;
100 0 0 0       if ($saveDelimiters) {
    0          
101 0 0 0       if (length($pieces[$#pieces]) or !$freshPiece) {
102 0           push @pieces, $delimiter;
103             } else {
104 0           $pieces[$#pieces] = $delimiter;
105             }
106 0           $startNewPiece = 1;
107             } elsif (@pieces > 1 or $pieces[0]) {
108 0           $startNewPiece = 1;
109             }
110 0           $line = $rest;
111             } elsif (defined($quote)) {
112 0           my ($restOfQuote,$remainder) =
113             ($rest =~ m/^((?:[^\\]|\\.)*?)$quotedquotes{$quote}(.*)$/s);
114 0 0         if (defined($restOfQuote)) {
115 0 0 0       if (!$keep and
      0        
116             $quote ne "\'" and $quote ne 'q(') {
117 0           $restOfQuote= remove_backslash($restOfQuote);
118             }
119 0           $pieces[$#pieces]= join('',$pieces[$#pieces],$prefix,
120             $quote,$restOfQuote,
121             $quotehash{$quote});
122 0           $line = $remainder;
123 0           $freshPiece = 0;
124             } else { # can't find matching quote, give up
125 0           $uquote = 1;
126 0           last;
127             }
128             } else { # nothing found, so remainder all one unquoted piece
129 0 0 0       if (!$keep and length($line)) {
130 0           $line= remove_backslash($line);
131             }
132 0           last;
133             }
134             }
135 0 0         if (length($line)) { $pieces[$#pieces] .= $line; }
  0            
136 0 0         if (defined($unmatched)) { ${$unmatched} = $uquote; }
  0            
  0            
137 0 0         return wantarray?@pieces:\@pieces;
138             }
139              
140             sub incomplete_expr
141             {
142 0     0 0   my ($line) = @_;
143 0 0         return 0 unless $line=~/[\[{('"]/s;
144              
145 0           my $unmatch = 0;
146 0           my @words= @{scalar(_decompose($line,$stdallinall, 1, undef, \$unmatch))};
  0            
147 0 0         if ($unmatch) { return 2; }
  0            
148              
149 0           my @openstack = (':'); # : is used as a bottom marker here
150 0           my %open_of_close = qw|) ( } { ] [ " '|;
151              
152 0           foreach my $word (@words) {
153 0 0         next if length($word)!=1;
154 0 0 0       if ($word eq '[' or $word eq '{' or $word eq '(' or $word eq '"' or
    0 0        
      0        
      0        
      0        
      0        
      0        
      0        
155             $word eq "\"") {
156 0           push @openstack, $word;
157             } elsif ($word eq ')' or $word eq '}' or $word eq ']' or $word eq '"' or
158             $word eq "\"") {
159 0           my $open= $open_of_close{$word};
160 0           my $curopen = pop @openstack;
161 0 0         if ($open ne $curopen) {
162 0           return -1;
163             }
164             }
165             }
166 0 0         if (scalar(@openstack) > 1) { return 1; }
  0            
167 0           return 0;
168             }
169              
170             #
171             # glob_expansion()
172             #
173             # LINE EXPANSIONS:
174             #
175             # If we're going to be a shell, let's act like a shell. The idea here
176             # is to provide expansion functions that individual evaluation
177             # strategies can use on the argument list to perform operations
178             # similar to the ones a shell argument list undergoes. Each of these
179             # functions should take a reference to an array of "words" and return
180             # a solid (to be conservative, as opposed to modifying in place) array of
181             # "expanded words".
182             #
183             # Bash defines eight types of expansion in its manpage: brace
184             # expansion, tilde expansion, parameter and variable expansion,
185             # command substitution, arithmetic expansion, word splitting,
186             # pathname expansion, and process expansion.
187             #
188             # Of these, arithmetic expansion makes no sense in Perl. Word
189             # splitting should happen "on the fly", i.e., the array returned by
190             # one of these functions might have more elements than the argument
191             # did. Since the perl builtin "glob" handles brace, tilde and pathname
192             # expansion, here's a glob_expansion function that covers all of
193             # those. Also a variable_expansion function that handles substituting
194             # in the values of Perl variables. That leaves only:
195             #
196             # TODO: command_expansion (i.e., backticks. For this,
197             # backticks would have to be added to decompose as a recognized quote
198             # character), process_expansion
199             #
200             # TODO: should some of these line-processing actions happen in a
201             # uniform way, or should things simply be left to each evaluation strategy
202             # as psh currently works?
203             #
204             # array glob_expansion (arrayref WORDS)
205             #
206             # For each element x of the array referred to by WORDS, such that x
207             # is not quoted, push glob(x) onto an array, and return the collected array.
208             #
209              
210             sub glob_expansion
211             {
212 0     0 0   my $arref= shift;
213 0           my $join_char= shift;
214 0           my @retval = ();
215              
216 0           for my $word (@{$arref}) {
  0            
217 0 0 0       if ($word =~ m/['"']/ # if it contains quotes
218             or ($word !~ m/{.*}|\[.*\]|[*?~]/)) { # or no globbing characters
219 0           push @retval, $word; # don't try to glob it
220             } else {
221             # Glob it. If anything happens, quote the
222             # results so they won't be clobbbered later.
223 0           my @results = Psh::OS::glob($word);
224 0 0 0       if (scalar(@results) == 0) {
    0          
225 0           @results = ($word);
226             } elsif (scalar(@results)>1 or $results[0] ne $word) {
227 0           foreach (@results) { $_ = "'$_'"; }
  0            
228             }
229 0 0         if( $join_char) {
230 0           push @retval, join($join_char, @results);
231             } else {
232 0           push @retval, @results;
233             }
234             }
235             }
236              
237 0           return @retval;
238             }
239              
240             sub unquote {
241 0     0 0   my $text= shift;
242              
243 0 0 0       if (substr($text,0,1) eq '\'' and
    0 0        
    0          
244             substr($text,-1,1) eq '\'') {
245 0           $text= substr($text,1,-1);
246             } elsif ( substr($text,0,1) eq "\"" and
247             substr($text,-1,1) eq "\"") {
248 0           $text= substr($text,1,-1);
249             } elsif (substr($text,0,1) eq "\\") {
250 0           $text= substr($text,1);
251             }
252 0           return $text;
253             }
254              
255             sub remove_backslash {
256 0     0 0   my $text= shift;
257              
258 0           $text=~ s/\\t/\t/g;
259 0           $text=~ s/\\n/\n/g;
260 0           $text=~ s/\\r/\r/g;
261 0           $text=~ s/\\f/\f/g;
262 0           $text=~ s/\\b/\b/g;
263 0           $text=~ s/\\a/\a/g;
264 0           $text=~ s/\\e/\e/g;
265 0           $text=~ s/\\(0[0-7][0-7])/chr(oct($1))/ge;
  0            
266 0           $text=~ s/\\(x[0-9a-fA-F][0-9a-fA-F])/chr(oct($1))/ge;
  0            
267 0           $text=~ s/\\(.)/$1/g;
268 0           return $text;
269             }
270              
271             sub ungroup {
272 0     0 0   my $text= shift;
273 0 0 0       if (substr($text,0,1) eq '(' and
    0 0        
274             substr($text,-1,1) eq ')') {
275 0           return substr($text,1,-1);
276             } elsif (substr($text,0,1) eq '{' and
277             substr($text,-1,1) eq '}') {
278 0           return substr($text,1,-1);
279             }
280 0           return $text;
281             }
282              
283             sub parse_fileno {
284 0     0 0   my $tmp= shift;
285 0           my $default1= shift;
286 0           my $default2= shift;
287            
288 0           my @tmp= split('=', $tmp); # [out=in] - not supported fully yet
289 0 0         if (@tmp>2) {
290 0           return undef;
291             }
292 0 0         if (@tmp<2) {
293 0           push @tmp, $default2;
294             }
295 0 0 0       if (@tmp==2 && !$tmp[0]) {
296 0           $tmp[0]= $default1;
297             }
298 0           my @result=();
299 0           foreach (@tmp) {
300 1     1   7 no strict 'refs';
  1         1  
  1         2726  
301 0 0         if (lc($_) eq 'all') {
302 0           $_=1;
303             }
304 0 0         if (/^\d+$/) {
305 0           push @result, $_+0;
306             } else {
307 0 0         if (ref *{"$Psh::PerlEval::current_package\:\:$_"}{FILEHANDLE}) {
  0            
308 0           push @result, fileno(*{"$Psh::PerlEval::current_package\:\:$_"});
  0            
309             }
310             }
311             }
312 0           return @result;
313             }
314              
315             sub make_tokens {
316 0     0 0   my $line= shift;
317 0           my $splitonly= shift;
318 0           my @tmpparts= @{scalar(_decompose($line,$stdallinall, 0))};
  0            
319 0 0         return @tmpparts if $splitonly;
320              
321             # Walk through parts and combine parenthesized parts properly
322 0           my @parts=();
323 0           my $nestlevel=0;
324 0           my @tmp=();
325 0           foreach (@tmpparts) {
326 0 0         if (length($_)==1) {
327 0 0 0       if ($_ eq '[' or $_ eq '(' or $_ eq '{') {
    0 0        
      0        
      0        
328 0           $nestlevel++;
329             } elsif ($_ eq '}' or $_ eq ')' or $_ eq ']') {
330 0           $nestlevel--;
331             }
332             }
333 0 0         if ($nestlevel) {
    0          
334 0           push @tmp, $_;
335             } elsif (@tmp) {
336 0           push @parts,join('',@tmp,$_);
337 0           @tmp=();
338             } else {
339 0           push @parts, $_;
340             }
341             }
342              
343 0           my @tokens= ();
344 0           my @t=();
345 0           my $tmp;
346 0           while( defined($tmp= shift @parts)) {
347 0 0 0       if ($tmp eq '||' or $tmp eq '&&') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
348 0           push @t, @tokens;
349 0 0         push @t, [T_END],[$tmp eq '||'?T_OR:T_AND];
350 0           @tokens=();
351             }
352             elsif ($tmp eq ';;') {
353 0           push @tokens, [T_WORD,';'];
354             }
355             elsif( $tmp eq '|') {
356 0           my @fileno=(1,0);
357 0 0         if (@parts>0) {
358 0           my $tmp= shift @parts;
359 0 0         if ($tmp=~/^\[(.+?)\]$/) {
360 0           my $tmp2= $1;
361 0 0         if (lc($tmp2) eq 'all') {
362 0           push @tokens, [T_REDIRECT, '>&', 2, 1];
363             }
364 0           @fileno= parse_fileno($tmp2,1,0);
365 0 0         if (!@fileno) {
366 0           print STDERR "Illegal syntax\n"; ## FIXME
367 0           return undef;
368             }
369             } else {
370 0           unshift @parts, $tmp;
371             }
372             }
373 0           push @t, [T_REDIRECT, '>&', $fileno[0], 'chainout']; # needs to come first
374 0           push @t, @tokens;
375 0           push @t, [T_PIPE];
376 0           @tokens=( [T_REDIRECT, '<&', $fileno[1], 'chainin']);
377             } elsif( $tmp =~ /^(>>?)$/) {
378 0           my $tmp= $1;
379              
380 0           my $file;
381 0           my @fileno=(1,0);
382 0           my $allflag=0;
383 0 0         if (@parts>0) {
384 0           my $tmp= shift @parts;
385 0 0         if ($tmp=~/^\[(.+?)\]$/) {
386 0           my $tmp2= $1;
387 0 0         if (lc($tmp2) eq 'all') {
388 0           $allflag=1;
389             }
390 0           @fileno= parse_fileno($tmp2,1,0);
391 0 0         if (!@fileno) {
392 0           print STDERR "Illegal syntax\n"; ## FIXME
393 0           return undef;
394             }
395             } else {
396 0           unshift @parts, $tmp;
397             }
398             }
399 0 0         if ($fileno[1]==0) {
400 0           while( @parts>0) {
401 0           $file= shift @parts;
402 0 0         last if( $file !~ /^\s+$/);
403 0           $file='';
404             }
405 0 0 0       if( !$file or substr($file,0,1) eq '&') {
406 0           Psh::Util::print_error_i18n('redirect_file_missing',
407             $tmp,$Psh::bin);
408 0           return undef;
409             }
410 0           push @tokens, [T_REDIRECT,$tmp,$fileno[0],unquote($file)];
411             } else {
412 0           push @tokens, [T_REDIRECT, '>&', @fileno];
413             }
414 0 0         if ($allflag) {
415 0           push @tokens, [T_REDIRECT, '>&', 2, 1];
416             }
417             } elsif( $tmp eq '<') {
418 0           my $file;
419 0           my @fileno=(0,0);
420 0 0         if (@parts>0) {
421 0           my $tmp= shift @parts;
422 0 0         if ($tmp=~/^\[(.+?)\]$/) {
423 0           @fileno= parse_fileno($1,0,0);
424 0 0         if (!@fileno) {
425 0           print STDERR "Illegal syntax\n"; ## FIXME
426 0           return undef;
427             }
428             }
429             else {
430 0           unshift @parts, $tmp;
431             }
432             }
433 0 0         if ($fileno[0]==0) {
434 0           while( @parts>0) {
435 0           $file= shift @parts;
436 0 0         last if( $file !~ /^\s+$/);
437 0           $file='';
438             }
439 0 0 0       if( !$file or substr($file,0,1) eq '&') {
440 0           Psh::Util::print_error_i18n('redirect_file_missing',
441             $tmp,$Psh::bin);
442 0           return undef;
443             }
444 0           push @tokens, [T_REDIRECT,'<',$fileno[1],unquote($file)];
445             } else {
446 0           push @tokens, [T_REDIRECT,'<&',$fileno[1],$fileno[0]];
447             }
448             } elsif( $tmp eq '&') {
449 0           push @t, @tokens;
450 0           push @t, [T_BACKGROUND],[T_END];
451 0           @tokens=();
452             } elsif( $tmp eq ';') {
453 0           push @t, @tokens;
454 0           push @t, [T_END];
455 0           @tokens= ();
456             } elsif ($tmp eq '`') {
457 0           my $tmp='';
458 0           while ( (my $tmp2= shift @parts) ne '`' ) {
459 0           $tmp.=' '.$tmp2;
460             }
461 0           $tmp= Psh::OS::backtick($tmp);
462 0           $tmp=~ s/\\/\\\\/g;
463 0           $tmp=~ s/\"/\\\"/g;
464 0           $tmp=~ s/\n/\\n/g;
465 0           $tmp=~ s/\$/\\\$/g;
466 0           $tmp=~ s/\@/\\\@/g;
467 0           push @tokens, [T_WORD, join('','"', $tmp,'"')];
468             } elsif( $tmp=~ /^\s+$/) {
469             } else {
470 0           push @tokens, [T_WORD,$tmp];
471             }
472             }
473 0           push @t, @tokens;
474 0           return @t;
475             }
476              
477             sub parse_line {
478 0     0 0   my $line= shift;
479 0           my (@use_strats) = @_;
480              
481 0 0         return () if substr($line,0,1) eq '#';
482              
483 0           my ($lvl1,$lvl2,$lvl3);
484 0 0         if (@use_strats) {
    0          
485 0           ($lvl1,$lvl2,$lvl3)= Psh::Strategy::parser_return_objects(@use_strats);
486             } elsif (@Psh::temp_use_strats) {
487 0           ($lvl1,$lvl2,$lvl3)= Psh::Strategy::parser_return_objects(@Psh::temp_use_strats);
488             } else {
489 0           ($lvl1,$lvl2,$lvl3)= Psh::Strategy::parser_strategy_list();
490             }
491              
492 0 0         if (@$lvl1) {
493 0           foreach my $strategy (@$lvl1) {
494 0           my $how= eval {
495 0           $strategy->applies(\$line);
496             };
497 0 0         if ($@) {
    0          
498 0           print STDERR $@;
499             } elsif ($how) {
500 0           my $name= $strategy->name;
501 0           Psh::Util::print_debug_class('s',
502             "[Using strategy $name: $how]\n");
503 0           return ([ T_EXECUTE, 1, [$strategy, $how, [], [$line], $line ]]);
504             }
505             }
506             }
507 0 0         if (@$lvl2) {
508 0           die "Level 2 Strategies currently not supported!";
509             }
510 0 0         if (@$lvl3) {
511 0           my @tokens= make_tokens( $line);
512 0           my @elements=();
513 0           my $element;
514 0           while( @tokens > 0) {
515 0           $element=parse_complex_command(\@tokens,$lvl3);
516 0 0         return undef if ! defined( $element); # TODO: Error handling
517 0           push @elements, $element;
518 0 0         if (@tokens > 0) {
519 0 0         if ($tokens[0][0] == T_END) {
520 0           shift @tokens;
521             }
522 0 0         if (@tokens > 0) {
523 0 0         if ($tokens[0][0] == T_AND) {
    0          
524 0           shift @tokens;
525 0           push @elements, [ T_AND ];
526             } elsif ($tokens[0][0] == T_OR) {
527 0           shift @tokens;
528 0           push @elements, [ T_OR ];
529             }
530             }
531             }
532             }
533 0           return @elements;
534             }
535             }
536              
537             sub parse_complex_command {
538 0     0 0   my $tokens= shift;
539 0           my $strategies= shift;
540 0           my $piped= 0;
541 0           my $foreground = 1;
542 0           return [ T_EXECUTE, $foreground, _subparse_complex_command($tokens,$strategies,\$piped,\$foreground,{})];
543             }
544              
545             sub _subparse_complex_command {
546 0     0     my ($tokens,$use_strats,$piped,$foreground,$alias_disabled)=@_;
547 0           my @simplecommands= parse_simple_command($tokens,$use_strats, $piped,$alias_disabled,$foreground);
548              
549 0   0       while (@$tokens > 0 && $tokens->[0][0] == T_PIPE) {
550 0           shift @$tokens;
551 0           $$piped= 1;
552 0           push @simplecommands, parse_simple_command($tokens,$use_strats,$piped,$alias_disabled,$foreground);
553             }
554              
555 0 0 0       if (@$tokens > 0 && $tokens->[0][0] == T_BACKGROUND) {
556 0           shift @$tokens;
557 0           $$foreground = 0;
558             }
559 0           return @simplecommands;
560             }
561              
562             sub parse_simple_command {
563 0     0 0   my ($tokens,$use_strats,$piped,$alias_disabled,$foreground)=@_;
564 0           my (@words,@options,@savetokens,@precom);
565 0           my $opt={};
566              
567 0           my $firstwords=1;
568 0   0       while (@$tokens > 0 and
      0        
569             ($tokens->[0][0] == T_WORD or
570             $tokens->[0][0] == T_REDIRECT)) {
571 0           my $token = shift @$tokens;
572 0 0         if ($token->[0] == T_WORD) {
    0          
573 0 0 0       if ($firstwords and
      0        
574             ($token->[1] eq 'noglob' or
575             $token->[1] eq 'noexpand' or
576             $token->[1] eq 'noalias')) {
577 0           push @precom, $token;
578 0           $opt->{$token->[1]}=1;
579             } else {
580 0           $firstwords=0;
581 0           push @savetokens,$token;
582 0           push @words, $token->[1];
583             }
584             } elsif ($token->[0] == T_REDIRECT) {
585 0           push @options, $token;
586             } else {
587             }
588             }
589              
590 0 0 0       if (%Psh::Support::Alias::aliases and
    0 0        
      0        
591             !$opt->{noalias} and
592             $Psh::Support::Alias::aliases{$words[0]} and
593             !$alias_disabled->{$words[0]}) {
594 0           my $alias= $Psh::Support::Alias::aliases{$words[0]};
595 0           $alias =~ s/\'/\\\'/g;
596 0           $alias_disabled->{$words[0]}=1;
597 0           my @tmp= make_tokens($alias);
598 0           unshift @tmp, @precom;
599 0           shift @savetokens;
600 0           push @tmp, @savetokens;
601 0           push @tmp, @options;
602 0           return _subparse_complex_command(\@tmp,$use_strats,$piped,$foreground,$alias_disabled);
603             } elsif (substr($words[0],0,1) eq "\\") {
604 0           $words[0]=substr($words[0],1);
605             }
606              
607 0           my $line= join ' ', @words;
608 0           local $Psh::current_options= $opt;
609 0           foreach my $strat (@$use_strats) {
610 0           my $how= eval {
611 0           $strat->applies(\$line,\@words,$$piped);
612             };
613 0 0         if ($@) {
    0          
614 0           print STDERR $@;
615             }
616             elsif ($how) {
617 0           my $name= $strat->name;
618 0           Psh::Util::print_debug_class('s',
619             "[Using strategy $name: $how]\n");
620 0           return [ $strat, $how, \@options, \@words, $line, $opt];
621             }
622             }
623 0           Psh::Util::print_error_i18n('clueless',$line,$Psh::bin);
624 0           die '';
625             }
626              
627             # TODO: right now this is pretty much of a hack. Could it be improved?
628             # For example, 'print hello \n' on the command line gets double
629             # quotes around hello and \n, so that it ends up doing
630             # print("hello","\n") which looks nice but is a surprise to
631             # bash users. Perhaps backslash escapes simply shouldn't be OK?
632              
633             sub needs_double_quotes
634             {
635 0     0 0   my ($word) = @_;
636              
637 0 0 0       return if !defined($word) or !$word;
638              
639 0 0 0       if ($word =~ m/[a-zA-Z]/ # if it has some letters
640             and $word =~ m!^(\\.|[$.:a-zA-Z0-9/.])*$!) { # and only these characters
641 0           return 1; # then double-quote it
642             }
643              
644 0           return 0;
645             }
646              
647              
648              
649              
650             1;
651             __END__