summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorng0 <ng0@n0.is>2019-09-16 06:59:58 +0000
committerng0 <ng0@n0.is>2019-09-16 06:59:58 +0000
commit7f5ae7f617f9db0ac07ea52c562e671afa6f5e14 (patch)
tree577dd4323e3dd1a09f79ea68d2694b76f40ed47f
parent7bfd145e5dd3ca1e7da3a1ed8f201a87f482c6ad (diff)
lint: Remove checkbashisms.pl, assume it is in PATH.
-rw-r--r--lint/Makefile.am22
-rw-r--r--lint/checkbashisms.168
-rwxr-xr-xlint/checkbashisms.pl.in814
3 files changed, 2 insertions, 902 deletions
diff --git a/lint/Makefile.am b/lint/Makefile.am
index f78cdd5f6..4ce615c47 100644
--- a/lint/Makefile.am
+++ b/lint/Makefile.am
@@ -1,25 +1,10 @@
all: check-linters
-do_subst = $(SED) -e 's,[@]PERL[@],$(PERL),g'
-
-SUFFIXES = pl.in .pl
-
-checkbashisms.pl: checkbashisms.pl.in Makefile
- $(do_subst) < $(srcdir)/checkbashisms.pl.in > checkbashisms.pl
- chmod +x checkbashisms.pl
-
-CLEANFILES= \
- checkbashisms.pl
-
-noinst_SCRIPTS = \
- $(CLEANFILES)
-
# Check for bashisms in shell scripts
# Very verbose, need to exclude more files.
check-bashism:
- printf "Run checkbashism on all .sh files.\n"
- printf "Currently this expects checkbashism.pl at a fixed location."
- find '..' -type f ! -path '*/.*' ! -path '*/_*' -name '*.sh' -print0 | xargs -0 $(srcdir)/checkbashisms.pl -f 2>&1 | tee $(srcdir)/bashism.log || true
+ printf "If checkbashisms.pl is in PATH, run checkbashism on all .sh files.\n"
+ find '..' -type f ! -path '*/.*' ! -path '*/_*' -name '*.sh' -print0 | xargs -0 checkbashisms.pl -f 2>&1 | tee $(srcdir)/bashism.log || true
check-python:
printf "Running flake8 and 2to3 if detected.\n"
@@ -57,6 +42,3 @@ check-texinfo:
@cd $(top_srcdir)/doc/tutorial ; find . -type f ! -path '*/.*' -name '*.texi' -print0 | xargs -0 awk '/XXX/ {print FILENAME":"NR":"$$0}' >> $(srcdir)/texinfo_tutorial.log || true
check-linters: check-bashism check-python check-man check-texinfo
-
-EXTRA_DIST = \
- checkbashisms.pl.in
diff --git a/lint/checkbashisms.1 b/lint/checkbashisms.1
deleted file mode 100644
index 6df5f3c78..000000000
--- a/lint/checkbashisms.1
+++ /dev/null
@@ -1,68 +0,0 @@
-.TH CHECKBASHISMS 1 "Debian Utilities" "DEBIAN" \" -*- nroff -*-
-.SH NAME
-checkbashisms \- check for bashisms in /bin/sh scripts
-.SH SYNOPSIS
-\fBcheckbashisms\fR \fIscript\fR ...
-.br
-\fBcheckbashisms \-\-help\fR|\fB\-\-version\fR
-.SH DESCRIPTION
-\fBcheckbashisms\fR, based on one of the checks from the \fBlintian\fR
-system, performs basic checks on \fI/bin/sh\fR shell scripts for the
-possible presence of bashisms. It takes the names of the shell
-scripts on the command line, and outputs warnings if possible bashisms
-are detected.
-.PP
-Note that the definition of a bashism in this context roughly equates
-to "a shell feature that is not required to be supported by POSIX"; this
-means that some issues flagged may be permitted under optional sections
-of POSIX, such as XSI or User Portability.
-.PP
-In cases where POSIX and Debian Policy disagree, \fBcheckbashisms\fR by
-default allows extensions permitted by Policy but may also provide
-options for stricter checking.
-.SH OPTIONS
-.TP
-.BR \-\-help ", " \-h
-Show a summary of options.
-.TP
-.BR \-\-newline ", " \-n
-Check for "\fBecho \-n\fR" usage (non POSIX but required by Debian Policy 10.4.)
-.TP
-.BR \-\-posix ", " \-p
-Check for issues which are non POSIX but required to be supported by Debian
-Policy 10.4 (implies \fB\-n\fR).
-.TP
-.BR \-\-force ", " \-f
-Force each script to be checked, even if it would normally not be (for
-instance, it has a bash or non POSIX shell shebang or appears to be a
-shell wrapper).
-.TP
-.BR \-\-extra ", " \-x
-Highlight lines which, whilst they do not contain bashisms, may be
-useful in determining whether a particular issue is a false positive
-which may be ignored.
-For example, the use of "\fB$BASH_ENV\fR" may be preceded by checking
-whether "\fB$BASH\fR" is set.
-.TP
-.BR \-\-version ", " \-v
-Show version and copyright information.
-.SH "EXIT VALUES"
-The exit value will be 0 if no possible bashisms or other problems
-were detected. Otherwise it will be the sum of the following error
-values:
-.TP
-1
-A possible bashism was detected.
-.TP
-2
-A file was skipped for some reason, for example, because it was
-unreadable or not found. The warning message will give details.
-.TP
-4
-No bashisms were detected in a bash script.
-.SH "SEE ALSO"
-.BR lintian (1)
-.SH AUTHOR
-\fBcheckbashisms\fR was originally written as a shell script by Yann Dirson
-<\fIdirson@debian.org\fR> and rewritten in Perl with many more features by
-Julian Gilbey <\fIjdg@debian.org\fR>.
diff --git a/lint/checkbashisms.pl.in b/lint/checkbashisms.pl.in
deleted file mode 100755
index 0b8b06f86..000000000
--- a/lint/checkbashisms.pl.in
+++ /dev/null
@@ -1,814 +0,0 @@
-#!@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] 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 ###VERSION###
-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);
-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,
- )
- 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";
- $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+-[^p]\s' => q<'command' with option other than -p>,
- $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'sleep\s+(?:-|\d+(?:[.a-z]|\s+\d))' =>
- q<sleep only takes one integer>,
- $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>;
- }
-}