Merge branch 'es/chainlint'

Revamp chainlint script for our tests.

* es/chainlint:
  chainlint: colorize problem annotations and test delimiters
  t: retire unused chainlint.sed
  t/Makefile: teach `make test` and `make prove` to run chainlint.pl
  test-lib: replace chainlint.sed with chainlint.pl
  test-lib: retire "lint harder" optimization hack
  t/chainlint: add more chainlint.pl self-tests
  chainlint.pl: allow `|| echo` to signal failure upstream of a pipe
  chainlint.pl: complain about loops lacking explicit failure handling
  chainlint.pl: don't flag broken &&-chain if failure indicated explicitly
  chainlint.pl: don't flag broken &&-chain if `$?` handled explicitly
  chainlint.pl: don't require `&` background command to end with `&&`
  t/Makefile: apply chainlint.pl to existing self-tests
  chainlint.pl: don't require `return|exit|continue` to end with `&&`
  chainlint.pl: validate test scripts in parallel
  chainlint.pl: add parser to identify test definitions
  chainlint.pl: add parser to validate tests
  chainlint.pl: add POSIX shell parser
  chainlint.pl: add POSIX shell lexical analyzer
  t: add skeleton chainlint.pl
This commit is contained in:
Junio C Hamano
2022-09-19 14:35:24 -07:00
73 changed files with 1479 additions and 449 deletions

View File

@ -1076,7 +1076,7 @@ if(NOT ${CMAKE_BINARY_DIR}/CMakeCache.txt STREQUAL ${CACHE_PATH})
"string(REPLACE \"\${GIT_BUILD_DIR_REPL}\" \"GIT_BUILD_DIR=\\\"$TEST_DIRECTORY/../${BUILD_DIR_RELATIVE}\\\"\" content \"\${content}\")\n" "string(REPLACE \"\${GIT_BUILD_DIR_REPL}\" \"GIT_BUILD_DIR=\\\"$TEST_DIRECTORY/../${BUILD_DIR_RELATIVE}\\\"\" content \"\${content}\")\n"
"file(WRITE ${CMAKE_SOURCE_DIR}/t/test-lib.sh \${content})") "file(WRITE ${CMAKE_SOURCE_DIR}/t/test-lib.sh \${content})")
#misc copies #misc copies
file(COPY ${CMAKE_SOURCE_DIR}/t/chainlint.sed DESTINATION ${CMAKE_BINARY_DIR}/t/) file(COPY ${CMAKE_SOURCE_DIR}/t/chainlint.pl DESTINATION ${CMAKE_BINARY_DIR}/t/)
file(COPY ${CMAKE_SOURCE_DIR}/po/is.po DESTINATION ${CMAKE_BINARY_DIR}/po/) file(COPY ${CMAKE_SOURCE_DIR}/po/is.po DESTINATION ${CMAKE_BINARY_DIR}/po/)
file(COPY ${CMAKE_SOURCE_DIR}/mergetools/tkdiff DESTINATION ${CMAKE_BINARY_DIR}/mergetools/) file(COPY ${CMAKE_SOURCE_DIR}/mergetools/tkdiff DESTINATION ${CMAKE_BINARY_DIR}/mergetools/)
file(COPY ${CMAKE_SOURCE_DIR}/contrib/completion/git-prompt.sh DESTINATION ${CMAKE_BINARY_DIR}/contrib/completion/) file(COPY ${CMAKE_SOURCE_DIR}/contrib/completion/git-prompt.sh DESTINATION ${CMAKE_BINARY_DIR}/contrib/completion/)

View File

