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