~ubuntu-branches/ubuntu/saucy/devscripts/saucy

« back to all changes in this revision

Viewing changes to scripts/checkbashisms.pl

  • Committer: Package Import Robot
  • Author(s): Benjamin Drung, Christoph Berg, James McCoy, Dmitry Smirnov, Paul Wise, Benjamin Drung, Cyril Brulebois, Tony Mancill, David Prévot, Josselin Mouette, Raphael Geissert, Regid Ichira, Colin Watson
  • Date: 2013-02-18 21:50:11 UTC
  • mfrom: (10.7.8 squeeze)
  • mto: (10.10.1 sid)
  • mto: This revision was merged to the branch mainline in revision 132.
  • Revision ID: package-import@ubuntu.com-20130218215011-efervwilveqwzzzx
Tags: 2.13.0
[ Christoph Berg ]
* origtargz: New script: fetch the orig tarball of a Debian package from
  various sources, and unpack it
* debcommit: --changelog-info will pass --author and --date for git commits.

[ James McCoy ]
* licensecheck: Recognize MPL 2.0 licenses.  Thanks to Ryan Pavlik for the
  patch.  (Closes: #687664)
* namecheck: Check Apache's projects page for names.  (Closes: #686862)
* debcommit:
  + Drop checks for old dpkg versions and always use the necessary Perl
    modules (Dpkg::Changelog::Parse, Dpkg::Vendor::Ubuntu,
    Dpkg::Changelog::Entry::Debian).
  + Add changelog info support for hg and bzr.
* annotate-output:
  + Handle an incomplete line of output.  (Closes: #695609)
  + Don't treat backslashes in the command's output as an escape.  (Closes:
    #695613)
  + Don't swallow leading whitespace.  (Closes: #695612, LP: #1120917)
* dscverify: Use "gpg --status-fd" to determine if a valid signature is
  found and only use the signed content.  (Closes: #695914)
* wrap-and-sort: Fix repeated word in man page.  (Closes: #696363)

[ Dmitry Smirnov ]
* licensecheck:
  + Remove any regular comments pattern. (Closes: #526698)
  + Improve command line parsing.
  + Fix GPL license version detection bug.
  + Fix BSD-3-clause detection.

[ Paul Wise ]
* checkbashisms: When examining a bash script, indicate the lack of use of
  bashisms.
* uscan:
  + Access GitHub directly instead of using githubredir.debian.net in
    example GitHub watch URL.
  + Add an example watch URL that matches the various file extensions used
    by common archive formats.
  + Add an example watch URL for Google Code projects.

[ Benjamin Drung ]
* Add bashism test cases from Raphael Geissert.
* Add autopkgtest support. (LP: #1073330)
* suspicious-source: Add inode/symlink and image/x-xpmi to whitelisted
  mime-types.
* wrap-and-sort:
  + Put special entries (variables and placeholders) at the end of the list.
  + Sort debian/control*.in files too.
* licensecheck: detect (L)GPL licenses more permissively. Thanks to
  Laurent Rineau for the patch. (Closes: #659231)
* Bump Standards-Version to 3.9.4 (no changes needed).

[ Cyril Brulebois ]
* Don't auto reverse diffs when DEBDIFF_AUTO_VER_SORT is set to yes, and
  when the version in both packages is the same. (Closes: #650732)

[ Tony Mancill ]
* debsnap: Escape the package name when used in regex.  (Closes: #696018)

[ David Prévot ]
* Minor manpages convention fix: do not terminate the SEE ALSO lists with a
  period. (Closes: #696416)
* French translation update.

[ Josselin Mouette ]
* nmudiff: Use dpkg-parsechangelog to fix manual parsing bug (Closes: #700584)

[ Raphael Geissert ]
* checkbashisms:
  + allow -FOO- as heredoc delimiter
  + simplify mixed single/double balanced quotes correctly
  + correct description of 'setvar'
  + detect traps for DEBUG, ERRNO, or RETURN
  + check for incorrect args. to 'exit' (Closes: #687450)
  + fix handling of # characters in quoted strings
  + detect use of $FUNCNAME, $TMOUT, and $TIMEFORMAT
  + detect uses of sleep with anything other than an int.
  + detect the use of the hash utility and $_
  + check for other forms of brace expansion
  + check for use of non-standard tilde expansion
  + check for case modification expansions
  + check for the use of $GLOBIGNORE

[ Regid Ichira ]
* rc-alert: Support using curl as alternative to wget. (Closes: #690024)
* wnpp-alert: Support using curl as alternative to wget. (Closes: #690056)
* wnpp-check: Support using curl as alternative to wget. (Closes: #690059)

[ Colin Watson ]
* debian/control: Mark devscripts Multi-Arch: foreign. (Closes: #694760)
* debchange, debcommit: Set the timestamp of temporary editor files back
  one second, to make modification detection more reliable in the absence
  of subsecond granularity. (Closes: #697923)

Show diffs side-by-side

added added

removed removed

Lines of Context:
33
33
   or: $progname --help
34
34
   or: $progname --version
35
35
This script performs basic checks for the presence of bashisms
36
 
in /bin/sh scripts.
 
36
in /bin/sh scripts and the lack of bashisms in /bin/bash ones.
37
37
EOF
38
38
 
39
39
my $version = <<"EOF";
74
74
 
75
75
$opt_echo = 1 if $opt_posix;
76
76
 
 
77
my $mode = 0;
 
78
my $issues = 0;
77
79
my $status = 0;
78
80
my $makefile = 0;
79
81
my (%bashisms, %string_bashisms, %singlequote_bashisms);
116
118
        next;
117
119
    }
118
120
 
 
121
    $issues = 0;
 
122
    $mode = 0;
119
123
    my $cat_string = "";
120
124
    my $cat_indented = 0;
121
125
    my $quote_string = "";
124
128
    my $found_rules = 0;
125
129
    my $buffered_orig_line = "";
126
130
    my $buffered_line = "";
 
131
    my %start_lines;
127
132
 
128
133
    while (<C>) {
129
134
        next unless ($check_lines_count == -1 or $. <= $check_lines_count);
142
147
                next if $opt_force;
143
148
 
144
149
                if ($interpreter =~ m,/bash$,) {
145
 
                    warn "script $display_filename is already a bash script; skipping\n";
146
 
                    $status |= 2;
147
 
                    last;  # end this file
 
150
                    $mode = 1;
148
151
                }
149
152
                elsif ($interpreter !~ m,/(sh|posh)$,) {
150
153
### ksh/zsh?
182
185
        s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
183
186
        s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
184
187
 
 
188
        # If inside a quoted string, remove everything before the quote
 
189
        s/^.+?\'//
 
190
            if ($quote_string eq "'");
 
191
        s/^.+?[^\\]\"//
 
192
            if ($quote_string eq '"');
 
193
 
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 "\"" ? "\'" : "\"");
298
307
 
299
308
                    # Remove balanced quotes and their content
300
 
                    $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
301
 
                    $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
 
309
                    while (1) {
 
310
                        my ($length_single, $length_double) = (0, 0);
 
311
 
 
312
                        # Determine which one would match first:
 
313
                        if ($templine =~ m/(^.+?(?:^|[^\\\"](?:\\\\)*)\')[^\']*\'/) {
 
314
                            $length_single = length($1);
 
315
                        }
 
316
                        if ($templine =~ m/(^.*?(?:^|[^\\\'](?:\\\\)*)\")(?:\\.|[^\\\"])+\"/) {
 
317
                            $length_double = length($1);
 
318
                        }
 
319
 
 
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/;
 
325
                        } else {
 
326
                            last;
 
327
                        }
 
328
                    }
302
329
 
303
330
                    # Don't flag quotes that are themselves quoted
304
331
                    # "a'b"
314
341
                    # start of a quoted block.
315
342
                    if ($count % 2 == 1) {
316
343
                        $quote_string = $quote;
 
344
                        $start_lines{'quote_string'} = $.;
317
345
                        $line =~ s/^(.*)$quote.*$/$1/;
318
346
                        last;
319
347
                    }
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
331
359
                    ;
354
382
            }
355
383
 
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;
370
400
 
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;
381
411
 
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);
409
439
                }
410
440
            }
 
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);
 
447
            }
411
448
 
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;
416
 
                $cat_string = $2;
417
 
                $cat_string = $3 if not defined $cat_string;
 
453
                my $quoted = defined($3);
 
454
                $cat_string = $quoted? $3 : $2;
 
455
                unless ($quoted) {
 
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.
 
459
                    my $pos = 0;
 
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;
 
466
                    }
 
467
                }
 
468
                $start_lines{'cat_string'} = $.;
418
469
            }
419
470
        }
420
471
    }
421
472
 
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 '');
428
479
 
429
480
    close C;
 
481
 
 
482
    if ($mode && !$issues) {
 
483
        warn "could not find any possible bashisms in bash script $filename\n";
 
484
        $status |= 4;
 
485
    }
430
486
}
431
487
 
432
488
exit $status;
434
490
sub output_explanation {
435
491
    my ($filename, $line, $explanation) = @_;
436
492
 
437
 
    warn "possible bashism in $filename line $. ($explanation):\n$line\n";
438
 
    $status |= 1;
 
493
    if ($mode) {
 
494
        # When examining a bash script, just flag that there are indeed
 
495
        # bashisms present
 
496
        $issues = 1;
 
497
    } else {
 
498
        warn "possible bashism in $filename line $. ($explanation):\n$line\n";
 
499
        $status |= 1;
 
500
    }
439
501
}
440
502
 
441
503
# Returns non-zero if the given file is not actually a shell script,
514
576
sub init_hashes {
515
577
 
516
578
    %bashisms = (
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>,
575
647
    );
576
648
 
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))'>,