[LTP] [PATCH 2/4] Vendor checkbashisms.pl version 2.20.5

Petr Vorel pvorel@suse.cz
Thu Sep 2 12:37:38 CEST 2021


>From https://salsa.debian.org/debian/devscripts/raw/master/scripts/checkbashisms.pl
(updated version in the script)

Signed-off-by: Petr Vorel <pvorel@suse.cz>
---
 scripts/checkbashisms.pl | 816 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 816 insertions(+)
 create mode 100755 scripts/checkbashisms.pl

diff --git a/scripts/checkbashisms.pl b/scripts/checkbashisms.pl
new file mode 100755
index 000000000..ba417c993
--- /dev/null
+++ b/scripts/checkbashisms.pl
@@ -0,0 +1,816 @@
+#!/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>;
+    }
+}
-- 
2.33.0



More information about the ltp mailing list