chiark / gitweb /
devscripts (2.10.69+squeeze4) stable-security; urgency=high
[devscripts.git] / scripts / checkbashisms.pl
1 #! /usr/bin/perl -w
2
3 # This script is essentially copied from /usr/share/lintian/checks/scripts,
4 # which is:
5 #   Copyright (C) 1998 Richard Braakman
6 #   Copyright (C) 2002 Josip Rodin
7 # This version is
8 #   Copyright (C) 2003 Julian Gilbey
9
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
23 use strict;
24 use Getopt::Long;
25
26 sub init_hashes;
27
28 (my $progname = $0) =~ s|.*/||;
29
30 my $usage = <<"EOF";
31 Usage: $progname [-n] [-f] [-x] script ...
32    or: $progname --help
33    or: $progname --version
34 This script performs basic checks for the presence of bashisms
35 in /bin/sh scripts.
36 EOF
37
38 my $version = <<"EOF";
39 This is $progname, from the Debian devscripts package, version ###VERSION###
40 This code is copyright 2003 by Julian Gilbey <jdg\@debian.org>,
41 based on original code which is copyright 1998 by Richard Braakman
42 and copyright 2002 by Josip Rodin.
43 This program comes with ABSOLUTELY NO WARRANTY.
44 You are free to redistribute this code under the terms of the
45 GNU General Public License, version 2, or (at your option) any later version.
46 EOF
47
48 my ($opt_echo, $opt_force, $opt_extra, $opt_posix);
49 my ($opt_help, $opt_version);
50
51 ##
52 ## handle command-line options
53 ##
54 $opt_help = 1 if int(@ARGV) == 0;
55
56 GetOptions("help|h" => \$opt_help,
57            "version|v" => \$opt_version,
58            "newline|n" => \$opt_echo,
59            "force|f" => \$opt_force,
60            "extra|x" => \$opt_extra,
61            "posix|p" => \$opt_posix,
62            )
63     or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
64
65 if ($opt_help) { print $usage; exit 0; }
66 if ($opt_version) { print $version; exit 0; }
67
68 $opt_echo = 1 if $opt_posix;
69
70 my $status = 0;
71 my $makefile = 0;
72 my (%bashisms, %string_bashisms, %singlequote_bashisms);
73
74 my $LEADIN = qr'(?:(?:^|[`&;(|{])\s*|(?:if|then|do|while|shell)\s+)';
75 init_hashes;
76
77 foreach my $filename (@ARGV) {
78     my $check_lines_count = -1;
79
80     if (!$opt_force) {
81         $check_lines_count = script_is_evil_and_wrong($filename);
82     }
83
84     if ($check_lines_count == 0 or $check_lines_count == 1) {
85         warn "script $filename does not appear to be a /bin/sh script; skipping\n";
86         next;
87     }
88
89     if ($check_lines_count != -1) {
90         warn "script $filename appears to be a shell wrapper; only checking the first "
91              . "$check_lines_count lines\n";
92     }
93
94     unless (open C, '<', $filename) {
95         warn "cannot open script $filename for reading: $!\n";
96         $status |= 2;
97         next;
98     }
99
100     my $cat_string = "";
101     my $cat_indented = 0;
102     my $quote_string = "";
103     my $last_continued = 0;
104     my $continued = 0;
105     my $found_rules = 0;
106     my $buffered_orig_line = "";
107     my $buffered_line = "";
108
109     while (<C>) {
110         next unless ($check_lines_count == -1 or $. <= $check_lines_count);
111
112         if ($. == 1) { # This should be an interpreter line
113             if (m,^\#!\s*(\S+),) {
114                 my $interpreter = $1;
115
116                 if ($interpreter =~ m,/make$,) {
117                     init_hashes if !$makefile++;
118                     $makefile = 1;
119                 } else {
120                     init_hashes if $makefile--;
121                     $makefile = 0;
122                 }
123                 next if $opt_force;
124
125                 if ($interpreter =~ m,/bash$,) {
126                     warn "script $filename is already a bash script; skipping\n";
127                     $status |= 2;
128                     last;  # end this file
129                 }
130                 elsif ($interpreter !~ m,/(sh|posh)$,) {
131 ### ksh/zsh?
132                     warn "script $filename does not appear to be a /bin/sh script; skipping\n";
133                     $status |= 2;
134                     last;
135                 }
136             } else {
137                 warn "script $filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
138             }
139         }
140
141         chomp;
142         my $orig_line = $_;
143
144         # We want to remove end-of-line comments, so need to skip
145         # comments that appear inside balanced pairs
146         # of single or double quotes
147
148         # Remove comments in the "quoted" part of a line that starts
149         # in a quoted block? The problem is that we have no idea
150         # whether the program interpreting the block treats the
151         # quote character as part of the comment or as a quote
152         # terminator. We err on the side of caution and assume it
153         # will be treated as part of the comment.
154         # s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne "";
155
156         # skip comment lines
157         if (m,^\s*\#, && $quote_string eq '' && $buffered_line eq '' && $cat_string eq '') {
158             next;
159         }
160
161         # Remove quoted strings so we can more easily ignore comments
162         # inside them
163         s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
164         s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
165
166         # If the remaining string contains what looks like a comment,
167         # eat it. In either case, swap the unmodified script line
168         # back in for processing.
169         if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) {
170             $_ = $orig_line;
171             s/\Q$1\E//;  # eat comments
172         } else {
173             $_ = $orig_line;
174         }
175
176         # Handle line continuation
177         if (!$makefile && $cat_string eq '' && m/\\$/) {
178             chop;
179             $buffered_line .= $_;
180             $buffered_orig_line .= $orig_line . "\n";
181             next;
182         }
183
184         if ($buffered_line ne '') {
185             $_ = $buffered_line . $_;
186             $orig_line = $buffered_orig_line . $orig_line;
187             $buffered_line ='';
188             $buffered_orig_line ='';
189         }
190
191         if ($makefile) {
192             $last_continued = $continued;
193             if (/[^\\]\\$/) {
194                 $continued = 1;
195             } else {
196                 $continued = 0;
197             }
198
199             # Don't match lines that look like a rule if we're in a
200             # continuation line before the start of the rules
201             if (/^[\w%-]+:+\s.*?;?(.*)$/ and !($last_continued and !$found_rules)) {
202                 $found_rules = 1;
203                 $_ = $1 if $1;
204             } 
205
206             last if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
207
208             # Remove "simple" target names
209             s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//;
210             s/^\t//;
211             s/(?<!\$)\$\((\w+)\)/\${$1}/g;
212             s/(\$){2}/$1/g;
213             s/^[\s\t]*[@-]{1,2}//;
214         }
215
216         if ($cat_string ne "" && (m/^\Q$cat_string\E$/ || ($cat_indented && m/^\t*\Q$cat_string\E$/))) {
217             $cat_string = "";
218             next;
219         }
220         my $within_another_shell = 0;
221         if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) {
222             $within_another_shell = 1;
223         }
224         # if cat_string is set, we are in a HERE document and need not
225         # check for things
226         if ($cat_string eq "" and !$within_another_shell) {
227             my $found = 0;
228             my $match = '';
229             my $explanation = '';
230             my $line = $_;
231
232             # Remove "" / '' as they clearly aren't quoted strings
233             # and not considering them makes the matching easier
234             $line =~ s/(^|[^\\])(\'\')+/$1/g;
235             $line =~ s/(^|[^\\])(\"\")+/$1/g;
236
237             if ($quote_string ne "") {
238                 my $otherquote = ($quote_string eq "\"" ? "\'" : "\"");
239                 # Inside a quoted block
240                 if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) {
241                     my $rest = $1;
242                     my $templine = $line;
243
244                     # Remove quoted strings delimited with $otherquote
245                     $templine =~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g;
246                     # Remove quotes that are themselves quoted
247                     # "a'b"
248                     $templine =~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g;
249                     # "\""
250                     $templine =~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g;
251
252                     # After all that, were there still any quotes left?
253                     my $count = () = $templine =~ /(^|[^\\])$quote_string/g;
254                     next if $count == 0;
255
256                     $count = () = $rest =~ /(^|[^\\])$quote_string/g;
257                     if ($count % 2 == 0) {
258                         # Quoted block ends on this line
259                         # Ignore everything before the closing quote
260                         $line = $rest || '';
261                         $quote_string = "";
262                     } else {
263                         next;
264                     }
265                 } else {
266                     # Still inside the quoted block, skip this line
267                     next;
268                 }
269             }
270
271             # Check even if we removed the end of a quoted block
272             # in the previous check, as a single line can end one
273             # block and begin another
274             if ($quote_string eq "") {
275                 # Possible start of a quoted block
276                 for my $quote ("\"", "\'") {
277                     my $templine = $line;
278                     my $otherquote = ($quote eq "\"" ? "\'" : "\"");
279
280                     # Remove balanced quotes and their content
281                     $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
282                     $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
283
284                     # Don't flag quotes that are themselves quoted
285                     # "a'b"
286                     $templine =~ s/$otherquote.*?$quote.*?$otherquote//g;
287                     # "\""
288                     $templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g;
289                     # \' or \"
290                     $templine =~ s/\\[\'\"]//g;
291                     my $count = () = $templine =~ /(^|(?!\\))$quote/g;
292
293                     # If there's an odd number of non-escaped
294                     # quotes in the line it's almost certainly the
295                     # start of a quoted block.
296                     if ($count % 2 == 1) {
297                         $quote_string = $quote;
298                         $line =~ s/^(.*)$quote.*$/$1/;
299                         last;
300                     }
301                 }
302             }
303
304             # since this test is ugly, I have to do it by itself
305             # detect source (.) trying to pass args to the command it runs
306             # The first expression weeds out '. "foo bar"'
307             if (not $found and
308                 not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
309                 and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
310                 if ($2 =~ /^(\&|\||\d?>|<)/) {
311                     # everything is ok
312                     ;
313                 } else {
314                     $found = 1;
315                     $match = $1;
316                     $explanation = "sourced script with arguments";
317                     output_explanation($filename, $orig_line, $explanation);
318                 }
319             }
320
321             # Remove "quoted quotes". They're likely to be inside
322             # another pair of quotes; we're not interested in
323             # them for their own sake and removing them makes finding
324             # the limits of the outer pair far easier.
325             $line =~ s/(^|[^\\\'\"])\"\'\"/$1/g;
326             $line =~ s/(^|[^\\\'\"])\'\"\'/$1/g;
327
328             while (my ($re,$expl) = each %singlequote_bashisms) {
329                 if ($line =~ m/($re)/) {
330                     $found = 1;
331                     $match = $1;
332                     $explanation = $expl;
333                     output_explanation($filename, $orig_line, $explanation);
334                 }
335             }
336
337             my $re='(?<![\$\\\])\$\'[^\']+\'';
338             if ($line =~ m/(.*)($re)/){
339                 my $count = () = $1 =~ /(^|[^\\])\'/g;
340                 if( $count % 2 == 0 ) {
341                     output_explanation($filename, $orig_line, q<$'...' should be "$(printf '...')">);
342                 }
343             }   
344
345             # $cat_line contains the version of the line we'll check
346             # for heredoc delimiters later. Initially, remove any
347             # spaces between << and the delimiter to make the following
348             # updates to $cat_line easier.
349             my $cat_line = $line;
350             $cat_line =~ s/(<\<-?)\s+/$1/g;
351
352             # Ignore anything inside single quotes; it could be an
353             # argument to grep or the like.
354             $line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
355
356             # As above, with the exception that we don't remove the string
357             # if the quote is immediately preceeded by a < or a -, so we
358             # can match "foo <<-?'xyz'" as a heredoc later
359             # The check is a little more greedy than we'd like, but the
360             # heredoc test itself will weed out any false positives
361             $cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
362
363             $re='(?<![\$\\\])\$\"[^\"]+\"';
364             if ($line =~ m/(.*)($re)/){
365                 my $count = () = $1 =~ /(^|[^\\])\"/g;
366                 if( $count % 2 == 0 ) {
367                     output_explanation($filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
368                 }
369             }   
370
371             while (my ($re,$expl) = each %string_bashisms) {
372                 if ($line =~ m/($re)/) {
373                     $found = 1;
374                     $match = $1;
375                     $explanation = $expl;
376                     output_explanation($filename, $orig_line, $explanation);
377                 }
378             }
379
380             # We've checked for all the things we still want to notice in
381             # double-quoted strings, so now remove those strings as well.
382             $line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
383             $cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
384             while (my ($re,$expl) = each %bashisms) {
385                 if ($line =~ m/($re)/) {
386                     $found = 1;
387                     $match = $1;
388                     $explanation = $expl;
389                     output_explanation($filename, $orig_line, $explanation);
390                 }
391             }
392
393             # Only look for the beginning of a heredoc here, after we've
394             # stripped out quoted material, to avoid false positives.
395             if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
396                 $cat_indented = ($1 && $1 eq '-')? 1 : 0;
397                 $cat_string = $2;
398                 $cat_string = $3 if not defined $cat_string;
399             }
400         }
401     }
402
403     warn "error: $filename:  Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
404         if ($cat_string ne '');
405     warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
406         if ($quote_string ne '');
407     warn "error: $filename: EOF reached while on line continuation.\n"
408         if ($buffered_line ne '');
409
410     close C;
411 }
412
413 exit $status;
414
415 sub output_explanation {
416     my ($filename, $line, $explanation) = @_;
417
418     warn "possible bashism in $filename line $. ($explanation):\n$line\n";
419     $status |= 1;
420 }
421
422 # Returns non-zero if the given file is not actually a shell script,
423 # just looks like one.
424 sub script_is_evil_and_wrong {
425     my ($filename) = @_;
426     my $ret = -1;
427     # lintian's version of this function aborts if the file
428     # can't be opened, but we simply return as the next
429     # test in the calling code handles reporting the error
430     # itself
431     open (IN, '<', $filename) or return $ret;
432     my $i = 0;
433     my $var = "0";
434     my $backgrounded = 0;
435     local $_;
436     while (<IN>) {
437         chomp;
438         next if /^#/o;
439         next if /^$/o;
440         last if (++$i > 55);
441         if (m~
442             # the exec should either be "eval"ed or a new statement
443             (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
444
445             # eat anything between the exec and $0
446             exec\s*.+\s*
447
448             # optionally quoted executable name (via $0)
449             .?\$$var.?\s*
450
451             # optional "end of options" indicator
452             (--\s*)?
453
454             # Match expressions of the form '${1+$@}', '${1:+"$@"',
455             # '"${1+$@', "$@", etc where the quotes (before the dollar
456             # sign(s)) are optional and the second (or only if the $1
457             # clause is omitted) parameter may be $@ or $*. 
458             # 
459             # Finally the whole subexpression may be omitted for scripts
460             # which do not pass on their parameters (i.e. after re-execing
461             # they take their parameters (and potentially data) from stdin
462             .?(\${1:?\+.?)?(\$(\@|\*))?~x) {
463             $ret = $. - 1;
464             last;
465         } elsif (/^\s*(\w+)=\$0;/) {
466             $var = $1;
467         } elsif (m~
468             # Match scripts which use "foo $0 $@ &\nexec true\n"
469             # Program name
470             \S+\s+
471
472             # As above
473             .?\$$var.?\s*
474             (--\s*)?
475             .?(\${1:?\+.?)?(\$(\@|\*))?.?\s*\&~x) {
476
477             $backgrounded = 1;
478         } elsif ($backgrounded and m~
479             # the exec should either be "eval"ed or a new statement
480             (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
481             exec\s+true(\s|\Z)~x) {
482
483             $ret = $. - 1;
484             last;
485         } elsif (m~\@DPATCH\@~) {
486             $ret = $. - 1;
487             last;
488         }
489
490     }
491     close IN;
492     return $ret;
493 }
494
495 sub init_hashes {
496
497     %bashisms = (
498         qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
499         $LEADIN . qr'select\s+\w+' =>     q<'select' is not POSIX>,
500         qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
501         qr'\[\s+[^\]]+\s+==\s' =>        q<should be 'b = a'>,
502         qr'\s\|\&' =>                    q<pipelining is not POSIX>,
503         qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
504         qr'\{\d+\.\.\d+\}' =>          q<brace expansion, should be $(seq a b)>,
505         qr'(?:^|\s+)\w+\[\d+\]=' =>      q<bash arrays, H[0]>,
506         $LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
507         $LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
508             => q<read without variable>,
509         $LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' =>      q<echo -e>,
510         $LEADIN . qr'exec\s+-[acl]' =>    q<exec -c/-l/-a name>,
511         $LEADIN . qr'let\s' =>            q<let ...>,
512         qr'(?<![\$\(])\(\(.*\)\)' =>     q<'((' should be '$(('>,
513         qr'(?:^|\s+)(\[|test)\s+-a' =>            q<test with unary -a (should be -e)>,
514         qr'\&>' =>                     q<should be \>word 2\>&1>,
515         qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' =>
516                                        q<should be \>word 2\>&1>,
517         qr'\[\[(?!:)' => q<alternative test command ([[ foo ]] should be [ foo ])>,
518         qr'/dev/(tcp|udp)'          => q</dev/(tcp|udp)>,
519         $LEADIN . qr'builtin\s' =>        q<builtin>,
520         $LEADIN . qr'caller\s' =>         q<caller>,
521         $LEADIN . qr'compgen\s' =>        q<compgen>,
522         $LEADIN . qr'complete\s' =>       q<complete>,
523         $LEADIN . qr'declare\s' =>        q<declare>,
524         $LEADIN . qr'dirs(\s|\Z)' =>      q<dirs>,
525         $LEADIN . qr'disown\s' =>         q<disown>,
526         $LEADIN . qr'enable\s' =>         q<enable>,
527         $LEADIN . qr'mapfile\s' =>        q<mapfile>,
528         $LEADIN . qr'readarray\s' =>      q<readarray>,
529         $LEADIN . qr'shopt(\s|\Z)' =>     q<shopt>,
530         $LEADIN . qr'suspend\s' =>        q<suspend>,
531         $LEADIN . qr'time\s' =>           q<time>,
532         $LEADIN . qr'type\s' =>           q<type>,
533         $LEADIN . qr'typeset\s' =>        q<typeset>,
534         $LEADIN . qr'ulimit(\s|\Z)' =>    q<ulimit>,
535         $LEADIN . qr'set\s+-[BHT]+' =>    q<set -[BHT]>,
536         $LEADIN . qr'alias\s+-p' =>       q<alias -p>,
537         $LEADIN . qr'unalias\s+-a' =>     q<unalias -a>,
538         $LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
539         qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
540                 => q<function names should only contain [a-z0-9_]>,
541         $LEADIN . qr'(push|pop)d(\s|\Z)' =>    q<(push|pop)d>,
542         $LEADIN . qr'export\s+-[^p]' =>  q<export only takes -p as an option>,
543         qr'(?:^|\s+)[<>]\(.*?\)'            => q<\<() process substituion>,
544         $LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>,
545         $LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>,
546         $LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' =>  q<sh --long-option>,
547         $LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' =>  q<sh [-+]O>,
548         qr'\[\^[^]]+\]' =>  q<[^] should be [!]>,
549         $LEADIN . qr'printf\s+-v' => q<'printf -v var ...' should be var='$(printf ...)'>,
550         $LEADIN . qr'coproc\s' =>        q<coproc>,
551         qr';;?&' =>  q<;;& and ;& special case operators>,
552         $LEADIN . qr'jobs\s' =>  q<jobs>,
553 #       $LEADIN . qr'jobs\s+-[^lp]\s' =>  q<'jobs' with option other than -l or -p>,
554         $LEADIN . qr'command\s+-[^p]\s' =>  q<'command' with option other than -p>,
555     );
556
557     %string_bashisms = (
558         qr'\$\[[^][]+\]' =>              q<'$[' should be '$(('>,
559         qr'\$\{\w+\:\d+(?::\d+)?\}' =>   q<${foo:3[:1]}>,
560         qr'\$\{!\w+[\@*]\}' =>           q<${!prefix[*|@]>,
561         qr'\$\{!\w+\}' =>                q<${!name}>,
562         qr'\$\{\w+(/.+?){1,2}\}' =>      q<${parm/?/pat[/str]}>,
563         qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
564         qr'\$\{?RANDOM\}?\b' =>          q<$RANDOM>,
565         qr'\$\{?(OS|MACH)TYPE\}?\b'   => q<$(OS|MACH)TYPE>,
566         qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
567         qr'\$\{?DIRSTACK\}?\b'        => q<$DIRSTACK>,
568         qr'\$\{?EUID\}?\b'            => q<$EUID should be "$(id -u)">,
569         qr'\$\{?UID\}?\b'              => q<$UID should be "$(id -ru)">,
570         qr'\$\{?SECONDS\}?\b'       => q<$SECONDS>,
571         qr'\$\{?BASH_[A-Z]+\}?\b'     => q<$BASH_SOMETHING>,
572         qr'\$\{?SHELLOPTS\}?\b'       => q<$SHELLOPTS>,
573         qr'\$\{?PIPESTATUS\}?\b'      => q<$PIPESTATUS>,
574         qr'\$\{?SHLVL\}?\b'           => q<$SHLVL>,
575         qr'<<<'                       => q<\<\<\< here string>,
576         $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
577         qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)'   => q<'$((n++))' should be '$n; $((n=n+1))'>,
578         qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)'   => q<'$((++n))' should be '$((n=n+1))'>,
579         qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)'   => q<'$((n--))' should be '$n; $((n=n-1))'>,
580         qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)'   => q<'$((--n))' should be '$((n=n-1))'>,
581         qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)'   => q<exponentiation is not POSIX>,
582         $LEADIN . qr'printf\s["\'][^"\']+?%[qb].+?["\']' => q<printf %q|%b>,
583     );
584
585     %singlequote_bashisms = (
586         $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' => q<unsafe echo with backslash>,
587         $LEADIN . qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' =>
588                                        q<should be '.', not 'source'>,
589     );
590
591     if ($opt_echo) {
592         $bashisms{$LEADIN . qr'echo\s+-[A-Za-z]*n'} = q<echo -n>;
593     }
594     if ($opt_posix) {
595         $bashisms{$LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)'} = q<local foo>;
596         $bashisms{$LEADIN . qr'local\s+\w+='} = q<local foo=bar>;
597         $bashisms{$LEADIN . qr'local\s+\w+\s+\w+'} = q<local x y>;
598         $bashisms{$LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s'} = q<test -a/-o>;
599         $bashisms{$LEADIN . qr'kill\s+-[^sl]\w*'} = q<kill -[0-9] or -[A-Z]>;
600         $bashisms{$LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]'} = q<trap with signal numbers>;
601     }
602
603     if ($makefile) {
604         $string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'} =
605             q<'$(\< foo)' should be '$(cat foo)'>;
606     } else {
607         $bashisms{$LEADIN . qr'\w+\+='} = q<should be VAR="${VAR}foo">;
608         $string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'} = q<'$(\< foo)' should be '$(cat foo)'>;
609     }
610
611     if ($opt_extra) {
612         $string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>;
613         $string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>;
614         $string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>;
615         $string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>;
616         $string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>;
617         $string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>;
618         $string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>;
619         $string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>;
620         $string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>;
621         $string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>;
622     }
623 }