182
185
s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
183
186
s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
188
# If inside a quoted string, remove everything before the quote
190
if ($quote_string eq "'");
192
if ($quote_string eq '"');
185
194
# If the remaining string contains what looks like a comment,
186
195
# eat it. In either case, swap the unmodified script line
187
196
# back in for processing.
297
306
my $otherquote = ($quote eq "\"" ? "\'" : "\"");
299
308
# Remove balanced quotes and their content
300
$templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
301
$templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
310
my ($length_single, $length_double) = (0, 0);
312
# Determine which one would match first:
313
if ($templine =~ m/(^.+?(?:^|[^\\\"](?:\\\\)*)\')[^\']*\'/) {
314
$length_single = length($1);
316
if ($templine =~ m/(^.*?(?:^|[^\\\'](?:\\\\)*)\")(?:\\.|[^\\\"])+\"/) {
317
$length_double = length($1);
320
# Now simplify accordingly (shorter is preferred):
321
if ($length_single != 0 && ($length_single < $length_double || $length_double == 0)) {
322
$templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/;
323
} elsif ($length_double != 0) {
324
$templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/;
303
330
# Don't flag quotes that are themselves quoted
324
352
# detect source (.) trying to pass args to the command it runs
325
353
# The first expression weeds out '. "foo bar"'
326
354
if (not $found and
327
not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
328
and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
355
not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/o
356
and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/o) {
329
357
if ($2 =~ /^(\&|\||\d?>|<)/) {
330
358
# everything is ok
356
384
my $re='(?<![\$\\\])\$\'[^\']+\'';
357
if ($line =~ m/(.*)($re)/){
385
if ($line =~ m/(.*)($re)/o){
358
386
my $count = () = $1 =~ /(^|[^\\])\'/g;
359
387
if( $count % 2 == 0 ) {
360
388
output_explanation($display_filename, $orig_line, q<$'...' should be "$(printf '...')">);
364
392
# $cat_line contains the version of the line we'll check
365
393
# for heredoc delimiters later. Initially, remove any
366
394
# spaces between << and the delimiter to make the following
367
# updates to $cat_line easier.
395
# updates to $cat_line easier. However, don't remove the
396
# spaces if the delimiter starts with a -, as that changes
397
# how the delimiter is searched.
368
398
my $cat_line = $line;
369
$cat_line =~ s/(<\<-?)\s+/$1/g;
399
$cat_line =~ s/(<\<-?)\s+(?!-)/$1/g;
371
401
# Ignore anything inside single quotes; it could be an
372
402
# argument to grep or the like.
380
410
$cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
382
412
$re='(?<![\$\\\])\$\"[^\"]+\"';
383
if ($line =~ m/(.*)($re)/){
413
if ($line =~ m/(.*)($re)/o){
384
414
my $count = () = $1 =~ /(^|[^\\])\"/g;
385
415
if( $count % 2 == 0 ) {
386
416
output_explanation($display_filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
408
438
output_explanation($display_filename, $orig_line, $explanation);
441
# This check requires the value to be compared, which could
442
# be done in the regex itself but requires "use re 'eval'".
443
# So it's better done in its own
444
if ($line =~ m/$LEADIN((?:exit|return)\s+(\d{3,}))/o && $2 > 255) {
445
$explanation = 'exit|return status code greater than 255';
446
output_explanation($display_filename, $orig_line, $explanation);
412
449
# Only look for the beginning of a heredoc here, after we've
413
450
# stripped out quoted material, to avoid false positives.
414
if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
451
if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:(?!<|'|")((?:[^\s;>|]+(?:(?<=\\)[\s;>|])?)+)|[\'\"](.*?)[\'\"])/) {
415
452
$cat_indented = ($1 && $1 eq '-')? 1 : 0;
417
$cat_string = $3 if not defined $cat_string;
453
my $quoted = defined($3);
454
$cat_string = $quoted? $3 : $2;
456
# Now strip backslashes. Keep the position of the
457
# last match in a variable, as s/// resets it back
458
# to undef, but we don't want that.
460
pos($cat_string) = $pos;
461
while ($cat_string =~ s/\G(.*?)\\/$1/) {
462
# postition += length of match + the character
463
# that followed the backslash:
464
$pos += length($1)+1;
465
pos($cat_string) = $pos;
468
$start_lines{'cat_string'} = $.;
422
warn "error: $filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
473
warn "error: $display_filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>, opened in line $start_lines{'cat_string'}\n"
423
474
if ($cat_string ne '');
424
warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
475
warn "error: $display_filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>, opened in line $start_lines{'quote_string'}\n"
425
476
if ($quote_string ne '');
426
warn "error: $filename: EOF reached while on line continuation.\n"
477
warn "error: $display_filename: EOF reached while on line continuation.\n"
427
478
if ($buffered_line ne '');
482
if ($mode && !$issues) {
483
warn "could not find any possible bashisms in bash script $filename\n";
434
490
sub output_explanation {
435
491
my ($filename, $line, $explanation) = @_;
437
warn "possible bashism in $filename line $. ($explanation):\n$line\n";
494
# When examining a bash script, just flag that there are indeed
498
warn "possible bashism in $filename line $. ($explanation):\n$line\n";
441
503
# Returns non-zero if the given file is not actually a shell script,
514
576
sub init_hashes {
517
qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
579
qr'(?:^|\s+)function [^<>\(\)\[\]\{\};|\s]+(\s|\(|\Z)' => q<'function' is useless>,
518
580
$LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>,
519
581
qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
520
582
qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
521
583
qr'\s\|\&' => q<pipelining is not POSIX>,
522
584
qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
523
qr'\{\d+\.\.\d+\}' => q<brace expansion, should be $(seq a b)>,
585
qr'\{\d+\.\.\d+(?:\.\.\d+)?\}' => q<brace expansion, {a..b[..c]}should be $(seq a [c] b)>,
586
qr'(?i)\{[a-z]\.\.[a-z](?:\.\.\d+)?\}' => q<brace expansion>,
524
587
qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
525
588
$LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
526
589
$LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
555
618
$LEADIN . qr'alias\s+-p' => q<alias -p>,
556
619
$LEADIN . qr'unalias\s+-a' => q<unalias -a>,
557
620
$LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
558
qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
621
# function '=' is special-cased due to bash arrays (think of "foo=()")
622
qr'(?:^|\s)\s*=\s*\(\s*\)\s*([\{|\(]|\Z)'
623
=> q<function names should only contain [a-z0-9_]>,
624
qr'(?:^|\s)(?<func>function\s)?\s*(?:[^<>\(\)\[\]\{\};|\s]*[^<>\(\)\[\]\{\};|\s\w][^<>\(\)\[\]\{\};|\s]*)(?(<func>)(?=)|(?<!=))\s*(?(<func>)(?:\(\s*\))?|\(\s*\))\s*([\{|\(]|\Z)'
559
625
=> q<function names should only contain [a-z0-9_]>,
560
626
$LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
561
627
$LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
571
637
$LEADIN . qr'jobs\s' => q<jobs>,
572
638
# $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>,
573
639
$LEADIN . qr'command\s+-[^p]\s' => q<'command' with option other than -p>,
574
$LEADIN . qr'setvar\s' => q<setvar 'foo' 'bar' should be eval \$'foo' 'bar'>,
640
$LEADIN . qr'setvar\s' => q<setvar 'foo' 'bar' should be eval 'foo="'"$bar"'"'>,
641
$LEADIN . qr'trap\s+["\']?.*["\']?\s+.*(?:ERR|DEBUG|RETURN)' => q<trap with ERR|DEBUG|RETURN>,
642
$LEADIN . qr'(?:exit|return)\s+-\d' => q<exit|return with negative status code>,
643
$LEADIN . qr'(?:exit|return)\s+--' => q<'exit --' should be 'exit' (idem for return)>,
644
$LEADIN . qr'sleep\s+(?:-|\d+(?:[.a-z]|\s+\d))' => q<sleep only takes one integer>,
645
$LEADIN . qr'hash(\s|\Z)' => q<hash>,
646
qr'(?:[:=\s])~(?:[+-]|[+-]?\d+)(?:[/\s]|\Z)' => q<non-standard tilde expansion>,
577
649
%string_bashisms = (
578
650
qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
579
qr'\$\{\w+\:(?:\d+|\$\{?\w+\}?)+(?::(?:\d+|\$\{?\w+\}?)+)?\}' => q<${foo:3[:1]}>,
651
qr'\$\{(?:\w+|@|\*)\:(?:\d+|\$\{?\w+\}?)+(?::(?:\d+|\$\{?\w+\}?)+)?\}' => q<${foo:3[:1]}>,
580
652
qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
581
653
qr'\$\{!\w+\}' => q<${!name}>,
582
qr'\$\{\w+(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
583
qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
654
qr'\$\{(?:\w+|@|\*)([,^]{1,2}.*?)\}' => q<${parm,[,][pat]} or ${parm^[^][pat]}>,
655
qr'\$\{[@*]([#%]{1,2}.*?)\}' => q<${[@|*]#[#]pat} or ${[@|*]%[%]pat}>,
656
qr'\$\{#[@*]\}' => q<${#@} or ${#*}>,
657
qr'\$\{(?:\w+|@|\*)(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
658
qr'\$\{\#?\w+\[.+\](?:[/,:#%^].+?)?\}' => q<bash arrays, ${name[0|*|@]}>,
584
659
qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
585
660
qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
586
661
qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
592
667
qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
593
668
qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
594
669
qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
670
qr'\$\{?FUNCNAME\}?\b' => q<$FUNCNAME>,
671
qr'\$\{?TMOUT\}?\b' => q<$TMOUT>,
672
qr'(?:^|\s+)TMOUT=' => q<TMOUT=>,
673
qr'\$\{?TIMEFORMAT\}?\b' => q<$TIMEFORMAT>,
674
qr'(?:^|\s+)TIMEFORMAT=' => q<TIMEFORMAT=>,
675
qr'\$\{?_\}?\b' => q<$_>,
676
qr'(?:^|\s+)GLOBIGNORE=' => q<GLOBIGNORE=>,
595
677
qr'<<<' => q<\<\<\< here string>,
596
678
$LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
597
679
qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => q<'$((n++))' should be '$n; $((n=n+1))'>,