Author: glen Date: Thu May 13 17:06:13 2010 GMT Module: packages Tag: HEAD ---- Log message: - adopted from altlinux src.rpm
---- Files affected: packages/checkbashisms: checkbashisms.1 (NONE -> 1.1) (NEW), checkbashisms.pl (NONE -> 1.1) (NEW) ---- Diffs: ================================================================ Index: packages/checkbashisms/checkbashisms.1 diff -u /dev/null packages/checkbashisms/checkbashisms.1:1.1 --- /dev/null Thu May 13 19:06:13 2010 +++ packages/checkbashisms/checkbashisms.1 Thu May 13 19:06:07 2010 @@ -0,0 +1,65 @@ +.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|\-\-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 "echo \-n" 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 \-n). +.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 "$BASH_ENV" may be preceeded by checking +whether "$BASH" 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. +.SH "SEE ALSO" +.BR lintian (1). +.SH AUTHOR +\fBcheckbashisms\fR was originally written as a shell script by Yann Dirson +<[email protected]> and rewritten in Perl with many more features by +Julian Gilbey <[email protected]>. ================================================================ Index: packages/checkbashisms/checkbashisms.pl diff -u /dev/null packages/checkbashisms/checkbashisms.pl:1.1 --- /dev/null Thu May 13 19:06:13 2010 +++ packages/checkbashisms/checkbashisms.pl Thu May 13 19:06:07 2010 @@ -0,0 +1,579 @@ +#! /usr/bin/perl -w + +# 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, you can find it on the World Wide +# Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free +# Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, +# MA 02111-1307, USA. + +use strict; +use Getopt::Long; + +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. +EOF + +my $version = <<"EOF"; +This is $progname, from the Debian devscripts package, version ###VERSION### +This code is copyright 2003 by Julian Gilbey <[email protected]>, +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); + +## +## 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 $status = 0; +my $makefile = 0; +my (%bashisms, %string_bashisms, %singlequote_bashisms); + +my $LEADIN = qr'(?:(?:^|[`&;(|{])\s*|(?:if|then|do|while|shell)\s+)'; +init_hashes; + +foreach my $filename (@ARGV) { + my $check_lines_count = -1; + + if (!$opt_force) { + $check_lines_count = script_is_evil_and_wrong($filename); + } + + if ($check_lines_count == 0 or $check_lines_count == 1) { + warn "script $filename does not appear to be a /bin/sh script; skipping\n"; + next; + } + + if ($check_lines_count != -1) { + warn "script $filename appears to be a shell wrapper; only checking the first " + . "$check_lines_count lines\n"; + } + + unless (open C, '<', $filename) { + warn "cannot open script $filename for reading: $!\n"; + $status |= 2; + next; + } + + my $cat_string = ""; + my $quote_string = ""; + my $last_continued = 0; + my $continued = 0; + my $found_rules = 0; + + while (<C>) { + next unless ($check_lines_count == -1 or $. <= $check_lines_count); + + if ($. == 1) { # This should be an interpreter line + if (m,^\#!\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$,) { + warn "script $filename is already a bash script; skipping\n"; + $status |= 2; + last; # end this file + } + elsif ($interpreter !~ m,/(sh|posh)$,) { +### ksh/zsh? + warn "script $filename does not appear to be a /bin/sh script; skipping\n"; + $status |= 2; + last; + } + } else { + warn "script $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 ""; + + next if m,^\s*\#,; # skip comment lines + + # Remove quoted strings so we can more easily ignore comments + # inside them + s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g; + s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g; + + # 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; + } + + 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%^(export )?SHELL\s*:?=\s*(/bin/)?bash\s*%; + + s/^\t//; + s/(\$){2}/$1/; + s/^[\s\t]*@//; + } + + if ($cat_string ne "" and m/^\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 + $templine =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1/g; + $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g; + + # Don't flag quotes that are themselves quoted + # "a'b" + $templine =~ s/$otherquote.*?$quote.*?$otherquote//g; + # "\"" + $templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/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; + $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)/ + and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) { + if ($2 =~ /^(\&|\||\d?>|<)/) { + # everything is ok + ; + } else { + $found = 1; + $match = $1; + $explanation = "sourced script with arguments"; + output_explanation($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; + + while (my ($re,$expl) = each %singlequote_bashisms) { + if ($line =~ m/($re)/) { + $found = 1; + $match = $1; + $explanation = $expl; + output_explanation($filename, $orig_line, $explanation); + } + } + + my $re='(?<![\$\\\])\$\'[^\']+\''; + if ($line =~ m/(.*)($re)/){ + my $count = () = $1 =~ /(^|[^\\])\'/g; + if( $count % 2 == 0 ) { + output_explanation($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. + 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 preceeded 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)/){ + my $count = () = $1 =~ /(^|[^\\])\"/g; + if( $count % 2 == 0 ) { + output_explanation($filename, $orig_line, q<$"foo" should be eval_gettext "foo">); + } + } + + while (my ($re,$expl) = each %string_bashisms) { + if ($line =~ m/($re)/) { + $found = 1; + $match = $1; + $explanation = $expl; + output_explanation($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; + while (my ($re,$expl) = each %bashisms) { + if ($line =~ m/($re)/) { + $found = 1; + $match = $1; + $explanation = $expl; + output_explanation($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*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) { + $cat_string = $1; + $cat_string = $2 if not defined $cat_string; + } + } + } + + close C; +} + +exit $status; + +sub output_explanation { + my ($filename, $line, $explanation) = @_; + + 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+$@', "$@", 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 \w+(\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+\}' => q<brace expansion, should be $(seq a b)>, + 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]+)' => + q<should be \>word 2\>&1>, + $LEADIN . qr'kill\s+-[^sl]\w*' => q<kill -[0-9] or -[A-Z]>, + $LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]' => q<trap with signal numbers>, + 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>, + qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\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 substituion>, + $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>, + ); + <<Diff was trimmed, longer than 597 lines>> _______________________________________________ pld-cvs-commit mailing list [email protected] http://lists.pld-linux.org/mailman/listinfo/pld-cvs-commit
