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