@ -36,14 +36,21 @@ CHAINLINTTMP_SQ = $(subst ','\'',$(CHAINLINTTMP))
T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))
THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh))) THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh)))
TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh
TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh)) TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))
TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))
CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test))) CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))
CHAINLINT = sed -f chainlint.sed CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl
# `test-chainlint` (which is a dependency of `test-lint`, `test` and `prove`)
# checks all tests in all scripts via a single invocation, so tell individual
# scripts not to "chainlint" themselves
CHAINLINTSUPPRESS = GIT_TEST_CHAIN_LINT=0 && export GIT_TEST_CHAIN_LINT &&
all: $(DEFAULT_TEST_TARGET) all: $(DEFAULT_TEST_TARGET)
test: pre-clean check-chainlint $(TEST_LINT) test: pre-clean check-chainlint $(TEST_LINT)
$(MAKE) aggregate-results-and-cleanup $(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup
failed: failed:
@failed=$$(cd '$(TEST_RESULTS_DIRECTORY_SQ)' && \ @failed=$$(cd '$(TEST_RESULTS_DIRECTORY_SQ)' && \
@ -52,7 +59,7 @@ failed:
test -z "$$failed" || $(MAKE) $$failed test -z "$$failed" || $(MAKE) $$failed
prove: pre-clean check-chainlint $(TEST_LINT) prove: pre-clean check-chainlint $(TEST_LINT)
@echo "*** prove ***"; $(PROVE) --exec '$(TEST_SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) @echo "*** prove ***"; $(CHAINLINTSUPPRESS) $(PROVE) --exec '$(TEST_SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS)
$(MAKE) clean-except-prove-cache $(MAKE) clean-except-prove-cache
$(T): $(T):
@ -73,13 +80,35 @@ clean-chainlint:
check-chainlint: check-chainlint:
@mkdir -p '$(CHAINLINTTMP_SQ)' && \ @mkdir -p '$(CHAINLINTTMP_SQ)' && \
sed -e '/^# LINT: /d' $(patsubst %,chainlint/%.test,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/tests && \ for i in $(CHAINLINTTESTS); do \
sed -e '/^[ ]*$$/d' $(patsubst %,chainlint/%.expect,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/expect && \ echo "test_expect_success '$$i' '" && \
$(CHAINLINT) '$(CHAINLINTTMP_SQ)'/tests | grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \ sed -e '/^# LINT: /d' chainlint/$$i.test && \
diff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual echo "'"; \
done >'$(CHAINLINTTMP_SQ)'/tests && \
{ \
echo "# chainlint: $(CHAINLINTTMP_SQ)/tests" && \
for i in $(CHAINLINTTESTS); do \
echo "# chainlint: $$i" && \
sed -e '/^[ ]*$$/d' chainlint/$$i.expect; \
done \
} >'$(CHAINLINTTMP_SQ)'/expect && \
$(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests | \
grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \
if test -f ../GIT-BUILD-OPTIONS; then \
. ../GIT-BUILD-OPTIONS; \
fi && \
if test -x ../git$$X; then \
DIFFW="../git$$X --no-pager diff -w --no-index"; \
else \
DIFFW="diff -w -u"; \
fi && \
$$DIFFW '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual
test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax \ test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax \
test-lint-filenames test-lint-filenames
ifneq ($(GIT_TEST_CHAIN_LINT),0)
test-lint: test-chainlint
endif
test-lint-duplicates: test-lint-duplicates:
@dups=`echo $(T) $(TPERF) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \ @dups=`echo $(T) $(TPERF) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \
@ -102,6 +131,9 @@ test-lint-filenames:
test -z "$$bad" || { \ test -z "$$bad" || { \
echo >&2 "non-portable file name(s): $$bad"; exit 1; } echo >&2 "non-portable file name(s): $$bad"; exit 1; }
test-chainlint:
@$(CHAINLINT) $(T) $(TLIBS) $(TPERF) $(TINTEROP)
aggregate-results-and-cleanup: $(T) aggregate-results-and-cleanup: $(T)
$(MAKE) aggregate-results $(MAKE) aggregate-results
$(MAKE) clean $(MAKE) clean
@ -117,4 +149,5 @@ valgrind:
perf: perf:
$(MAKE) -C perf/ all $(MAKE) -C perf/ all
.PHONY: pre-clean $(T) aggregate-results clean valgrind perf check-chainlint clean-chainlint .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
check-chainlint clean-chainlint test-chainlint

View File

@ -196,11 +196,6 @@ appropriately before running "make". Short options can be bundled, i.e.
this feature by setting the GIT_TEST_CHAIN_LINT environment this feature by setting the GIT_TEST_CHAIN_LINT environment
variable to "1" or "0", respectively. variable to "1" or "0", respectively.
A few test scripts disable some of the more advanced
chain-linting detection in the name of efficiency. You can
override this by setting the GIT_TEST_CHAIN_LINT_HARDER
environment variable to "1".
--stress:: --stress::
Run the test script repeatedly in multiple parallel jobs until Run the test script repeatedly in multiple parallel jobs until
one of them fails. Useful for reproducing rare failures in one of them fails. Useful for reproducing rare failures in

770
t/chainlint.pl Executable file
View File

@ -0,0 +1,770 @@
#!/usr/bin/env perl
#
# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>
#
# This tool scans shell scripts for test definitions and checks those tests for
# problems, such as broken &&-chains, which might hide bugs in the tests
# themselves or in behaviors being exercised by the tests.
#
# Input arguments are pathnames of shell scripts containing test definitions,
# or globs referencing a collection of scripts. For each problem discovered,
# the pathname of the script containing the test is printed along with the test
# name and the test body with a `?!FOO?!` annotation at the location of each
# detected problem, where "FOO" is a tag such as "AMP" which indicates a broken
# &&-chain. Returns zero if no problems are discovered, otherwise non-zero.
use warnings;
use strict;
use Config;
use File::Glob;
use Getopt::Long;
my $jobs = -1;
my $show_stats;
my $emit_all;
# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3
# "Token Recognition" of POSIX chapter 2 "Shell Command Language". Although
# similar to lexical analyzers for other languages, this one differs in a few
# substantial ways due to quirks of the shell command language.
#
# For instance, in many languages, newline is just whitespace like space or
# TAB, but in shell a newline is a command separator, thus a distinct lexical
# token. A newline is significant and returned as a distinct token even at the
# end of a shell comment.
#
# In other languages, `1+2` would typically be scanned as three tokens
# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar
# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.
# In shell, several characters with special meaning lose that meaning when not
# surrounded by whitespace. For instance, the negation operator `!` is special
# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is
# just a plain character in the longer token "foo!uucp". In many other
# languages, `"string"/foo:'string'` might be scanned as five tokens ("string",
# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.
#
# The lexical analyzer for the shell command language is also somewhat unusual
# in that it recursively invokes the parser to handle the body of `$(...)`
# expressions which can contain arbitrary shell code. Such expressions may be
# encountered both inside and outside of double-quoted strings.
#
# The lexical analyzer is responsible for consuming shell here-doc bodies which
# extend from the line following a `<<TAG` operator until a line consisting
# solely of `TAG`. Here-doc consumption begins when a newline is encountered.
# It is legal for multiple here-doc `<<TAG` operators to be present on a single
# line, in which case their bodies must be present one following the next, and
# are consumed in the (left-to-right) order the `<<TAG` operators appear on the
# line. A special complication is that the bodies of all here-docs must be
# consumed when the newline is encountered even if the parse context depth has
# changed. For instance, in `cat <<A && x=$(cat <<B &&\n`, bodies of here-docs
# "A" and "B" must be consumed even though "A" was introduced outside the
# recursive parse context in which "B" was introduced and in which the newline
# is encountered.
package Lexer;
sub new {
my ($class, $parser, $s) = @_;
bless {
parser => $parser,
buff => $s,
heretags => []
} => $class;
}
sub scan_heredoc_tag {
my $self = shift @_;
${$self->{buff}} =~ /\G(-?)/gc;
my $indented = $1;
my $tag = $self->scan_token();
$tag =~ s/['"\\]//g;
push(@{$self->{heretags}}, $indented ? "\t$tag" : "$tag");
return "<<$indented$tag";
}
sub scan_op {
my ($self, $c) = @_;
my $b = $self->{buff};
return $c unless $$b =~ /\G(.)/sgc;
my $cc = $c . $1;
return scan_heredoc_tag($self) if $cc eq '<<';
return $cc if $cc =~ /^(?:&&|\|\||>>|;;|<&|>&|<>|>\|)$/;
pos($$b)--;
return $c;
}
sub scan_sqstring {
my $self = shift @_;
${$self->{buff}} =~ /\G([^']*'|.*\z)/sgc;
return "'" . $1;
}
sub scan_dqstring {
my $self = shift @_;
my $b = $self->{buff};
my $s = '"';
while (1) {
# slurp up non-special characters
$s .= $1 if $$b =~ /\G([^"\$\\]+)/gc;
# handle special characters
last unless $$b =~ /\G(.)/sgc;
my $c = $1;
$s .= '"', last if $c eq '"';
$s .= '$' . $self->scan_dollar(), next if $c eq '$';
if ($c eq '\\') {
$s .= '\\', last unless $$b =~ /\G(.)/sgc;
$c = $1;
next if $c eq "\n"; # line splice
# backslash escapes only $, `, ", \ in dq-string
$s .= '\\' unless $c =~ /^[\$`"\\]$/;
$s .= $c;
next;
}
die("internal error scanning dq-string '$c'\n");
}
return $s;
}
sub scan_balanced {
my ($self, $c1, $c2) = @_;
my $b = $self->{buff};
my $depth = 1;
my $s = $c1;
while ($$b =~ /\G([^\Q$c1$c2\E]*(?:[\Q$c1$c2\E]|\z))/gc) {
$s .= $1;
$depth++, next if $s =~ /\Q$c1\E$/;
$depth--;
last if $depth == 0;
}
return $s;
}
sub scan_subst {
my $self = shift @_;
my @tokens = $self->{parser}->parse(qr/^\)$/);
$self->{parser}->next_token(); # closing ")"
return @tokens;
}
sub scan_dollar {
my $self = shift @_;
my $b = $self->{buff};
return $self->scan_balanced('(', ')') if $$b =~ /\G\((?=\()/gc; # $((...))
return '(' . join(' ', $self->scan_subst()) . ')' if $$b =~ /\G\(/gc; # $(...)
return $self->scan_balanced('{', '}') if $$b =~ /\G\{/gc; # ${...}
return $1 if $$b =~ /\G(\w+)/gc; # $var
return $1 if $$b =~ /\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.
return '';
}
sub swallow_heredocs {
my $self = shift @_;
my $b = $self->{buff};
my $tags = $self->{heretags};
while (my $tag = shift @$tags) {
my $indent = $tag =~ s/^\t// ? '\\s*' : '';
$$b =~ /(?:\G|\n)$indent\Q$tag\E(?:\n|\z)/gc;
}
}
sub scan_token {
my $self = shift @_;
my $b = $self->{buff};
my $token = '';
RESTART:
$$b =~ /\G[ \t]+/gc; # skip whitespace (but not newline)
return "\n" if $$b =~ /\G#[^\n]*(?:\n|\z)/gc; # comment
while (1) {
# slurp up non-special characters
$token .= $1 if $$b =~ /\G([^\\;&|<>(){}'"\$\s]+)/gc;
# handle special characters
last unless $$b =~ /\G(.)/sgc;
my $c = $1;
last if $c =~ /^[ \t]$/; # whitespace ends token
pos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\n]$/;
$token .= $self->scan_sqstring(), next if $c eq "'";
$token .= $self->scan_dqstring(), next if $c eq '"';
$token .= $c . $self->scan_dollar(), next if $c eq '$';
$self->swallow_heredocs(), $token = $c, last if $c eq "\n";
$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;
$token = $c, last if $c =~ /^[(){}]$/;
if ($c eq '\\') {
$token .= '\\', last unless $$b =~ /\G(.)/sgc;
$c = $1;
next if $c eq "\n" && length($token); # line splice
goto RESTART if $c eq "\n"; # line splice
$token .= '\\' . $c;
next;
}
die("internal error scanning character '$c'\n");
}
return length($token) ? $token : undef;
}
# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It
# is a recursive descent parser very roughly modeled after section 2.10 "Shell
# Grammar" of POSIX chapter 2 "Shell Command Language".
package ShellParser;
sub new {
my ($class, $s) = @_;
my $self = bless {
buff => [],
stop => [],
output => []
} => $class;
$self->{lexer} = Lexer->new($self, $s);
return $self;
}
sub next_token {
my $self = shift @_;
return pop(@{$self->{buff}}) if @{$self->{buff}};
return $self->{lexer}->scan_token();
}
sub untoken {
my $self = shift @_;
push(@{$self->{buff}}, @_);
}
sub peek {
my $self = shift @_;
my $token = $self->next_token();
return undef unless defined($token);
$self->untoken($token);
return $token;
}
sub stop_at {
my ($self, $token) = @_;
return 1 unless defined($token);
my $stop = ${$self->{stop}}[-1] if @{$self->{stop}};
return defined($stop) && $token =~ $stop;
}
sub expect {
my ($self, $expect) = @_;
my $token = $self->next_token();
return $token if defined($token) && $token eq $expect;
push(@{$self->{output}}, "?!ERR?! expected '$expect' but found '" . (defined($token) ? $token : "<end-of-input>") . "'\n");
$self->untoken($token) if defined($token);
return ();
}
sub optional_newlines {
my $self = shift @_;
my @tokens;
while (my $token = $self->peek()) {
last unless $token eq "\n";
push(@tokens, $self->next_token());
}
return @tokens;
}
sub parse_group {
my $self = shift @_;
return ($self->parse(qr/^}$/),
$self->expect('}'));
}
sub parse_subshell {
my $self = shift @_;
return ($self->parse(qr/^\)$/),
$self->expect(')'));
}
sub parse_case_pattern {
my $self = shift @_;
my @tokens;
while (defined(my $token = $self->next_token())) {
push(@tokens, $token);
last if $token eq ')';
}
return @tokens;
}
sub parse_case {
my $self = shift @_;
my @tokens;
push(@tokens,
$self->next_token(), # subject
$self->optional_newlines(),
$self->expect('in'),
$self->optional_newlines());
while (1) {
my $token = $self->peek();
last unless defined($token) && $token ne 'esac';
push(@tokens,
$self->parse_case_pattern(),
$self->optional_newlines(),
$self->parse(qr/^(?:;;|esac)$/)); # item body
$token = $self->peek();
last unless defined($token) && $token ne 'esac';
push(@tokens,
$self->expect(';;'),
$self->optional_newlines());
}
push(@tokens, $self->expect('esac'));
return @tokens;
}
sub parse_for {
my $self = shift @_;
my @tokens;
push(@tokens,
$self->next_token(), # variable
$self->optional_newlines());
my $token = $self->peek();
if (defined($token) && $token eq 'in') {
push(@tokens,
$self->expect('in'),
$self->optional_newlines());
}
push(@tokens,
$self->parse(qr/^do$/), # items
$self->expect('do'),
$self->optional_newlines(),
$self->parse_loop_body(),
$self->expect('done'));
return @tokens;
}
sub parse_if {
my $self = shift @_;
my @tokens;
while (1) {
push(@tokens,
$self->parse(qr/^then$/), # if/elif condition
$self->expect('then'),
$self->optional_newlines(),
$self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body
my $token = $self->peek();
last unless defined($token) && $token eq 'elif';
push(@tokens, $self->expect('elif'));
}
my $token = $self->peek();
if (defined($token) && $token eq 'else') {
push(@tokens,
$self->expect('else'),
$self->optional_newlines(),
$self->parse(qr/^fi$/)); # else body
}
push(@tokens, $self->expect('fi'));
return @tokens;
}
sub parse_loop_body {
my $self = shift @_;
return $self->parse(qr/^done$/);
}
sub parse_loop {
my $self = shift @_;
return ($self->parse(qr/^do$/), # condition
$self->expect('do'),
$self->optional_newlines(),
$self->parse_loop_body(),
$self->expect('done'));
}
sub parse_func {
my $self = shift @_;
return ($self->expect('('),
$self->expect(')'),
$self->optional_newlines(),
$self->parse_cmd()); # body
}
sub parse_bash_array_assignment {
my $self = shift @_;
my @tokens = $self->expect('(');
while (defined(my $token = $self->next_token())) {
push(@tokens, $token);
last if $token eq ')';
}
return @tokens;
}
my %compound = (
'{' => \&parse_group,
'(' => \&parse_subshell,
'case' => \&parse_case,
'for' => \&parse_for,
'if' => \&parse_if,
'until' => \&parse_loop,
'while' => \&parse_loop);
sub parse_cmd {
my $self = shift @_;
my $cmd = $self->next_token();
return () unless defined($cmd);
return $cmd if $cmd eq "\n";
my $token;
my @tokens = $cmd;
if ($cmd eq '!') {
push(@tokens, $self->parse_cmd());
return @tokens;
} elsif (my $f = $compound{$cmd}) {
push(@tokens, $self->$f());
} elsif (defined($token = $self->peek()) && $token eq '(') {
if ($cmd !~ /\w=$/) {
push(@tokens, $self->parse_func());
return @tokens;
}
$tokens[-1] .= join(' ', $self->parse_bash_array_assignment());
}
while (defined(my $token = $self->next_token())) {
$self->untoken($token), last if $self->stop_at($token);
push(@tokens, $token);
last if $token =~ /^(?:[;&\n|]|&&|\|\|)$/;
}
push(@tokens, $self->next_token()) if $tokens[-1] ne "\n" && defined($token = $self->peek()) && $token eq "\n";
return @tokens;
}
sub accumulate {
my ($self, $tokens, $cmd) = @_;
push(@$tokens, @$cmd);
}
sub parse {
my ($self, $stop) = @_;
push(@{$self->{stop}}, $stop);
goto DONE if $self->stop_at($self->peek());
my @tokens;
while (my @cmd = $self->parse_cmd()) {
$self->accumulate(\@tokens, \@cmd);
last if $self->stop_at($self->peek());
}
DONE:
pop(@{$self->{stop}});
return @tokens;
}
# TestParser is a subclass of ShellParser which, beyond parsing shell script
# code, is also imbued with semantic knowledge of test construction, and checks
# tests for common problems (such as broken &&-chains) which might hide bugs in
# the tests themselves or in behaviors being exercised by the tests. As such,
# TestParser is only called upon to parse test bodies, not the top-level
# scripts in which the tests are defined.
package TestParser;
use base 'ShellParser';
sub find_non_nl {
my $tokens = shift @_;
my $n = shift @_;
$n = $#$tokens if !defined($n);
$n-- while $n >= 0 && $$tokens[$n] eq "\n";
return $n;
}
sub ends_with {
my ($tokens, $needles) = @_;
my $n = find_non_nl($tokens);
for my $needle (reverse(@$needles)) {
return undef if $n < 0;
$n = find_non_nl($tokens, $n), next if $needle eq "\n";
return undef if $$tokens[$n] !~ $needle;
$n--;
}
return 1;
}
sub match_ending {
my ($tokens, $endings) = @_;
for my $needles (@$endings) {
next if @$tokens < scalar(grep {$_ ne "\n"} @$needles);
return 1 if ends_with($tokens, $needles);
}
return undef;
}
sub parse_loop_body {
my $self = shift @_;
my @tokens = $self->SUPER::parse_loop_body(@_);
# did loop signal failure via "|| return" or "|| exit"?
return @tokens if !@tokens || grep(/^(?:return|exit|\$\?)$/, @tokens);
# did loop upstream of a pipe signal failure via "|| echo 'impossible
# text'" as the final command in the loop body?
return @tokens if ends_with(\@tokens, [qr/^\|\|$/, "\n", qr/^echo$/, qr/^.+$/]);
# flag missing "return/exit" handling explicit failure in loop body
my $n = find_non_nl(\@tokens);
splice(@tokens, $n + 1, 0, '?!LOOP?!');
return @tokens;
}
my @safe_endings = (
[qr/^(?:&&|\|\||\||&)$/],
[qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/],
[qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/, qr/^;$/],
[qr/^(?:exit|return|continue)$/],
[qr/^(?:exit|return|continue)$/, qr/^;$/]);
sub accumulate {
my ($self, $tokens, $cmd) = @_;
goto DONE unless @$tokens;
goto DONE if @$cmd == 1 && $$cmd[0] eq "\n";
# did previous command end with "&&", "|", "|| return" or similar?
goto DONE if match_ending($tokens, \@safe_endings);
# if this command handles "$?" specially, then okay for previous
# command to be missing "&&"
for my $token (@$cmd) {
goto DONE if $token =~ /\$\?/;
}
# if this command is "false", "return 1", or "exit 1" (which signal
# failure explicitly), then okay for all preceding commands to be
# missing "&&"
if ($$cmd[0] =~ /^(?:false|return|exit)$/) {
@$tokens = grep(!/^\?!AMP\?!$/, @$tokens);
goto DONE;
}
# flag missing "&&" at end of previous command
my $n = find_non_nl($tokens);
splice(@$tokens, $n + 1, 0, '?!AMP?!') unless $n < 0;
DONE:
$self->SUPER::accumulate($tokens, $cmd);
}
# ScriptParser is a subclass of ShellParser which identifies individual test
# definitions within test scripts, and passes each test body through TestParser
# to identify possible problems. ShellParser detects test definitions not only
# at the top-level of test scripts but also within compound commands such as
# loops and function definitions.
package ScriptParser;
use base 'ShellParser';
sub new {
my $class = shift @_;
my $self = $class->SUPER::new(@_);
$self->{ntests} = 0;
return $self;
}
# extract the raw content of a token, which may be a single string or a
# composition of multiple strings and non-string character runs; for instance,
# `"test body"` unwraps to `test body`; `word"a b"42'c d'` to `worda b42c d`
sub unwrap {
my $token = @_ ? shift @_ : $_;
# simple case: 'sqstring' or "dqstring"
return $token if $token =~ s/^'([^']*)'$/$1/;
return $token if $token =~ s/^"([^"]*)"$/$1/;
# composite case
my ($s, $q, $escaped);
while (1) {
# slurp up non-special characters
$s .= $1 if $token =~ /\G([^\\'"]*)/gc;
# handle special characters
last unless $token =~ /\G(.)/sgc;
my $c = $1;
$q = undef, next if defined($q) && $c eq $q;
$q = $c, next if !defined($q) && $c =~ /^['"]$/;
if ($c eq '\\') {
last unless $token =~ /\G(.)/sgc;
$c = $1;
$s .= '\\' if $c eq "\n"; # preserve line splice
}
$s .= $c;
}
return $s
}
sub check_test {
my $self = shift @_;
my ($title, $body) = map(unwrap, @_);
$self->{ntests}++;
my $parser = TestParser->new(\$body);
my @tokens = $parser->parse();
return unless $emit_all || grep(/\?![^?]+\?!/, @tokens);
my $c = main::fd_colors(1);
my $checked = join(' ', @tokens);
$checked =~ s/^\n//;
$checked =~ s/^ //mg;
$checked =~ s/ $//mg;
$checked =~ s/(\?![^?]+\?!)/$c->{rev}$c->{red}$1$c->{reset}/mg;
$checked .= "\n" unless $checked =~ /\n$/;
push(@{$self->{output}}, "$c->{blue}# chainlint: $title$c->{reset}\n$checked");
}
sub parse_cmd {
my $self = shift @_;
my @tokens = $self->SUPER::parse_cmd();
return @tokens unless @tokens && $tokens[0] =~ /^test_expect_(?:success|failure)$/;
my $n = $#tokens;
$n-- while $n >= 0 && $tokens[$n] =~ /^(?:[;&\n|]|&&|\|\|)$/;
$self->check_test($tokens[1], $tokens[2]) if $n == 2; # title body
$self->check_test($tokens[2], $tokens[3]) if $n > 2; # prereq title body
return @tokens;
}
# main contains high-level functionality for processing command-line switches,
# feeding input test scripts to ScriptParser, and reporting results.
package main;
my $getnow = sub { return time(); };
my $interval = sub { return time() - shift; };
if (eval {require Time::HiRes; Time::HiRes->import(); 1;}) {
$getnow = sub { return [Time::HiRes::gettimeofday()]; };
$interval = sub { return Time::HiRes::tv_interval(shift); };
}
# Restore TERM if test framework set it to "dumb" so 'tput' will work; do this
# outside of get_colors() since under 'ithreads' all threads use %ENV of main
# thread and ignore %ENV changes in subthreads.
$ENV{TERM} = $ENV{USER_TERM} if $ENV{USER_TERM};
my @NOCOLORS = (bold => '', rev => '', reset => '', blue => '', green => '', red => '');
my %COLORS = ();
sub get_colors {
return \%COLORS if %COLORS;
if (exists($ENV{NO_COLOR}) ||
system("tput sgr0 >/dev/null 2>&1") != 0 ||
system("tput bold >/dev/null 2>&1") != 0 ||
system("tput rev >/dev/null 2>&1") != 0 ||
system("tput setaf 1 >/dev/null 2>&1") != 0) {
%COLORS = @NOCOLORS;
return \%COLORS;
}
%COLORS = (bold => `tput bold`,
rev => `tput rev`,
reset => `tput sgr0`,
blue => `tput setaf 4`,
green => `tput setaf 2`,
red => `tput setaf 1`);
chomp(%COLORS);
return \%COLORS;
}
my %FD_COLORS = ();
sub fd_colors {
my $fd = shift;
return $FD_COLORS{$fd} if exists($FD_COLORS{$fd});
$FD_COLORS{$fd} = -t $fd ? get_colors() : {@NOCOLORS};
return $FD_COLORS{$fd};
}
sub ncores {
# Windows
return $ENV{NUMBER_OF_PROCESSORS} if exists($ENV{NUMBER_OF_PROCESSORS});
# Linux / MSYS2 / Cygwin / WSL
do { local @ARGV='/proc/cpuinfo'; return scalar(grep(/^processor\s*:/, <>)); } if -r '/proc/cpuinfo';
# macOS & BSD
return qx/sysctl -n hw.ncpu/ if $^O =~ /(?:^darwin$|bsd)/;
return 1;
}
sub show_stats {
my ($start_time, $stats) = @_;
my $walltime = $interval->($start_time);
my ($usertime) = times();
my ($total_workers, $total_scripts, $total_tests, $total_errs) = (0, 0, 0, 0);
my $c = fd_colors(2);
print(STDERR $c->{green});
for (@$stats) {
my ($worker, $nscripts, $ntests, $nerrs) = @$_;
print(STDERR "worker $worker: $nscripts scripts, $ntests tests, $nerrs errors\n");
$total_workers++;
$total_scripts += $nscripts;
$total_tests += $ntests;
$total_errs += $nerrs;
}
printf(STDERR "total: %d workers, %d scripts, %d tests, %d errors, %.2fs/%.2fs (wall/user)$c->{reset}\n", $total_workers, $total_scripts, $total_tests, $total_errs, $walltime, $usertime);
}
sub check_script {
my ($id, $next_script, $emit) = @_;
my ($nscripts, $ntests, $nerrs) = (0, 0, 0);
while (my $path = $next_script->()) {
$nscripts++;
my $fh;
unless (open($fh, "<", $path)) {
$emit->("?!ERR?! $path: $!\n");
next;
}
my $s = do { local $/; <$fh> };
close($fh);
my $parser = ScriptParser->new(\$s);
1 while $parser->parse_cmd();
if (@{$parser->{output}}) {
my $c = fd_colors(1);
my $s = join('', @{$parser->{output}});
$emit->("$c->{bold}$c->{blue}# chainlint: $path$c->{reset}\n" . $s);
$nerrs += () = $s =~ /\?![^?]+\?!/g;
}
$ntests += $parser->{ntests};
}
return [$id, $nscripts, $ntests, $nerrs];
}
sub exit_code {
my $stats = shift @_;
for (@$stats) {
my ($worker, $nscripts, $ntests, $nerrs) = @$_;
return 1 if $nerrs;
}
return 0;
}
Getopt::Long::Configure(qw{bundling});
GetOptions(
"emit-all!" => \$emit_all,
"jobs|j=i" => \$jobs,
"stats|show-stats!" => \$show_stats) or die("option error\n");
$jobs = ncores() if $jobs < 1;
my $start_time = $getnow->();
my @stats;
my @scripts;
push(@scripts, File::Glob::bsd_glob($_)) for (@ARGV);
unless (@scripts) {
show_stats($start_time, \@stats) if $show_stats;
exit;
}
unless ($Config{useithreads} && eval {
require threads; threads->import();
require Thread::Queue; Thread::Queue->import();
1;
}) {
push(@stats, check_script(1, sub { shift(@scripts); }, sub { print(@_); }));
show_stats($start_time, \@stats) if $show_stats;
exit(exit_code(\@stats));
}
my $script_queue = Thread::Queue->new();
my $output_queue = Thread::Queue->new();
sub next_script { return $script_queue->dequeue(); }
sub emit { $output_queue->enqueue(@_); }
sub monitor {
while (my $s = $output_queue->dequeue()) {
print($s);
}
}
my $mon = threads->create({'context' => 'void'}, \&monitor);
threads->create({'context' => 'list'}, \&check_script, $_, \&next_script, \&emit) for 1..$jobs;
$script_queue->enqueue(@scripts);
$script_queue->end();
for (threads->list()) {
push(@stats, $_->join()) unless $_ == $mon;
}
$output_queue->end();
$mon->join();
show_stats($start_time, \@stats) if $show_stats;
exit(exit_code(\@stats));

View File

@ -1,399 +0,0 @@
#------------------------------------------------------------------------------
# Detect broken &&-chains in tests.
#
# At present, only &&-chains in subshells are examined by this linter;
# top-level &&-chains are instead checked directly by the test framework. Like
# the top-level &&-chain linter, the subshell linter (intentionally) does not
# check &&-chains within {...} blocks.
#
# Checking for &&-chain breakage is done line-by-line by pure textual
# inspection.
#
# Incomplete lines (those ending with "\") are stitched together with following
# lines to simplify processing, particularly of "one-liner" statements.
# Top-level here-docs are swallowed to avoid false positives within the
# here-doc body, although the statement to which the here-doc is attached is
# retained.
#
# Heuristics are used to detect end-of-subshell when the closing ")" is cuddled
# with the final subshell statement on the same line:
#
# (cd foo &&
# bar)
#
# in order to avoid misinterpreting the ")" in constructs such as "x=$(...)"
# and "case $x in *)" as ending the subshell.
#
# Lines missing a final "&&" are flagged with "?!AMP?!", as are lines which
# chain commands with ";" internally rather than "&&". A line may be flagged
# for both violations.
#
# Detection of a missing &&-link in a multi-line subshell is complicated by the
# fact that the last statement before the closing ")" must not end with "&&".
# Since processing is line-by-line, it is not known whether a missing "&&" is
# legitimate or not until the _next_ line is seen. To accommodate this, within
# multi-line subshells, each line is stored in sed's "hold" area until after
# the next line is seen and processed. If the next line is a stand-alone ")",
# then a missing "&&" on the previous line is legitimate; otherwise a missing
# "&&" is a break in the &&-chain.
#
# (
# cd foo &&
# bar
# )
#
# In practical terms, when "bar" is encountered, it is flagged with "?!AMP?!",
# but when the stand-alone ")" line is seen which closes the subshell, the
# "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold"
# area) since the final statement of a subshell must not end with "&&". The
# final line of a subshell may still break the &&-chain by using ";" internally
# to chain commands together rather than "&&", but an internal "?!AMP?!" is
# never removed from a line even though a line-ending "?!AMP?!" might be.
#
# Care is taken to recognize the last _statement_ of a multi-line subshell, not
# necessarily the last textual _line_ within the subshell, since &&-chaining
# applies to statements, not to lines. Consequently, blank lines, comment
# lines, and here-docs are swallowed (but not the command to which the here-doc
# is attached), leaving the last statement in the "hold" area, not the last
# line, thus simplifying &&-link checking.
#
# The final statement before "done" in for- and while-loops, and before "elif",
# "else", and "fi" in if-then-else likewise must not end with "&&", thus
# receives similar treatment.
#
# Swallowing here-docs with arbitrary tags requires a bit of finesse. When a
# line such as "cat <<EOF" is seen, the here-doc tag is copied to the front of
# the line enclosed in angle brackets as a sentinel, giving "<EOF>cat <<EOF".
# As each subsequent line is read, it is appended to the target line and a
# (whitespace-loose) back-reference match /^<(.*)>\n\1$/ is attempted to see if
# the content inside "<...>" matches the entirety of the newly-read line. For
# instance, if the next line read is "some data", when concatenated with the
# target line, it becomes "<EOF>cat <<EOF\nsome data", and a match is attempted
# to see if "EOF" matches "some data". Since it doesn't, the next line is
# attempted. When a line consisting of only "EOF" (and possible whitespace) is
# encountered, it is appended to the target line giving "<EOF>cat <<EOF\nEOF",
# in which case the "EOF" inside "<...>" does match the text following the
# newline, thus the closing here-doc tag has been found. The closing tag line
# and the "<...>" prefix on the target line are then discarded, leaving just
# the target line "cat <<EOF".
#------------------------------------------------------------------------------
# incomplete line -- slurp up next line
:squash
/\\$/ {
N
s/\\\n//
bsquash
}
# here-doc -- swallow it to avoid false hits within its body (but keep the
# command to which it was attached)
/<<-*[ ]*[\\'"]*[A-Za-z0-9_]/ {
/"[^"]*<<[^"]*"/bnotdoc
s/^\(.*<<-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1\2/
:hered
N
/^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{
s/\n.*$//
bhered
}
s/^<[^>]*>//
s/\n.*$//
}
:notdoc
# one-liner "(...) &&"
/^[ ]*!*[ ]*(..*)[ ]*&&[ ]*$/boneline
# same as above but without trailing "&&"
/^[ ]*!*[ ]*(..*)[ ]*$/boneline
# one-liner "(...) >x" (or "2>x" or "<x" or "|x" or "&"
/^[ ]*!*[ ]*(..*)[ ]*[0-9]*[<>|&]/boneline
# multi-line "(...\n...)"
/^[ ]*(/bsubsh
# innocuous line -- print it and advance to next line
b
# found one-liner "(...)" -- mark suspect if it uses ";" internally rather than
# "&&" (but not ";" in a string)
:oneline
/;/{
/"[^"]*;[^"]*"/!s/;/; ?!AMP?!/
}
b
:subsh
# bare "(" line? -- stash for later printing
/^[ ]*([ ]*$/ {
h
bnextln
}
# "(..." line -- "(" opening subshell cuddled with command; temporarily replace
# "(" with sentinel "^" and process the line as if "(" had been seen solo on
# the preceding line; this temporary replacement prevents several rules from
# accidentally thinking "(" introduces a nested subshell; "^" is changed back
# to "(" at output time
x
s/.*//
x
s/(/^/
bslurp
:nextln
N
s/.*\n//
:slurp
# incomplete line "...\"
/\\$/bicmplte
# multi-line quoted string "...\n..."?
/"/bdqstr
# multi-line quoted string '...\n...'? (but not contraction in string "it's")
/'/{
/"[^'"]*'[^'"]*"/!bsqstr
}
:folded
# here-doc -- swallow it (but not "<<" in a string)
/<<-*[ ]*[\\'"]*[A-Za-z0-9_]/{
/"[^"]*<<[^"]*"/!bheredoc
}
# comment or empty line -- discard since final non-comment, non-empty line
# before closing ")", "done", "elsif", "else", or "fi" will need to be
# re-visited to drop "suspect" marking since final line of those constructs
# legitimately lacks "&&", so "suspect" mark must be removed
/^[ ]*#/bnextln
/^[ ]*$/bnextln
# in-line comment -- strip it (but not "#" in a string, Bash ${#...} array
# length, or Perforce "//depot/path#42" revision in filespec)
/[ ]#/{
/"[^"]*#[^"]*"/!s/[ ]#.*$//
}
# one-liner "case ... esac"
/^[ ^]*case[ ]*..*esac/bchkchn
# multi-line "case ... esac"
/^[ ^]*case[ ]..*[ ]in/bcase
# multi-line "for ... done" or "while ... done"
/^[ ^]*for[ ]..*[ ]in/bcont
/^[ ^]*while[ ]/bcont
/^[ ]*do[ ]/bcont
/^[ ]*do[ ]*$/bcont
/;[ ]*do/bcont
/^[ ]*done[ ]*&&[ ]*$/bdone
/^[ ]*done[ ]*$/bdone
/^[ ]*done[ ]*[<>|]/bdone
/^[ ]*done[ ]*)/bdone
/||[ ]*exit[ ]/bcont
/||[ ]*exit[ ]*$/bcont
# multi-line "if...elsif...else...fi"
/^[ ^]*if[ ]/bcont
/^[ ]*then[ ]/bcont
/^[ ]*then[ ]*$/bcont
/;[ ]*then/bcont
/^[ ]*elif[ ]/belse
/^[ ]*elif[ ]*$/belse
/^[ ]*else[ ]/belse
/^[ ]*else[ ]*$/belse
/^[ ]*fi[ ]*&&[ ]*$/bdone
/^[ ]*fi[ ]*$/bdone
/^[ ]*fi[ ]*[<>|]/bdone
/^[ ]*fi[ ]*)/bdone
# nested one-liner "(...) &&"
/^[ ^]*(.*)[ ]*&&[ ]*$/bchkchn
# nested one-liner "(...)"
/^[ ^]*(.*)[ ]*$/bchkchn
# nested one-liner "(...) >x" (or "2>x" or "<x" or "|x")
/^[ ^]*(.*)[ ]*[0-9]*[<>|]/bchkchn
# nested multi-line "(...\n...)"
/^[ ^]*(/bnest
# multi-line "{...\n...}"
/^[ ^]*{/bblock
# closing ")" on own line -- exit subshell
/^[ ]*)/bclssolo
# "$((...))" -- arithmetic expansion; not closing ")"
/\$(([^)][^)]*))[^)]*$/bchkchn
# "$(...)" -- command substitution; not closing ")"
/\$([^)][^)]*)[^)]*$/bchkchn
# multi-line "$(...\n...)" -- command substitution; treat as nested subshell
/\$([^)]*$/bnest
# "=(...)" -- Bash array assignment; not closing ")"
/=(/bchkchn
# closing "...) &&"
/)[ ]*&&[ ]*$/bclose
# closing "...)"
/)[ ]*$/bclose
# closing "...) >x" (or "2>x" or "<x" or "|x")
/)[ ]*[<>|]/bclose
:chkchn
# mark suspect if line uses ";" internally rather than "&&" (but not ";" in a
# string and not ";;" in one-liner "case...esac")
/;/{
/;;/!{
/"[^"]*;[^"]*"/!s/;/; ?!AMP?!/
}
}
# line ends with pipe "...|" -- valid; not missing "&&"
/|[ ]*$/bcont
# missing end-of-line "&&" -- mark suspect
/&&[ ]*$/!s/$/ ?!AMP?!/
:cont
# retrieve and print previous line
x
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
n
bslurp
# found incomplete line "...\" -- slurp up next line
:icmplte
N
s/\\\n//
bslurp
# check for multi-line double-quoted string "...\n..." -- fold to one line
:dqstr
# remove all quote pairs
s/"\([^"]*\)"/@!\1@!/g
# done if no dangling quote
/"/!bdqdone
# otherwise, slurp next line and try again
N
s/\n//
bdqstr
:dqdone
s/@!/"/g
bfolded
# check for multi-line single-quoted string '...\n...' -- fold to one line
:sqstr
# remove all quote pairs
s/'\([^']*\)'/@!\1@!/g
# done if no dangling quote
/'/!bsqdone
# otherwise, slurp next line and try again
N
s/\n//
bsqstr
:sqdone
s/@!/'/g
bfolded
# found here-doc -- swallow it to avoid false hits within its body (but keep
# the command to which it was attached)
:heredoc
s/^\(.*\)<<\(-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\3>\1?!HERE?!\2\3/
:hdocsub
N
/^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{
s/\n.*$//
bhdocsub
}
s/^<[^>]*>//
s/\n.*$//
bfolded
# found "case ... in" -- pass through untouched
:case
x
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
n
:cascom
/^[ ]*#/{
N
s/.*\n//
bcascom
}
/^[ ]*esac/bslurp
bcase
# found "else" or "elif" -- drop "suspect" from final line before "else" since
# that line legitimately lacks "&&"
:else
x
s/\( ?!AMP?!\)* ?!AMP?!$//
x
bcont
# found "done" closing for-loop or while-loop, or "fi" closing if-then -- drop
# "suspect" from final contained line since that line legitimately lacks "&&"
:done
x
s/\( ?!AMP?!\)* ?!AMP?!$//
x
# is 'done' or 'fi' cuddled with ")" to close subshell?
/done.*)/bclose
/fi.*)/bclose
bchkchn
# found nested multi-line "(...\n...)" -- pass through untouched
:nest
x
:nstslrp
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
n
:nstcom
# comment -- not closing ")" if in comment
/^[ ]*#/{
N
s/.*\n//
bnstcom
}
# closing ")" on own line -- stop nested slurp
/^[ ]*)/bnstcl
# "$((...))" -- arithmetic expansion; not closing ")"
/\$(([^)][^)]*))[^)]*$/bnstcnt
# "$(...)" -- command substitution; not closing ")"
/\$([^)][^)]*)[^)]*$/bnstcnt
# closing "...)" -- stop nested slurp
/)/bnstcl
:nstcnt
x
bnstslrp
:nstcl
# is it "))" which closes nested and parent subshells?
/)[ ]*)/bslurp
bchkchn
# found multi-line "{...\n...}" block -- pass through untouched
:block
x
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
n
:blkcom
/^[ ]*#/{
N
s/.*\n//
bblkcom
}
# closing "}" -- stop block slurp
/}/bchkchn
bblock
# found closing ")" on own line -- drop "suspect" from final line of subshell
# since that line legitimately lacks "&&" and exit subshell loop
:clssolo
x
s/\( ?!AMP?!\)* ?!AMP?!$//
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
p
x
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
b
# found closing "...)" -- exit subshell loop
:close
x
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
p
x
s/^\([ ]*\)^/\1(/
s/?!HERE?!/<</g
b

View File

@ -0,0 +1,18 @@
test_done ( ) {
case "$test_failure" in
0 )
test_at_end_hook_
exit 0 ;;
* )
if test $test_external_has_tap -eq 0
then
say_color error "# failed $test_failure among $msg"
say "1..$test_count"
fi
exit 1 ;;
esac
}

View File

@ -0,0 +1,19 @@
# LINT: blank line before "esac"
test_done () {
case "$test_failure" in
0)
test_at_end_hook_
exit 0 ;;
*)
if test $test_external_has_tap -eq 0
then
say_color error "# failed $test_failure among $msg"
say "1..$test_count"
fi
exit 1 ;;
esac
}

View File

@ -1,7 +1,7 @@
( (
foo && foo &&
{ {
echo a echo a ?!AMP?!
echo b echo b
} && } &&
bar && bar &&
@ -9,4 +9,15 @@
echo c echo c
} ?!AMP?! } ?!AMP?!
baz baz
) ) &&
{
echo a ; ?!AMP?! echo b
} &&
{ echo a ; ?!AMP?! echo b ; } &&
{
echo "${var}9" &&
echo "done"
} &&
finis

View File

@ -11,4 +11,17 @@
echo c echo c
} }
baz baz
) ) &&
# LINT: ";" not allowed in place of "&&"
{
echo a; echo b
} &&
{ echo a; echo b; } &&
# LINT: "}" inside string not mistaken as end of block
{
echo "${var}9" &&
echo "done"
} &&
finis

View File

@ -0,0 +1,9 @@
JGIT_DAEMON_PID= &&
git init --bare empty.git &&
> empty.git/git-daemon-export-ok &&
mkfifo jgit_daemon_output &&
{
jgit daemon --port="$JGIT_DAEMON_PORT" . > jgit_daemon_output &
JGIT_DAEMON_PID=$!
} &&
test_expect_code 2 git ls-remote --exit-code git://localhost:$JGIT_DAEMON_PORT/empty.git

View File

@ -0,0 +1,10 @@
JGIT_DAEMON_PID= &&
git init --bare empty.git &&
>empty.git/git-daemon-export-ok &&
mkfifo jgit_daemon_output &&
{
# LINT: exit status of "&" is always 0 so &&-chaining immaterial
jgit daemon --port="$JGIT_DAEMON_PORT" . >jgit_daemon_output &
JGIT_DAEMON_PID=$!
} &&
test_expect_code 2 git ls-remote --exit-code git://localhost:$JGIT_DAEMON_PORT/empty.git

View File

@ -0,0 +1,12 @@
git ls-tree --name-only -r refs/notes/many_notes |
while read path
do
test "$path" = "foobar/non-note.txt" && continue
test "$path" = "deadbeef" && continue
test "$path" = "de/adbeef" && continue
if test $(expr length "$path") -ne $hexsz
then
return 1
fi
done

View File

@ -0,0 +1,13 @@
git ls-tree --name-only -r refs/notes/many_notes |
while read path
do
# LINT: broken &&-chain okay if explicit "continue"
test "$path" = "foobar/non-note.txt" && continue
test "$path" = "deadbeef" && continue
test "$path" = "de/adbeef" && continue
if test $(expr length "$path") -ne $hexsz
then
return 1
fi
done

View File

@ -0,0 +1,9 @@
if condition not satisified
then
echo it did not work...
echo failed!
false
else
echo it went okay ?!AMP?!
congratulate user
fi

View File

@ -0,0 +1,10 @@
# LINT: broken &&-chain okay if explicit "false" signals failure
if condition not satisified
then
echo it did not work...
echo failed!
false
else
echo it went okay
congratulate user
fi

View File

@ -0,0 +1,19 @@
case "$(git ls-files)" in
one ) echo pass one ;;
* ) echo bad one ; return 1 ;;
esac &&
(
case "$(git ls-files)" in
two ) echo pass two ;;
* ) echo bad two ; exit 1 ;;
esac
) &&
case "$(git ls-files)" in
dir/two"$LF"one ) echo pass both ;;
* ) echo bad ; return 1 ;;
esac &&
for i in 1 2 3 4 ; do
git checkout main -b $i || return $?
test_commit $i $i $i tag$i || return $?
done

View File

@ -0,0 +1,23 @@
case "$(git ls-files)" in
one) echo pass one ;;
# LINT: broken &&-chain okay if explicit "return 1" signals failuire
*) echo bad one; return 1 ;;
esac &&
(
case "$(git ls-files)" in
two) echo pass two ;;
# LINT: broken &&-chain okay if explicit "exit 1" signals failuire
*) echo bad two; exit 1 ;;
esac
) &&
case "$(git ls-files)" in
dir/two"$LF"one) echo pass both ;;
# LINT: broken &&-chain okay if explicit "return 1" signals failuire
*) echo bad; return 1 ;;
esac &&
for i in 1 2 3 4 ; do
# LINT: broken &&-chain okay if explicit "return $?" signals failure
git checkout main -b $i || return $?
test_commit $i $i $i tag$i || return $?
done

View File

@ -0,0 +1,9 @@
OUT=$(( ( large_git ; echo $? 1 >& 3 ) | : ) 3 >& 1) &&
test_match_signal 13 "$OUT" &&
{ test-tool sigchain > actual ; ret=$? ; } &&
{
test_match_signal 15 "$ret" ||
test "$ret" = 3
} &&
test_cmp expect actual

View File

@ -0,0 +1,11 @@
# LINT: broken &&-chain okay if next command handles "$?" explicitly
OUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&
test_match_signal 13 "$OUT" &&
# LINT: broken &&-chain okay if next command handles "$?" explicitly
{ test-tool sigchain >actual; ret=$?; } &&
{
test_match_signal 15 "$ret" ||
test "$ret" = 3
} &&
test_cmp expect actual

View File

@ -0,0 +1,9 @@
echo nobody home && {
test the doohicky ?!AMP?!
right now
} &&
GIT_EXTERNAL_DIFF=echo git diff | {
read path oldfile oldhex oldmode newfile newhex newmode &&
test "z$oh" = "z$oldhex"
}

View File

@ -0,0 +1,11 @@
# LINT: start of block chained to preceding command
echo nobody home && {
test the doohicky
right now
} &&
# LINT: preceding command pipes to block on same line
GIT_EXTERNAL_DIFF=echo git diff | {
read path oldfile oldhex oldmode newfile newhex newmode &&
test "z$oh" = "z$oldhex"
}

View File

@ -0,0 +1,10 @@
mkdir sub && (
cd sub &&
foo the bar ?!AMP?!
nuff said
) &&
cut "-d " -f actual | ( read s1 s2 s3 &&
test -f $s1 ?!AMP?!
test $(cat $s2) = tree2path1 &&
test $(cat $s3) = tree3path1 )

View File

@ -0,0 +1,13 @@
# LINT: start of subshell chained to preceding command
mkdir sub && (
cd sub &&
foo the bar
nuff said
) &&
# LINT: preceding command pipes to subshell on same line
cut "-d " -f actual | (read s1 s2 s3 &&
test -f $s1
test $(cat $s2) = tree2path1 &&
# LINT: closing subshell ")" correctly detected on same line as "$(...)"
test $(cat $s3) = tree3path1)

View File

@ -0,0 +1,2 @@
OUT=$(( ( large_git 1 >& 3 ) | : ) 3 >& 1) &&
test_match_signal 13 "$OUT"

View File

@ -0,0 +1,3 @@
# LINT: subshell nested in subshell nested in command substitution
OUT=$( ((large_git 1>&3) | :) 3>&1 ) &&
test_match_signal 13 "$OUT"

View File

@ -4,6 +4,6 @@
: :
else else
echo >file echo >file
fi fi ?!LOOP?!
done) && done) &&
test ! -f file test ! -f file

View File

@ -0,0 +1,2 @@
run_sub_test_lib_test_err run-inv-range-start "--run invalid range start" --run="a-5" <<-EOF &&
check_sub_test_lib_test_err run-inv-range-start <<-EOF_OUT 3 <<-EOF_ERR

View File

@ -0,0 +1,12 @@
run_sub_test_lib_test_err run-inv-range-start \
"--run invalid range start" \
--run="a-5" <<-\EOF &&
test_expect_success "passing test #1" "true"
test_done
EOF
check_sub_test_lib_test_err run-inv-range-start \
<<-\EOF_OUT 3<<-EOF_ERR
> FATAL: Unexpected exit with code 1
EOF_OUT
> error: --run: invalid non-numeric in range start: ${SQ}a-5${SQ}
EOF_ERR

View File

@ -0,0 +1,3 @@
echo 'fatal: reword option of --fixup is mutually exclusive with' '--patch/--interactive/--all/--include/--only' > expect &&
test_must_fail git commit --fixup=reword:HEAD~ $1 2 > actual &&
test_cmp expect actual

View File

@ -0,0 +1,7 @@
# LINT: line-splice within DQ-string
'"
echo 'fatal: reword option of --fixup is mutually exclusive with'\
'--patch/--interactive/--all/--include/--only' >expect &&
test_must_fail git commit --fixup=reword:HEAD~ $1 2>actual &&
test_cmp expect actual
"'

View File

@ -0,0 +1,11 @@
grep "^ ! [rejected][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" out &&
grep "^\.git$" output.txt &&
(
cd client$version &&
GIT_TEST_PROTOCOL_VERSION=$version git fetch-pack --no-progress .. $(cat ../input)
) > output &&
cut -d ' ' -f 2 < output | sort > actual &&
test_cmp expect actual

View File

@ -0,0 +1,15 @@
# LINT: regex dollar-sign eol anchor in double-quoted string not special
grep "^ ! \[rejected\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" out &&
# LINT: escaped "$" not mistaken for variable expansion
grep "^\\.git\$" output.txt &&
'"
(
cd client$version &&
# LINT: escaped dollar-sign in double-quoted test body
GIT_TEST_PROTOCOL_VERSION=$version git fetch-pack --no-progress .. \$(cat ../input)
) >output &&
cut -d ' ' -f 2 <output | sort >actual &&
test_cmp expect actual
"'

View File

@ -0,0 +1,3 @@
git ls-tree $tree path > current &&
cat > expected <<EOF &&
test_output

View File

@ -0,0 +1,5 @@
git ls-tree $tree path >current &&
# LINT: empty here-doc
cat >expected <<\EOF &&
EOF
test_output

View File

@ -0,0 +1,4 @@
if ! condition ; then echo nope ; else yep ; fi &&
test_prerequisite !MINGW &&
mail uucp!address &&
echo !whatever!

View File

@ -0,0 +1,8 @@
# LINT: "! word" is two tokens
if ! condition; then echo nope; else yep; fi &&
# LINT: "!word" is single token, not two tokens "!" and "word"
test_prerequisite !MINGW &&
# LINT: "word!word" is single token, not three tokens "word", "!", and "word"
mail uucp!address &&
# LINT: "!word!" is single token, not three tokens "!", "word", and "!"
echo !whatever!

View File

@ -0,0 +1,5 @@
for it
do
path=$(expr "$it" : ( [^:]*) ) &&
git update-index --add "$path" || exit
done

View File

@ -0,0 +1,6 @@
# LINT: for-loop lacking optional "in [word...]" before "do"
for it
do
path=$(expr "$it" : '\([^:]*\)') &&
git update-index --add "$path" || exit
done

View File

@ -2,10 +2,10 @@
for i in a b c for i in a b c
do do
echo $i ?!AMP?! echo $i ?!AMP?!
cat <<-EOF cat <<-EOF ?!LOOP?!
done ?!AMP?! done ?!AMP?!
for i in a b c; do for i in a b c; do
echo $i && echo $i &&
cat $i cat $i ?!LOOP?!
done done
) )

View File

@ -0,0 +1,11 @@
sha1_file ( ) {
echo "$*" | sed "s#..#.git/objects/&/#"
} &&
remove_object ( ) {
file=$(sha1_file "$*") &&
test -e "$file" ?!AMP?!
rm -f "$file"
} ?!AMP?!
sha1_file arg && remove_object arg

13
t/chainlint/function.test Normal file
View File

@ -0,0 +1,13 @@
# LINT: "()" in function definition not mistaken for subshell
sha1_file() {
echo "$*" | sed "s#..#.git/objects/&/#"
} &&
# LINT: broken &&-chain in function and after function
remove_object() {
file=$(sha1_file "$*") &&
test -e "$file"
rm -f "$file"
}
sha1_file arg && remove_object arg

View File

@ -0,0 +1,5 @@
cat > expect <<-EOF &&
cat > expect <<-EOF ?!AMP?!
cleanup

View File

@ -0,0 +1,13 @@
# LINT: whitespace between operator "<<-" and tag legal
cat >expect <<- EOF &&
header: 43475048 1 $(test_oid oid_version) $NUM_CHUNKS 0
num_commits: $1
chunks: oid_fanout oid_lookup commit_metadata generation_data bloom_indexes bloom_data
EOF
# LINT: not an indented here-doc; just a plain here-doc with tag named "-EOF"
cat >expect << -EOF
this is not indented
-EOF
cleanup

View File

@ -1,4 +1,5 @@
( (
cat <<-TXT && echo "multi-line string" ?!AMP?! cat <<-TXT && echo "multi-line
string" ?!AMP?!
bap bap
) )

View File

@ -0,0 +1,7 @@
if bob &&
marcia ||
kevin
then
echo "nomads" ?!AMP?!
echo "for sure"
fi

View File

@ -0,0 +1,8 @@
# LINT: "if" condition split across multiple lines at "&&" or "||"
if bob &&
marcia ||
kevin
then
echo "nomads"
echo "for sure"
fi

View File

@ -3,7 +3,7 @@
do do
if false if false
then then
echo "err" ?!AMP?! echo "err"
exit 1 exit 1
fi ?!AMP?! fi ?!AMP?!
foo foo

View File

@ -3,7 +3,7 @@
do do
if false if false
then then
# LINT: missing "&&" on "echo" # LINT: missing "&&" on "echo" okay since "exit 1" signals error explicitly
echo "err" echo "err"
exit 1 exit 1
# LINT: missing "&&" on "fi" # LINT: missing "&&" on "fi"

View File

@ -0,0 +1,15 @@
git init r1 &&
for n in 1 2 3 4 5
do
echo "This is file: $n" > r1/file.$n &&
git -C r1 add file.$n &&
git -C r1 commit -m "$n" || return 1
done &&
git init r2 &&
for n in 1000 10000
do
printf "%"$n"s" X > r2/large.$n &&
git -C r2 add large.$n &&
git -C r2 commit -m "$n" ?!LOOP?!
done

View File

@ -0,0 +1,17 @@
git init r1 &&
# LINT: loop handles failure explicitly with "|| return 1"
for n in 1 2 3 4 5
do
echo "This is file: $n" > r1/file.$n &&
git -C r1 add file.$n &&
git -C r1 commit -m "$n" || return 1
done &&
git init r2 &&
# LINT: loop fails to handle failure explicitly with "|| return 1"
for n in 1000 10000
do
printf "%"$n"s" X > r2/large.$n &&
git -C r2 add large.$n &&
git -C r2 commit -m "$n"
done

View File

@ -0,0 +1,18 @@
( while test $i -le $blobcount
do
printf "Generating blob $i/$blobcount\r" >& 2 &&
printf "blob\nmark :$i\ndata $blobsize\n" &&
printf "%-${blobsize}s" $i &&
echo "M 100644 :$i $i" >> commit &&
i=$(($i+1)) ||
echo $? > exit-status
done &&
echo "commit refs/heads/main" &&
echo "author A U Thor <author@email.com> 123456789 +0000" &&
echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
echo "data 5" &&
echo ">2gb" &&
cat commit ) |
git fast-import --big-file-threshold=2 &&
test ! -f exit-status

View File

@ -0,0 +1,19 @@
# LINT: "$?" handled explicitly within loop body
(while test $i -le $blobcount
do
printf "Generating blob $i/$blobcount\r" >&2 &&
printf "blob\nmark :$i\ndata $blobsize\n" &&
#test-tool genrandom $i $blobsize &&
printf "%-${blobsize}s" $i &&
echo "M 100644 :$i $i" >> commit &&
i=$(($i+1)) ||
echo $? > exit-status
done &&
echo "commit refs/heads/main" &&
echo "author A U Thor <author@email.com> 123456789 +0000" &&
echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
echo "data 5" &&
echo ">2gb" &&
cat commit) |
git fast-import --big-file-threshold=2 &&
test ! -f exit-status

View File

@ -4,7 +4,7 @@
while true while true
do do
echo "pop" ?!AMP?! echo "pop" ?!AMP?!
echo "glup" echo "glup" ?!LOOP?!
done ?!AMP?! done ?!AMP?!
foo foo
fi ?!AMP?! fi ?!AMP?!

View File

@ -0,0 +1,10 @@
(
git rev-list --objects --no-object-names base..loose |
while read oid
do
path="$objdir/$(test_oid_to_path "$oid")" &&
printf "%s %d\n" "$oid" "$(test-tool chmtime --get "$path")" ||
echo "object list generation failed for $oid"
done |
sort -k1
) >expect &&

View File

@ -0,0 +1,11 @@
(
git rev-list --objects --no-object-names base..loose |
while read oid
do
# LINT: "|| echo" signals failure in loop upstream of a pipe
path="$objdir/$(test_oid_to_path "$oid")" &&
printf "%s %d\n" "$oid" "$(test-tool chmtime --get "$path")" ||
echo "object list generation failed for $oid"
done |
sort -k1
) >expect &&

View File

@ -1,9 +1,14 @@
( (
x="line 1 line 2 line 3" && x="line 1
y="line 1 line2" ?!AMP?! line 2
line 3" &&
y="line 1
line2" ?!AMP?!
foobar foobar
) && ) &&
( (
echo "xyz" "abc def ghi" && echo "xyz" "abc
def
ghi" &&
barfoo barfoo
) )

View File

@ -0,0 +1,31 @@
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" ?!LOOP?!
done ?!LOOP?!
done &&
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" || return 1
done
done &&
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" ?!LOOP?!
done || return 1
done &&
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" || return 1
done || return 1
done

View File

@ -0,0 +1,35 @@
# LINT: neither loop handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j"
done
done &&
# LINT: inner loop handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j" || return 1
done
done &&
# LINT: outer loop handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j"
done || return 1
done &&
# LINT: inner & outer loops handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j" || return 1
done || return 1
done

View File

@ -6,7 +6,7 @@
) >file && ) >file &&
cd foo && cd foo &&
( (
echo a echo a ?!AMP?!
echo b echo b
) >file ) >file
) )

View File

@ -0,0 +1,9 @@
git init dir-rename-and-content &&
(
cd dir-rename-and-content &&
test_write_lines 1 2 3 4 5 >foo &&
mkdir olddir &&
for i in a b c; do echo $i >olddir/$i; ?!LOOP?! done ?!AMP?!
git add foo olddir &&
git commit -m "original" &&
)

View File

@ -0,0 +1,10 @@
git init dir-rename-and-content &&
(
cd dir-rename-and-content &&
test_write_lines 1 2 3 4 5 >foo &&
mkdir olddir &&
# LINT: one-liner for-loop missing "|| exit"; also broken &&-chain
for i in a b c; do echo $i >olddir/$i; done
git add foo olddir &&
git commit -m "original" &&
)

View File

@ -0,0 +1,5 @@
while test $i -lt $((num - 5))
do
git notes add -m "notes for commit$i" HEAD~$i || return 1
i=$((i + 1))
done

View File

@ -0,0 +1,6 @@
while test $i -lt $((num - 5))
do
# LINT: "|| return {n}" valid loop escape outside subshell; no "&&" needed
git notes add -m "notes for commit$i" HEAD~$i || return 1
i=$((i + 1))
done

View File

@ -15,5 +15,5 @@
) && ) &&
(cd foo && (cd foo &&
for i in a b c; do for i in a b c; do
echo; echo; ?!LOOP?!
done) done)

View File

@ -0,0 +1,4 @@
perl -e '
defined($_ = -s $_) or die for @ARGV;
exit 1 if $ARGV[0] <= $ARGV[1];
' test-2-$packname_2.pack test-3-$packname_3.pack

View File

@ -0,0 +1,5 @@
# LINT: SQ-string Perl code fragment within SQ-string
perl -e '\''
defined($_ = -s $_) or die for @ARGV;
exit 1 if $ARGV[0] <= $ARGV[1];
'\'' test-2-$packname_2.pack test-3-$packname_3.pack

View File

@ -1,10 +1,17 @@
( (
chks="sub1sub2sub3sub4" && chks="sub1
sub2
sub3
sub4" &&
chks_sub=$(cat <<TXT | sed "s,^,sub dir/," chks_sub=$(cat <<TXT | sed "s,^,sub dir/,"
) && ) &&
chkms="main-sub1main-sub2main-sub3main-sub4" && chkms="main-sub1
main-sub2
main-sub3
main-sub4" &&
chkms_sub=$(cat <<TXT | sed "s,^,sub dir/," chkms_sub=$(cat <<TXT | sed "s,^,sub dir/,"
) && ) &&
subfiles=$(git ls-files) && subfiles=$(git ls-files) &&
check_equal "$subfiles" "$chkms$chks" check_equal "$subfiles" "$chkms
$chks"
) )

View File

@ -0,0 +1,27 @@
git config filter.rot13.smudge ./rot13.sh &&
git config filter.rot13.clean ./rot13.sh &&
{
echo "*.t filter=rot13" ?!AMP?!
echo "*.i ident"
} > .gitattributes &&
{
echo a b c d e f g h i j k l m ?!AMP?!
echo n o p q r s t u v w x y z ?!AMP?!
echo '$Id$'
} > test &&
cat test > test.t &&
cat test > test.o &&
cat test > test.i &&
git add test test.t test.i &&
rm -f test test.t test.i &&
git checkout -- test test.t test.i &&
echo "content-test2" > test2.o &&
echo "content-test3 - filename with special characters" > "test3 'sq',$x=.o" ?!AMP?!
downstream_url_for_sed=$(
printf "%sn" "$downstream_url" |
sed -e 's/\/\\/g' -e 's/[[/.*^$]/\&/g'
)

View File

@ -0,0 +1,32 @@
# LINT: single token; composite of multiple strings
git config filter.rot13.smudge ./rot13.sh &&
git config filter.rot13.clean ./rot13.sh &&
{
echo "*.t filter=rot13"
echo "*.i ident"
} >.gitattributes &&
{
echo a b c d e f g h i j k l m
echo n o p q r s t u v w x y z
# LINT: exit/enter string context and escaped-quote outside of string
echo '\''$Id$'\''
} >test &&
cat test >test.t &&
cat test >test.o &&
cat test >test.i &&
git add test test.t test.i &&
rm -f test test.t test.i &&
git checkout -- test test.t test.i &&
echo "content-test2" >test2.o &&
# LINT: exit/enter string context and escaped-quote outside of string
echo "content-test3 - filename with special characters" >"test3 '\''sq'\'',\$x=.o"
# LINT: single token; composite of multiple strings
downstream_url_for_sed=$(
printf "%s\n" "$downstream_url" |
# LINT: exit/enter string context; "&" inside string not command terminator
sed -e '\''s/\\/\\\\/g'\'' -e '\''s/[[/.*^$]/\\&/g'\''
)

View File

@ -2,10 +2,10 @@
while true while true
do do
echo foo ?!AMP?! echo foo ?!AMP?!
cat <<-EOF cat <<-EOF ?!LOOP?!
done ?!AMP?! done ?!AMP?!
while true; do while true; do
echo foo && echo foo &&
cat bar cat bar ?!LOOP?!
done done
) )

View File

@ -387,9 +387,7 @@ test_expect_success 'setup main' '
test_tick test_tick
' '
# Disable extra chain-linting for the next set of tests. There are many
# auto-generated ones that are not worth checking over and over.
GIT_TEST_CHAIN_LINT_HARDER_DEFAULT=0
warn_LF_CRLF="LF will be replaced by CRLF" warn_LF_CRLF="LF will be replaced by CRLF"
warn_CRLF_LF="CRLF will be replaced by LF" warn_CRLF_LF="CRLF will be replaced by LF"
@ -606,9 +604,6 @@ do
checkout_files "" "$id" "crlf" true "" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul checkout_files "" "$id" "crlf" true "" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul
done done
# The rest of the tests are unique; do the usual linting.
unset GIT_TEST_CHAIN_LINT_HARDER_DEFAULT
# Should be the last test case: remove some files from the worktree # Should be the last test case: remove some files from the worktree
test_expect_success 'ls-files --eol -d -z' ' test_expect_success 'ls-files --eol -d -z' '
rm crlf_false_attr__CRLF.txt crlf_false_attr__CRLF_mix_LF.txt crlf_false_attr__LF.txt .gitattributes && rm crlf_false_attr__CRLF.txt crlf_false_attr__CRLF_mix_LF.txt crlf_false_attr__LF.txt .gitattributes &&

View File

@ -5,11 +5,6 @@ test_description='wildmatch tests'
TEST_PASSES_SANITIZE_LEAK=true TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh . ./test-lib.sh
# Disable expensive chain-lint tests; all of the tests in this script
# are variants of a few trivial test-tool invocations, and there are a lot of
# them.
GIT_TEST_CHAIN_LINT_HARDER_DEFAULT=0
should_create_test_file() { should_create_test_file() {
file=$1 file=$1

View File

@ -1091,11 +1091,7 @@ test_run_ () {
trace= trace=
# 117 is magic because it is unlikely to match the exit # 117 is magic because it is unlikely to match the exit
# code of other programs # code of other programs
if test "OK-117" != "$(test_eval_ "(exit 117) && $1${LF}${LF}echo OK-\$?" 3>&1)" || if test "OK-117" != "$(test_eval_ "(exit 117) && $1${LF}${LF}echo OK-\$?" 3>&1)"
{
test "${GIT_TEST_CHAIN_LINT_HARDER:-${GIT_TEST_CHAIN_LINT_HARDER_DEFAULT:-1}}" != 0 &&
$(printf '%s\n' "$1" | sed -f "$GIT_BUILD_DIR/t/chainlint.sed" | grep -q '?![A-Z][A-Z]*?!')
}
then then
BUG "broken &&-chain or run-away HERE-DOC: $1" BUG "broken &&-chain or run-away HERE-DOC: $1"
fi fi
@ -1591,6 +1587,12 @@ then
BAIL_OUT_ENV_NEEDS_SANITIZE_LEAK "GIT_TEST_SANITIZE_LEAK_LOG=true" BAIL_OUT_ENV_NEEDS_SANITIZE_LEAK "GIT_TEST_SANITIZE_LEAK_LOG=true"
fi fi
if test "${GIT_TEST_CHAIN_LINT:-1}" != 0
then
"$PERL_PATH" "$TEST_DIRECTORY/chainlint.pl" "$0" ||
BUG "lint error (see '?!...!? annotations above)"
fi
# Last-minute variable setup # Last-minute variable setup
USER_HOME="$HOME" USER_HOME="$HOME"
HOME="$TRASH_DIRECTORY" HOME="$TRASH_DIRECTORY"