| #!/usr/bin/perl |
| |
| # This script is essentially copied from /usr/share/lintian/checks/scripts, |
| # which is: |
| # Copyright (C) 1998 Richard Braakman |
| # Copyright (C) 2002 Josip Rodin |
| # This version is |
| # Copyright (C) 2003 Julian Gilbey |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <https://www.gnu.org/licenses/>. |
| |
| use strict; |
| use warnings; |
| use Getopt::Long qw(:config bundling permute no_getopt_compat); |
| use File::Temp qw/tempfile/; |
| |
| sub init_hashes; |
| |
| (my $progname = $0) =~ s|.*/||; |
| |
| my $usage = <<"EOF"; |
| Usage: $progname [-n] [-f] [-x] [-e] script ... |
| or: $progname --help |
| or: $progname --version |
| This script performs basic checks for the presence of bashisms |
| in /bin/sh scripts and the lack of bashisms in /bin/bash ones. |
| EOF |
| |
| my $version = <<"EOF"; |
| This is $progname, from the Debian devscripts package, version 2.20.5 |
| This code is copyright 2003 by Julian Gilbey <jdg\@debian.org>, |
| based on original code which is copyright 1998 by Richard Braakman |
| and copyright 2002 by Josip Rodin. |
| This program comes with ABSOLUTELY NO WARRANTY. |
| You are free to redistribute this code under the terms of the |
| GNU General Public License, version 2, or (at your option) any later version. |
| EOF |
| |
| my ($opt_echo, $opt_force, $opt_extra, $opt_posix, $opt_early_fail); |
| my ($opt_help, $opt_version); |
| my @filenames; |
| |
| # Detect if STDIN is a pipe |
| if (scalar(@ARGV) == 0 && (-p STDIN or -f STDIN)) { |
| push(@ARGV, '-'); |
| } |
| |
| ## |
| ## handle command-line options |
| ## |
| $opt_help = 1 if int(@ARGV) == 0; |
| |
| GetOptions( |
| "help|h" => \$opt_help, |
| "version|v" => \$opt_version, |
| "newline|n" => \$opt_echo, |
| "force|f" => \$opt_force, |
| "extra|x" => \$opt_extra, |
| "posix|p" => \$opt_posix, |
| "early-fail|e" => \$opt_early_fail, |
| ) |
| or die |
| "Usage: $progname [options] filelist\nRun $progname --help for more details\n"; |
| |
| if ($opt_help) { print $usage; exit 0; } |
| if ($opt_version) { print $version; exit 0; } |
| |
| $opt_echo = 1 if $opt_posix; |
| |
| my $mode = 0; |
| my $issues = 0; |
| my $status = 0; |
| my $makefile = 0; |
| my (%bashisms, %string_bashisms, %singlequote_bashisms); |
| |
| my $LEADIN |
| = qr'(?:(?:^|[`&;(|{])\s*|(?:(?:if|elif|while)(?:\s+!)?|then|do|shell)\s+)'; |
| init_hashes; |
| |
| my @bashisms_keys = sort keys %bashisms; |
| my @string_bashisms_keys = sort keys %string_bashisms; |
| my @singlequote_bashisms_keys = sort keys %singlequote_bashisms; |
| |
| foreach my $filename (@ARGV) { |
| my $check_lines_count = -1; |
| |
| my $display_filename = $filename; |
| |
| if ($filename eq '-') { |
| my $tmp_fh; |
| ($tmp_fh, $filename) |
| = tempfile("chkbashisms_tmp.XXXX", TMPDIR => 1, UNLINK => 1); |
| while (my $line = <STDIN>) { |
| print $tmp_fh $line; |
| } |
| close($tmp_fh); |
| $display_filename = "(stdin)"; |
| } |
| |
| if (!$opt_force) { |
| $check_lines_count = script_is_evil_and_wrong($filename); |
| } |
| |
| if ($check_lines_count == 0 or $check_lines_count == 1) { |
| warn |
| "script $display_filename does not appear to be a /bin/sh script; skipping\n"; |
| next; |
| } |
| |
| if ($check_lines_count != -1) { |
| warn |
| "script $display_filename appears to be a shell wrapper; only checking the first " |
| . "$check_lines_count lines\n"; |
| } |
| |
| unless (open C, '<', $filename) { |
| warn "cannot open script $display_filename for reading: $!\n"; |
| $status |= 2; |
| next; |
| } |
| |
| $issues = 0; |
| $mode = 0; |
| my $cat_string = ""; |
| my $cat_indented = 0; |
| my $quote_string = ""; |
| my $last_continued = 0; |
| my $continued = 0; |
| my $found_rules = 0; |
| my $buffered_orig_line = ""; |
| my $buffered_line = ""; |
| my %start_lines; |
| |
| while (<C>) { |
| next unless ($check_lines_count == -1 or $. <= $check_lines_count); |
| |
| if ($. == 1) { # This should be an interpreter line |
| if (m,^\#!\s*(?:\S+/env\s+)?(\S+),) { |
| my $interpreter = $1; |
| |
| if ($interpreter =~ m,(?:^|/)make$,) { |
| init_hashes if !$makefile++; |
| $makefile = 1; |
| } else { |
| init_hashes if $makefile--; |
| $makefile = 0; |
| } |
| next if $opt_force; |
| |
| if ($interpreter =~ m,(?:^|/)bash$,) { |
| $mode = 1; |
| } elsif ($interpreter !~ m,(?:^|/)(sh|dash|posh)$,) { |
| ### ksh/zsh? |
| warn |
| "script $display_filename does not appear to be a /bin/sh script; skipping\n"; |
| $status |= 2; |
| last; |
| } |
| } else { |
| warn |
| "script $display_filename does not appear to have a \#! interpreter line;\nyou may get strange results\n"; |
| } |
| } |
| |
| chomp; |
| my $orig_line = $_; |
| |
| # We want to remove end-of-line comments, so need to skip |
| # comments that appear inside balanced pairs |
| # of single or double quotes |
| |
| # Remove comments in the "quoted" part of a line that starts |
| # in a quoted block? The problem is that we have no idea |
| # whether the program interpreting the block treats the |
| # quote character as part of the comment or as a quote |
| # terminator. We err on the side of caution and assume it |
| # will be treated as part of the comment. |
| # s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne ""; |
| |
| # skip comment lines |
| if ( m,^\s*\#, |
| && $quote_string eq '' |
| && $buffered_line eq '' |
| && $cat_string eq '') { |
| next; |
| } |
| |
| # Remove quoted strings so we can more easily ignore comments |
| # inside them |
| s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g; |
| s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g; |
| |
| # If inside a quoted string, remove everything before the quote |
| s/^.+?\'// |
| if ($quote_string eq "'"); |
| s/^.+?[^\\]\"// |
| if ($quote_string eq '"'); |
| |
| # If the remaining string contains what looks like a comment, |
| # eat it. In either case, swap the unmodified script line |
| # back in for processing. |
| if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) { |
| $_ = $orig_line; |
| s/\Q$1\E//; # eat comments |
| } else { |
| $_ = $orig_line; |
| } |
| |
| # Handle line continuation |
| if (!$makefile && $cat_string eq '' && m/\\$/) { |
| chop; |
| $buffered_line .= $_; |
| $buffered_orig_line .= $orig_line . "\n"; |
| next; |
| } |
| |
| if ($buffered_line ne '') { |
| $_ = $buffered_line . $_; |
| $orig_line = $buffered_orig_line . $orig_line; |
| $buffered_line = ''; |
| $buffered_orig_line = ''; |
| } |
| |
| if ($makefile) { |
| $last_continued = $continued; |
| if (/[^\\]\\$/) { |
| $continued = 1; |
| } else { |
| $continued = 0; |
| } |
| |
| # Don't match lines that look like a rule if we're in a |
| # continuation line before the start of the rules |
| if (/^[\w%-]+:+\s.*?;?(.*)$/ |
| and !($last_continued and !$found_rules)) { |
| $found_rules = 1; |
| $_ = $1 if $1; |
| } |
| |
| last |
| if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%; |
| |
| # Remove "simple" target names |
| s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//; |
| s/^\t//; |
| s/(?<!\$)\$\((\w+)\)/\${$1}/g; |
| s/(\$){2}/$1/g; |
| s/^[\s\t]*[@-]{1,2}//; |
| } |
| |
| if ( |
| $cat_string ne "" |
| && (m/^\Q$cat_string\E$/ |
| || ($cat_indented && m/^\t*\Q$cat_string\E$/)) |
| ) { |
| $cat_string = ""; |
| next; |
| } |
| my $within_another_shell = 0; |
| if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) { |
| $within_another_shell = 1; |
| } |
| # if cat_string is set, we are in a HERE document and need not |
| # check for things |
| if ($cat_string eq "" and !$within_another_shell) { |
| my $found = 0; |
| my $match = ''; |
| my $explanation = ''; |
| my $line = $_; |
| |
| # Remove "" / '' as they clearly aren't quoted strings |
| # and not considering them makes the matching easier |
| $line =~ s/(^|[^\\])(\'\')+/$1/g; |
| $line =~ s/(^|[^\\])(\"\")+/$1/g; |
| |
| if ($quote_string ne "") { |
| my $otherquote = ($quote_string eq "\"" ? "\'" : "\""); |
| # Inside a quoted block |
| if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) { |
| my $rest = $1; |
| my $templine = $line; |
| |
| # Remove quoted strings delimited with $otherquote |
| $templine |
| =~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g; |
| # Remove quotes that are themselves quoted |
| # "a'b" |
| $templine |
| =~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g; |
| # "\"" |
| $templine |
| =~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g; |
| |
| # After all that, were there still any quotes left? |
| my $count = () = $templine =~ /(^|[^\\])$quote_string/g; |
| next if $count == 0; |
| |
| $count = () = $rest =~ /(^|[^\\])$quote_string/g; |
| if ($count % 2 == 0) { |
| # Quoted block ends on this line |
| # Ignore everything before the closing quote |
| $line = $rest || ''; |
| $quote_string = ""; |
| } else { |
| next; |
| } |
| } else { |
| # Still inside the quoted block, skip this line |
| next; |
| } |
| } |
| |
| # Check even if we removed the end of a quoted block |
| # in the previous check, as a single line can end one |
| # block and begin another |
| if ($quote_string eq "") { |
| # Possible start of a quoted block |
| for my $quote ("\"", "\'") { |
| my $templine = $line; |
| my $otherquote = ($quote eq "\"" ? "\'" : "\""); |
| |
| # Remove balanced quotes and their content |
| while (1) { |
| my ($length_single, $length_double) = (0, 0); |
| |
| # Determine which one would match first: |
| if ($templine |
| =~ m/(^.+?(?:^|[^\\\"](?:\\\\)*)\')[^\']*\'/) { |
| $length_single = length($1); |
| } |
| if ($templine |
| =~ m/(^.*?(?:^|[^\\\'](?:\\\\)*)\")(?:\\.|[^\\\"])+\"/ |
| ) { |
| $length_double = length($1); |
| } |
| |
| # Now simplify accordingly (shorter is preferred): |
| if ( |
| $length_single != 0 |
| && ( $length_single < $length_double |
| || $length_double == 0) |
| ) { |
| $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/; |
| } elsif ($length_double != 0) { |
| $templine |
| =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/; |
| } else { |
| last; |
| } |
| } |
| |
| # Don't flag quotes that are themselves quoted |
| # "a'b" |
| $templine =~ s/$otherquote.*?$quote.*?$otherquote//g; |
| # "\"" |
| $templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g; |
| # \' or \" |
| $templine =~ s/\\[\'\"]//g; |
| my $count = () = $templine =~ /(^|(?!\\))$quote/g; |
| |
| # If there's an odd number of non-escaped |
| # quotes in the line it's almost certainly the |
| # start of a quoted block. |
| if ($count % 2 == 1) { |
| $quote_string = $quote; |
| $start_lines{'quote_string'} = $.; |
| $line =~ s/^(.*)$quote.*$/$1/; |
| last; |
| } |
| } |
| } |
| |
| # since this test is ugly, I have to do it by itself |
| # detect source (.) trying to pass args to the command it runs |
| # The first expression weeds out '. "foo bar"' |
| if ( not $found |
| and not |
| m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/o |
| and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/o) { |
| if ($2 =~ /^(\&|\||\d?>|<)/) { |
| # everything is ok |
| ; |
| } else { |
| $found = 1; |
| $match = $1; |
| $explanation = "sourced script with arguments"; |
| output_explanation($display_filename, $orig_line, |
| $explanation); |
| } |
| } |
| |
| # Remove "quoted quotes". They're likely to be inside |
| # another pair of quotes; we're not interested in |
| # them for their own sake and removing them makes finding |
| # the limits of the outer pair far easier. |
| $line =~ s/(^|[^\\\'\"])\"\'\"/$1/g; |
| $line =~ s/(^|[^\\\'\"])\'\"\'/$1/g; |
| |
| foreach my $re (@singlequote_bashisms_keys) { |
| my $expl = $singlequote_bashisms{$re}; |
| if ($line =~ m/($re)/) { |
| $found = 1; |
| $match = $1; |
| $explanation = $expl; |
| output_explanation($display_filename, $orig_line, |
| $explanation); |
| } |
| } |
| |
| my $re = '(?<![\$\\\])\$\'[^\']+\''; |
| if ($line =~ m/(.*)($re)/o) { |
| my $count = () = $1 =~ /(^|[^\\])\'/g; |
| if ($count % 2 == 0) { |
| output_explanation($display_filename, $orig_line, |
| q<$'...' should be "$(printf '...')">); |
| } |
| } |
| |
| # $cat_line contains the version of the line we'll check |
| # for heredoc delimiters later. Initially, remove any |
| # spaces between << and the delimiter to make the following |
| # updates to $cat_line easier. However, don't remove the |
| # spaces if the delimiter starts with a -, as that changes |
| # how the delimiter is searched. |
| my $cat_line = $line; |
| $cat_line =~ s/(<\<-?)\s+(?!-)/$1/g; |
| |
| # Ignore anything inside single quotes; it could be an |
| # argument to grep or the like. |
| $line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g; |
| |
| # As above, with the exception that we don't remove the string |
| # if the quote is immediately preceded by a < or a -, so we |
| # can match "foo <<-?'xyz'" as a heredoc later |
| # The check is a little more greedy than we'd like, but the |
| # heredoc test itself will weed out any false positives |
| $cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g; |
| |
| $re = '(?<![\$\\\])\$\"[^\"]+\"'; |
| if ($line =~ m/(.*)($re)/o) { |
| my $count = () = $1 =~ /(^|[^\\])\"/g; |
| if ($count % 2 == 0) { |
| output_explanation($display_filename, $orig_line, |
| q<$"foo" should be eval_gettext "foo">); |
| } |
| } |
| |
| foreach my $re (@string_bashisms_keys) { |
| my $expl = $string_bashisms{$re}; |
| if ($line =~ m/($re)/) { |
| $found = 1; |
| $match = $1; |
| $explanation = $expl; |
| output_explanation($display_filename, $orig_line, |
| $explanation); |
| } |
| } |
| |
| # We've checked for all the things we still want to notice in |
| # double-quoted strings, so now remove those strings as well. |
| $line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g; |
| $cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g; |
| foreach my $re (@bashisms_keys) { |
| my $expl = $bashisms{$re}; |
| if ($line =~ m/($re)/) { |
| $found = 1; |
| $match = $1; |
| $explanation = $expl; |
| output_explanation($display_filename, $orig_line, |
| $explanation); |
| } |
| } |
| # This check requires the value to be compared, which could |
| # be done in the regex itself but requires "use re 'eval'". |
| # So it's better done in its own |
| if ($line =~ m/$LEADIN((?:exit|return)\s+(\d{3,}))/o && $2 > 255) { |
| $explanation = 'exit|return status code greater than 255'; |
| output_explanation($display_filename, $orig_line, |
| $explanation); |
| } |
| |
| # Only look for the beginning of a heredoc here, after we've |
| # stripped out quoted material, to avoid false positives. |
| if ($cat_line |
| =~ m/(?:^|[^<])\<\<(\-?)\s*(?:(?!<|'|")((?:[^\s;>|]+(?:(?<=\\)[\s;>|])?)+)|[\'\"](.*?)[\'\"])/ |
| ) { |
| $cat_indented = ($1 && $1 eq '-') ? 1 : 0; |
| my $quoted = defined($3); |
| $cat_string = $quoted ? $3 : $2; |
| unless ($quoted) { |
| # Now strip backslashes. Keep the position of the |
| # last match in a variable, as s/// resets it back |
| # to undef, but we don't want that. |
| my $pos = 0; |
| pos($cat_string) = $pos; |
| while ($cat_string =~ s/\G(.*?)\\/$1/) { |
| # position += length of match + the character |
| # that followed the backslash: |
| $pos += length($1) + 1; |
| pos($cat_string) = $pos; |
| } |
| } |
| $start_lines{'cat_string'} = $.; |
| } |
| } |
| } |
| |
| warn |
| "error: $display_filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>, opened in line $start_lines{'cat_string'}\n" |
| if ($cat_string ne ''); |
| warn |
| "error: $display_filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>, opened in line $start_lines{'quote_string'}\n" |
| if ($quote_string ne ''); |
| warn "error: $display_filename: EOF reached while on line continuation.\n" |
| if ($buffered_line ne ''); |
| |
| close C; |
| |
| if ($mode && !$issues) { |
| warn "could not find any possible bashisms in bash script $filename\n"; |
| $status |= 4; |
| } |
| } |
| |
| exit $status; |
| |
| sub output_explanation { |
| my ($filename, $line, $explanation) = @_; |
| |
| if ($mode) { |
| # When examining a bash script, just flag that there are indeed |
| # bashisms present |
| $issues = 1; |
| } else { |
| warn "possible bashism in $filename line $. ($explanation):\n$line\n"; |
| if ($opt_early_fail) { |
| exit 1; |
| } |
| $status |= 1; |
| } |
| } |
| |
| # Returns non-zero if the given file is not actually a shell script, |
| # just looks like one. |
| sub script_is_evil_and_wrong { |
| my ($filename) = @_; |
| my $ret = -1; |
| # lintian's version of this function aborts if the file |
| # can't be opened, but we simply return as the next |
| # test in the calling code handles reporting the error |
| # itself |
| open(IN, '<', $filename) or return $ret; |
| my $i = 0; |
| my $var = "0"; |
| my $backgrounded = 0; |
| local $_; |
| while (<IN>) { |
| chomp; |
| next if /^#/o; |
| next if /^$/o; |
| last if (++$i > 55); |
| if ( |
| m~ |
| # the exec should either be "eval"ed or a new statement |
| (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*) |
| |
| # eat anything between the exec and $0 |
| exec\s*.+\s* |
| |
| # optionally quoted executable name (via $0) |
| .?\$$var.?\s* |
| |
| # optional "end of options" indicator |
| (--\s*)? |
| |
| # Match expressions of the form '${1+$@}', '${1:+"$@"', |
| # '"${1+$@', "$@", etc where the quotes (before the dollar |
| # sign(s)) are optional and the second (or only if the $1 |
| # clause is omitted) parameter may be $@ or $*. |
| # |
| # Finally the whole subexpression may be omitted for scripts |
| # which do not pass on their parameters (i.e. after re-execing |
| # they take their parameters (and potentially data) from stdin |
| .?(\$\{1:?\+.?)?(\$(\@|\*))?~x |
| ) { |
| $ret = $. - 1; |
| last; |
| } elsif (/^\s*(\w+)=\$0;/) { |
| $var = $1; |
| } elsif ( |
| m~ |
| # Match scripts which use "foo $0 $@ &\nexec true\n" |
| # Program name |
| \S+\s+ |
| |
| # As above |
| .?\$$var.?\s* |
| (--\s*)? |
| .?(\$\{1:?\+.?)?(\$(\@|\*))?.?\s*\&~x |
| ) { |
| |
| $backgrounded = 1; |
| } elsif ( |
| $backgrounded |
| and m~ |
| # the exec should either be "eval"ed or a new statement |
| (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*) |
| exec\s+true(\s|\Z)~x |
| ) { |
| |
| $ret = $. - 1; |
| last; |
| } elsif (m~\@DPATCH\@~) { |
| $ret = $. - 1; |
| last; |
| } |
| |
| } |
| close IN; |
| return $ret; |
| } |
| |
| sub init_hashes { |
| |
| %bashisms = ( |
| qr'(?:^|\s+)function [^<>\(\)\[\]\{\};|\s]+(\s|\(|\Z)' => |
| q<'function' is useless>, |
| $LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>, |
| qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>, |
| qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>, |
| qr'\s\|\&' => q<pipelining is not POSIX>, |
| qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>, |
| qr'\{\d+\.\.\d+(?:\.\.\d+)?\}' => |
| q<brace expansion, {a..b[..c]}should be $(seq a [c] b)>, |
| qr'(?i)\{[a-z]\.\.[a-z](?:\.\.\d+)?\}' => q<brace expansion>, |
| qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>, |
| $LEADIN |
| . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => |
| q<read with option other than -r>, |
| $LEADIN |
| . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)' => |
| q<read without variable>, |
| $LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' => q<echo -e>, |
| $LEADIN . qr'exec\s+-[acl]' => q<exec -c/-l/-a name>, |
| $LEADIN . qr'let\s' => q<let ...>, |
| qr'(?<![\$\(])\(\(.*\)\)' => q<'((' should be '$(('>, |
| qr'(?:^|\s+)(\[|test)\s+-a' => q<test with unary -a (should be -e)>, |
| qr'\&>' => q<should be \>word 2\>&1>, |
| qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' => |
| q<should be \>word 2\>&1>, |
| qr'\[\[(?!:)' => |
| q<alternative test command ([[ foo ]] should be [ foo ])>, |
| qr'/dev/(tcp|udp)' => q</dev/(tcp|udp)>, |
| $LEADIN . qr'builtin\s' => q<builtin>, |
| $LEADIN . qr'caller\s' => q<caller>, |
| $LEADIN . qr'compgen\s' => q<compgen>, |
| $LEADIN . qr'complete\s' => q<complete>, |
| $LEADIN . qr'declare\s' => q<declare>, |
| $LEADIN . qr'dirs(\s|\Z)' => q<dirs>, |
| $LEADIN . qr'disown\s' => q<disown>, |
| $LEADIN . qr'enable\s' => q<enable>, |
| $LEADIN . qr'mapfile\s' => q<mapfile>, |
| $LEADIN . qr'readarray\s' => q<readarray>, |
| $LEADIN . qr'shopt(\s|\Z)' => q<shopt>, |
| $LEADIN . qr'suspend\s' => q<suspend>, |
| $LEADIN . qr'time\s' => q<time>, |
| $LEADIN . qr'type\s' => q<type>, |
| $LEADIN . qr'typeset\s' => q<typeset>, |
| $LEADIN . qr'ulimit(\s|\Z)' => q<ulimit>, |
| $LEADIN . qr'set\s+-[BHT]+' => q<set -[BHT]>, |
| $LEADIN . qr'alias\s+-p' => q<alias -p>, |
| $LEADIN . qr'unalias\s+-a' => q<unalias -a>, |
| $LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>, |
| # function '=' is special-cased due to bash arrays (think of "foo=()") |
| qr'(?:^|\s)\s*=\s*\(\s*\)\s*([\{|\(]|\Z)' => |
| q<function names should only contain [a-z0-9_]>, |
| qr'(?:^|\s)(?<func>function\s)?\s*(?:[^<>\(\)\[\]\{\};|\s]*[^<>\(\)\[\]\{\};|\s\w][^<>\(\)\[\]\{\};|\s]*)(?(<func>)(?=)|(?<!=))\s*(?(<func>)(?:\(\s*\))?|\(\s*\))\s*([\{|\(]|\Z)' |
| => q<function names should only contain [a-z0-9_]>, |
| $LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>, |
| $LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>, |
| qr'(?:^|\s+)[<>]\(.*?\)' => q<\<() process substitution>, |
| $LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>, |
| $LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>, |
| $LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' => q<sh --long-option>, |
| $LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' => q<sh [-+]O>, |
| qr'\[\^[^]]+\]' => q<[^] should be [!]>, |
| $LEADIN |
| . qr'printf\s+-v' => |
| q<'printf -v var ...' should be var='$(printf ...)'>, |
| $LEADIN . qr'coproc\s' => q<coproc>, |
| qr';;?&' => q<;;& and ;& special case operators>, |
| $LEADIN . qr'jobs\s' => q<jobs>, |
| # $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>, |
| $LEADIN |
| . qr'command\s+(?:-[pvV]+\s+)*-(?:[pvV])*[^pvV\s]' => |
| q<'command' with option other than -p, -v or -V>, |
| $LEADIN |
| . qr'setvar\s' => |
| q<setvar 'foo' 'bar' should be eval 'foo="'"$bar"'"'>, |
| $LEADIN |
| . qr'trap\s+["\']?.*["\']?\s+.*(?:ERR|DEBUG|RETURN)' => |
| q<trap with ERR|DEBUG|RETURN>, |
| $LEADIN |
| . qr'(?:exit|return)\s+-\d' => |
| q<exit|return with negative status code>, |
| $LEADIN |
| . qr'(?:exit|return)\s+--' => |
| q<'exit --' should be 'exit' (idem for return)>, |
| $LEADIN . qr'hash(\s|\Z)' => q<hash>, |
| qr'(?:[:=\s])~(?:[+-]|[+-]?\d+)(?:[/\s]|\Z)' => |
| q<non-standard tilde expansion>, |
| ); |
| |
| %string_bashisms = ( |
| qr'\$\[[^][]+\]' => q<'$[' should be '$(('>, |
| qr'\$\{(?:\w+|@|\*)\:(?:\d+|\$\{?\w+\}?)+(?::(?:\d+|\$\{?\w+\}?)+)?\}' |
| => q<${foo:3[:1]}>, |
| qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>, |
| qr'\$\{!\w+\}' => q<${!name}>, |
| qr'\$\{(?:\w+|@|\*)([,^]{1,2}.*?)\}' => |
| q<${parm,[,][pat]} or ${parm^[^][pat]}>, |
| qr'\$\{[@*]([#%]{1,2}.*?)\}' => q<${[@|*]#[#]pat} or ${[@|*]%[%]pat}>, |
| qr'\$\{#[@*]\}' => q<${#@} or ${#*}>, |
| qr'\$\{(?:\w+|@|\*)(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>, |
| qr'\$\{\#?\w+\[.+\](?:[/,:#%^].+?)?\}' => |
| q<bash arrays, ${name[0|*|@]}>, |
| qr'\$\{?RANDOM\}?\b' => q<$RANDOM>, |
| qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>, |
| qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>, |
| qr'\$\{?DIRSTACK\}?\b' => q<$DIRSTACK>, |
| qr'\$\{?EUID\}?\b' => q<$EUID should be "$(id -u)">, |
| qr'\$\{?UID\}?\b' => q<$UID should be "$(id -ru)">, |
| qr'\$\{?SECONDS\}?\b' => q<$SECONDS>, |
| qr'\$\{?BASH_[A-Z]+\}?\b' => q<$BASH_SOMETHING>, |
| qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>, |
| qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>, |
| qr'\$\{?SHLVL\}?\b' => q<$SHLVL>, |
| qr'\$\{?FUNCNAME\}?\b' => q<$FUNCNAME>, |
| qr'\$\{?TMOUT\}?\b' => q<$TMOUT>, |
| qr'(?:^|\s+)TMOUT=' => q<TMOUT=>, |
| qr'\$\{?TIMEFORMAT\}?\b' => q<$TIMEFORMAT>, |
| qr'(?:^|\s+)TIMEFORMAT=' => q<TIMEFORMAT=>, |
| qr'(?<![$\\])\$\{?_\}?\b' => q<$_>, |
| qr'(?:^|\s+)GLOBIGNORE=' => q<GLOBIGNORE=>, |
| qr'<<<' => q<\<\<\< here string>, |
| $LEADIN |
| . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => |
| q<unsafe echo with backslash>, |
| qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => |
| q<'$((n++))' should be '$n; $((n=n+1))'>, |
| qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)' => |
| q<'$((++n))' should be '$((n=n+1))'>, |
| qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)' => |
| q<'$((n--))' should be '$n; $((n=n-1))'>, |
| qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)' => |
| q<'$((--n))' should be '$((n=n-1))'>, |
| qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)' => q<exponentiation is not POSIX>, |
| $LEADIN . qr'printf\s["\'][^"\']*?%q.+?["\']' => q<printf %q>, |
| ); |
| |
| %singlequote_bashisms = ( |
| $LEADIN |
| . qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' => |
| q<unsafe echo with backslash>, |
| $LEADIN |
| . qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' => |
| q<should be '.', not 'source'>, |
| ); |
| |
| if ($opt_echo) { |
| $bashisms{ $LEADIN . qr'echo\s+-[A-Za-z]*n' } = q<echo -n>; |
| } |
| if ($opt_posix) { |
| $bashisms{ $LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)' } |
| = q<local foo>; |
| $bashisms{ $LEADIN . qr'local\s+\w+=' } = q<local foo=bar>; |
| $bashisms{ $LEADIN . qr'local\s+\w+\s+\w+' } = q<local x y>; |
| $bashisms{ $LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s' } = q<test -a/-o>; |
| $bashisms{ $LEADIN . qr'kill\s+-[^sl]\w*' } = q<kill -[0-9] or -[A-Z]>; |
| $bashisms{ $LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]' } |
| = q<trap with signal numbers>; |
| } |
| |
| if ($makefile) { |
| $string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'} |
| = q<'$(\< foo)' should be '$(cat foo)'>; |
| } else { |
| $bashisms{ $LEADIN . qr'\w+\+=' } = q<should be VAR="${VAR}foo">; |
| $string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'} |
| = q<'$(\< foo)' should be '$(cat foo)'>; |
| } |
| |
| if ($opt_extra) { |
| $string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>; |
| $string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>; |
| $string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>; |
| $string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>; |
| $string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>; |
| $string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>; |
| $string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>; |
| $string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>; |
| $string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>; |
| $string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>; |
| } |
| } |