mirror of
https://github.com/git/git
synced 2024-08-24 18:26:02 +00:00
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:
commit
9d58241ee4
|
@ -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"
|
||||
"file(WRITE ${CMAKE_SOURCE_DIR}/t/test-lib.sh \${content})")
|
||||
#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}/mergetools/tkdiff DESTINATION ${CMAKE_BINARY_DIR}/mergetools/)
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/contrib/completion/git-prompt.sh DESTINATION ${CMAKE_BINARY_DIR}/contrib/completion/)
|
||||
|
|
49
t/Makefile
49
t/Makefile
|
@ -36,14 +36,21 @@ CHAINLINTTMP_SQ = $(subst ','\'',$(CHAINLINTTMP))
|
|||
|
||||
T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.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))
|
||||
TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))
|
||||
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)
|
||||
|
||||
test: pre-clean check-chainlint $(TEST_LINT)
|
||||
$(MAKE) aggregate-results-and-cleanup
|
||||
$(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup
|
||||
|
||||
failed:
|
||||
@failed=$$(cd '$(TEST_RESULTS_DIRECTORY_SQ)' && \
|
||||
|
@ -52,7 +59,7 @@ failed:
|
|||
test -z "$$failed" || $(MAKE) $$failed
|
||||
|
||||
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
|
||||
|
||||
$(T):
|
||||
|
@ -73,13 +80,35 @@ clean-chainlint:
|
|||
|
||||
check-chainlint:
|
||||
@mkdir -p '$(CHAINLINTTMP_SQ)' && \
|
||||
sed -e '/^# LINT: /d' $(patsubst %,chainlint/%.test,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/tests && \
|
||||
sed -e '/^[ ]*$$/d' $(patsubst %,chainlint/%.expect,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/expect && \
|
||||
$(CHAINLINT) '$(CHAINLINTTMP_SQ)'/tests | grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \
|
||||
diff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual
|
||||
for i in $(CHAINLINTTESTS); do \
|
||||
echo "test_expect_success '$$i' '" && \
|
||||
sed -e '/^# LINT: /d' chainlint/$$i.test && \
|
||||
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-filenames
|
||||
ifneq ($(GIT_TEST_CHAIN_LINT),0)
|
||||
test-lint: test-chainlint
|
||||
endif
|
||||
|
||||
test-lint-duplicates:
|
||||
@dups=`echo $(T) $(TPERF) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \
|
||||
|
@ -102,6 +131,9 @@ test-lint-filenames:
|
|||
test -z "$$bad" || { \
|
||||
echo >&2 "non-portable file name(s): $$bad"; exit 1; }
|
||||
|
||||
test-chainlint:
|
||||
@$(CHAINLINT) $(T) $(TLIBS) $(TPERF) $(TINTEROP)
|
||||
|
||||
aggregate-results-and-cleanup: $(T)
|
||||
$(MAKE) aggregate-results
|
||||
$(MAKE) clean
|
||||
|
@ -117,4 +149,5 @@ valgrind:
|
|||
perf:
|
||||
$(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
|
||||
|
|
5
t/README
5
t/README
|
@ -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
|
||||
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::
|
||||
Run the test script repeatedly in multiple parallel jobs until
|
||||
one of them fails. Useful for reproducing rare failures in
|
||||
|
|
770
t/chainlint.pl
Executable file
770
t/chainlint.pl
Executable 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));
|
399
t/chainlint.sed
399
t/chainlint.sed
|
@ -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
|
18
t/chainlint/blank-line-before-esac.expect
Normal file
18
t/chainlint/blank-line-before-esac.expect
Normal 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
|
||||
}
|
19
t/chainlint/blank-line-before-esac.test
Normal file
19
t/chainlint/blank-line-before-esac.test
Normal 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
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
(
|
||||
foo &&
|
||||
{
|
||||
echo a
|
||||
echo a ?!AMP?!
|
||||
echo b
|
||||
} &&
|
||||
bar &&
|
||||
|
@ -9,4 +9,15 @@
|
|||
echo c
|
||||
} ?!AMP?!
|
||||
baz
|
||||
)
|
||||
) &&
|
||||
|
||||
{
|
||||
echo a ; ?!AMP?! echo b
|
||||
} &&
|
||||
{ echo a ; ?!AMP?! echo b ; } &&
|
||||
|
||||
{
|
||||
echo "${var}9" &&
|
||||
echo "done"
|
||||
} &&
|
||||
finis
|
||||
|
|
|
@ -11,4 +11,17 @@
|
|||
echo c
|
||||
}
|
||||
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
|
||||
|
|
9
t/chainlint/chain-break-background.expect
Normal file
9
t/chainlint/chain-break-background.expect
Normal 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
|
10
t/chainlint/chain-break-background.test
Normal file
10
t/chainlint/chain-break-background.test
Normal 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
|
12
t/chainlint/chain-break-continue.expect
Normal file
12
t/chainlint/chain-break-continue.expect
Normal 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
|
13
t/chainlint/chain-break-continue.test
Normal file
13
t/chainlint/chain-break-continue.test
Normal 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
|
9
t/chainlint/chain-break-false.expect
Normal file
9
t/chainlint/chain-break-false.expect
Normal 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
|
10
t/chainlint/chain-break-false.test
Normal file
10
t/chainlint/chain-break-false.test
Normal 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
|
19
t/chainlint/chain-break-return-exit.expect
Normal file
19
t/chainlint/chain-break-return-exit.expect
Normal 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
|
23
t/chainlint/chain-break-return-exit.test
Normal file
23
t/chainlint/chain-break-return-exit.test
Normal 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
|
9
t/chainlint/chain-break-status.expect
Normal file
9
t/chainlint/chain-break-status.expect
Normal 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
|
11
t/chainlint/chain-break-status.test
Normal file
11
t/chainlint/chain-break-status.test
Normal 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
|
9
t/chainlint/chained-block.expect
Normal file
9
t/chainlint/chained-block.expect
Normal 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"
|
||||
}
|
11
t/chainlint/chained-block.test
Normal file
11
t/chainlint/chained-block.test
Normal 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"
|
||||
}
|
10
t/chainlint/chained-subshell.expect
Normal file
10
t/chainlint/chained-subshell.expect
Normal 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 )
|
13
t/chainlint/chained-subshell.test
Normal file
13
t/chainlint/chained-subshell.test
Normal 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)
|
2
t/chainlint/command-substitution-subsubshell.expect
Normal file
2
t/chainlint/command-substitution-subsubshell.expect
Normal file
|
@ -0,0 +1,2 @@
|
|||
OUT=$(( ( large_git 1 >& 3 ) | : ) 3 >& 1) &&
|
||||
test_match_signal 13 "$OUT"
|
3
t/chainlint/command-substitution-subsubshell.test
Normal file
3
t/chainlint/command-substitution-subsubshell.test
Normal 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"
|
|
@ -4,6 +4,6 @@
|
|||
:
|
||||
else
|
||||
echo >file
|
||||
fi
|
||||
fi ?!LOOP?!
|
||||
done) &&
|
||||
test ! -f file
|
||||
|
|
2
t/chainlint/double-here-doc.expect
Normal file
2
t/chainlint/double-here-doc.expect
Normal 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
|
12
t/chainlint/double-here-doc.test
Normal file
12
t/chainlint/double-here-doc.test
Normal 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
|
3
t/chainlint/dqstring-line-splice.expect
Normal file
3
t/chainlint/dqstring-line-splice.expect
Normal 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
|
7
t/chainlint/dqstring-line-splice.test
Normal file
7
t/chainlint/dqstring-line-splice.test
Normal 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
|
||||
"'
|
11
t/chainlint/dqstring-no-interpolate.expect
Normal file
11
t/chainlint/dqstring-no-interpolate.expect
Normal 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
|
15
t/chainlint/dqstring-no-interpolate.test
Normal file
15
t/chainlint/dqstring-no-interpolate.test
Normal 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
|
||||
"'
|
3
t/chainlint/empty-here-doc.expect
Normal file
3
t/chainlint/empty-here-doc.expect
Normal file
|
@ -0,0 +1,3 @@
|
|||
git ls-tree $tree path > current &&
|
||||
cat > expected <<EOF &&
|
||||
test_output
|
5
t/chainlint/empty-here-doc.test
Normal file
5
t/chainlint/empty-here-doc.test
Normal file
|
@ -0,0 +1,5 @@
|
|||
git ls-tree $tree path >current &&
|
||||
# LINT: empty here-doc
|
||||
cat >expected <<\EOF &&
|
||||
EOF
|
||||
test_output
|
4
t/chainlint/exclamation.expect
Normal file
4
t/chainlint/exclamation.expect
Normal file
|
@ -0,0 +1,4 @@
|
|||
if ! condition ; then echo nope ; else yep ; fi &&
|
||||
test_prerequisite !MINGW &&
|
||||
mail uucp!address &&
|
||||
echo !whatever!
|
8
t/chainlint/exclamation.test
Normal file
8
t/chainlint/exclamation.test
Normal 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!
|
5
t/chainlint/for-loop-abbreviated.expect
Normal file
5
t/chainlint/for-loop-abbreviated.expect
Normal file
|
@ -0,0 +1,5 @@
|
|||
for it
|
||||
do
|
||||
path=$(expr "$it" : ( [^:]*) ) &&
|
||||
git update-index --add "$path" || exit
|
||||
done
|
6
t/chainlint/for-loop-abbreviated.test
Normal file
6
t/chainlint/for-loop-abbreviated.test
Normal 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
|
|
@ -2,10 +2,10 @@
|
|||
for i in a b c
|
||||
do
|
||||
echo $i ?!AMP?!
|
||||
cat <<-EOF
|
||||
cat <<-EOF ?!LOOP?!
|
||||
done ?!AMP?!
|
||||
for i in a b c; do
|
||||
echo $i &&
|
||||
cat $i
|
||||
cat $i ?!LOOP?!
|
||||
done
|
||||
)
|
||||
|
|
11
t/chainlint/function.expect
Normal file
11
t/chainlint/function.expect
Normal 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
13
t/chainlint/function.test
Normal 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
|
5
t/chainlint/here-doc-indent-operator.expect
Normal file
5
t/chainlint/here-doc-indent-operator.expect
Normal file
|
@ -0,0 +1,5 @@
|
|||
cat > expect <<-EOF &&
|
||||
|
||||
cat > expect <<-EOF ?!AMP?!
|
||||
|
||||
cleanup
|
13
t/chainlint/here-doc-indent-operator.test
Normal file
13
t/chainlint/here-doc-indent-operator.test
Normal 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
|
|
@ -1,4 +1,5 @@
|
|||
(
|
||||
cat <<-TXT && echo "multi-line string" ?!AMP?!
|
||||
cat <<-TXT && echo "multi-line
|
||||
string" ?!AMP?!
|
||||
bap
|
||||
)
|
||||
|
|
7
t/chainlint/if-condition-split.expect
Normal file
7
t/chainlint/if-condition-split.expect
Normal file
|
@ -0,0 +1,7 @@
|
|||
if bob &&
|
||||
marcia ||
|
||||
kevin
|
||||
then
|
||||
echo "nomads" ?!AMP?!
|
||||
echo "for sure"
|
||||
fi
|
8
t/chainlint/if-condition-split.test
Normal file
8
t/chainlint/if-condition-split.test
Normal 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
|
|
@ -3,7 +3,7 @@
|
|||
do
|
||||
if false
|
||||
then
|
||||
echo "err" ?!AMP?!
|
||||
echo "err"
|
||||
exit 1
|
||||
fi ?!AMP?!
|
||||
foo
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
do
|
||||
if false
|
||||
then
|
||||
# LINT: missing "&&" on "echo"
|
||||
# LINT: missing "&&" on "echo" okay since "exit 1" signals error explicitly
|
||||
echo "err"
|
||||
exit 1
|
||||
# LINT: missing "&&" on "fi"
|
||||
|
|
15
t/chainlint/loop-detect-failure.expect
Normal file
15
t/chainlint/loop-detect-failure.expect
Normal 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
|
17
t/chainlint/loop-detect-failure.test
Normal file
17
t/chainlint/loop-detect-failure.test
Normal 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
|
18
t/chainlint/loop-detect-status.expect
Normal file
18
t/chainlint/loop-detect-status.expect
Normal 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
|
19
t/chainlint/loop-detect-status.test
Normal file
19
t/chainlint/loop-detect-status.test
Normal 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
|
|
@ -4,7 +4,7 @@
|
|||
while true
|
||||
do
|
||||
echo "pop" ?!AMP?!
|
||||
echo "glup"
|
||||
echo "glup" ?!LOOP?!
|
||||
done ?!AMP?!
|
||||
foo
|
||||
fi ?!AMP?!
|
||||
|
|
10
t/chainlint/loop-upstream-pipe.expect
Normal file
10
t/chainlint/loop-upstream-pipe.expect
Normal 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 &&
|
11
t/chainlint/loop-upstream-pipe.test
Normal file
11
t/chainlint/loop-upstream-pipe.test
Normal 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 &&
|
|
@ -1,9 +1,14 @@
|
|||
(
|
||||
x="line 1 line 2 line 3" &&
|
||||
y="line 1 line2" ?!AMP?!
|
||||
x="line 1
|
||||
line 2
|
||||
line 3" &&
|
||||
y="line 1
|
||||
line2" ?!AMP?!
|
||||
foobar
|
||||
) &&
|
||||
(
|
||||
echo "xyz" "abc def ghi" &&
|
||||
echo "xyz" "abc
|
||||
def
|
||||
ghi" &&
|
||||
barfoo
|
||||
)
|
||||
|
|
31
t/chainlint/nested-loop-detect-failure.expect
Normal file
31
t/chainlint/nested-loop-detect-failure.expect
Normal 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
|
35
t/chainlint/nested-loop-detect-failure.test
Normal file
35
t/chainlint/nested-loop-detect-failure.test
Normal 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
|
|
@ -6,7 +6,7 @@
|
|||
) >file &&
|
||||
cd foo &&
|
||||
(
|
||||
echo a
|
||||
echo a ?!AMP?!
|
||||
echo b
|
||||
) >file
|
||||
)
|
||||
|
|
9
t/chainlint/one-liner-for-loop.expect
Normal file
9
t/chainlint/one-liner-for-loop.expect
Normal 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" &&
|
||||
)
|
10
t/chainlint/one-liner-for-loop.test
Normal file
10
t/chainlint/one-liner-for-loop.test
Normal 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" &&
|
||||
)
|
5
t/chainlint/return-loop.expect
Normal file
5
t/chainlint/return-loop.expect
Normal 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
|
6
t/chainlint/return-loop.test
Normal file
6
t/chainlint/return-loop.test
Normal 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
|
|
@ -15,5 +15,5 @@
|
|||
) &&
|
||||
(cd foo &&
|
||||
for i in a b c; do
|
||||
echo;
|
||||
echo; ?!LOOP?!
|
||||
done)
|
||||
|
|
4
t/chainlint/sqstring-in-sqstring.expect
Normal file
4
t/chainlint/sqstring-in-sqstring.expect
Normal 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
|
5
t/chainlint/sqstring-in-sqstring.test
Normal file
5
t/chainlint/sqstring-in-sqstring.test
Normal 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
|
|
@ -1,10 +1,17 @@
|
|||
(
|
||||
chks="sub1sub2sub3sub4" &&
|
||||
chks="sub1
|
||||
sub2
|
||||
sub3
|
||||
sub4" &&
|
||||
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/,"
|
||||
) &&
|
||||
subfiles=$(git ls-files) &&
|
||||
check_equal "$subfiles" "$chkms$chks"
|
||||
check_equal "$subfiles" "$chkms
|
||||
$chks"
|
||||
)
|
||||
|
|
27
t/chainlint/token-pasting.expect
Normal file
27
t/chainlint/token-pasting.expect
Normal 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'
|
||||
)
|
32
t/chainlint/token-pasting.test
Normal file
32
t/chainlint/token-pasting.test
Normal 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'\''
|
||||
)
|
|
@ -2,10 +2,10 @@
|
|||
while true
|
||||
do
|
||||
echo foo ?!AMP?!
|
||||
cat <<-EOF
|
||||
cat <<-EOF ?!LOOP?!
|
||||
done ?!AMP?!
|
||||
while true; do
|
||||
echo foo &&
|
||||
cat bar
|
||||
cat bar ?!LOOP?!
|
||||
done
|
||||
)
|
||||
|
|
|
@ -387,9 +387,7 @@ test_expect_success 'setup main' '
|
|||
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_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
|
||||
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
|
||||
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 &&
|
||||
|
|
|
@ -5,11 +5,6 @@ test_description='wildmatch tests'
|
|||
TEST_PASSES_SANITIZE_LEAK=true
|
||||
. ./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() {
|
||||
file=$1
|
||||
|
||||
|
|
|
@ -1091,11 +1091,7 @@ test_run_ () {
|
|||
trace=
|
||||
# 117 is magic because it is unlikely to match the exit
|
||||
# code of other programs
|
||||
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]*?!')
|
||||
}
|
||||
if test "OK-117" != "$(test_eval_ "(exit 117) && $1${LF}${LF}echo OK-\$?" 3>&1)"
|
||||
then
|
||||
BUG "broken &&-chain or run-away HERE-DOC: $1"
|
||||
fi
|
||||
|
@ -1591,6 +1587,12 @@ then
|
|||
BAIL_OUT_ENV_NEEDS_SANITIZE_LEAK "GIT_TEST_SANITIZE_LEAK_LOG=true"
|
||||
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
|
||||
USER_HOME="$HOME"
|
||||
HOME="$TRASH_DIRECTORY"
|
||||
|
|
Loading…
Reference in a new issue