Git fork

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

+1479 -449
+1 -1
contrib/buildsystems/CMakeLists.txt
··· 1076 1076 "string(REPLACE \"\${GIT_BUILD_DIR_REPL}\" \"GIT_BUILD_DIR=\\\"$TEST_DIRECTORY/../${BUILD_DIR_RELATIVE}\\\"\" content \"\${content}\")\n" 1077 1077 "file(WRITE ${CMAKE_SOURCE_DIR}/t/test-lib.sh \${content})") 1078 1078 #misc copies 1079 - file(COPY ${CMAKE_SOURCE_DIR}/t/chainlint.sed DESTINATION ${CMAKE_BINARY_DIR}/t/) 1079 + file(COPY ${CMAKE_SOURCE_DIR}/t/chainlint.pl DESTINATION ${CMAKE_BINARY_DIR}/t/) 1080 1080 file(COPY ${CMAKE_SOURCE_DIR}/po/is.po DESTINATION ${CMAKE_BINARY_DIR}/po/) 1081 1081 file(COPY ${CMAKE_SOURCE_DIR}/mergetools/tkdiff DESTINATION ${CMAKE_BINARY_DIR}/mergetools/) 1082 1082 file(COPY ${CMAKE_SOURCE_DIR}/contrib/completion/git-prompt.sh DESTINATION ${CMAKE_BINARY_DIR}/contrib/completion/)
+41 -8
t/Makefile
··· 36 36 37 37 T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) 38 38 THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh))) 39 + TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh 39 40 TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh)) 41 + TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh)) 40 42 CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test))) 41 - CHAINLINT = sed -f chainlint.sed 43 + CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl 44 + 45 + # `test-chainlint` (which is a dependency of `test-lint`, `test` and `prove`) 46 + # checks all tests in all scripts via a single invocation, so tell individual 47 + # scripts not to "chainlint" themselves 48 + CHAINLINTSUPPRESS = GIT_TEST_CHAIN_LINT=0 && export GIT_TEST_CHAIN_LINT && 42 49 43 50 all: $(DEFAULT_TEST_TARGET) 44 51 45 52 test: pre-clean check-chainlint $(TEST_LINT) 46 - $(MAKE) aggregate-results-and-cleanup 53 + $(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup 47 54 48 55 failed: 49 56 @failed=$$(cd '$(TEST_RESULTS_DIRECTORY_SQ)' && \ ··· 52 59 test -z "$$failed" || $(MAKE) $$failed 53 60 54 61 prove: pre-clean check-chainlint $(TEST_LINT) 55 - @echo "*** prove ***"; $(PROVE) --exec '$(TEST_SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) 62 + @echo "*** prove ***"; $(CHAINLINTSUPPRESS) $(PROVE) --exec '$(TEST_SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) 56 63 $(MAKE) clean-except-prove-cache 57 64 58 65 $(T): ··· 73 80 74 81 check-chainlint: 75 82 @mkdir -p '$(CHAINLINTTMP_SQ)' && \ 76 - sed -e '/^# LINT: /d' $(patsubst %,chainlint/%.test,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/tests && \ 77 - sed -e '/^[ ]*$$/d' $(patsubst %,chainlint/%.expect,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/expect && \ 78 - $(CHAINLINT) '$(CHAINLINTTMP_SQ)'/tests | grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \ 79 - diff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual 83 + for i in $(CHAINLINTTESTS); do \ 84 + echo "test_expect_success '$$i' '" && \ 85 + sed -e '/^# LINT: /d' chainlint/$$i.test && \ 86 + echo "'"; \ 87 + done >'$(CHAINLINTTMP_SQ)'/tests && \ 88 + { \ 89 + echo "# chainlint: $(CHAINLINTTMP_SQ)/tests" && \ 90 + for i in $(CHAINLINTTESTS); do \ 91 + echo "# chainlint: $$i" && \ 92 + sed -e '/^[ ]*$$/d' chainlint/$$i.expect; \ 93 + done \ 94 + } >'$(CHAINLINTTMP_SQ)'/expect && \ 95 + $(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests | \ 96 + grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \ 97 + if test -f ../GIT-BUILD-OPTIONS; then \ 98 + . ../GIT-BUILD-OPTIONS; \ 99 + fi && \ 100 + if test -x ../git$$X; then \ 101 + DIFFW="../git$$X --no-pager diff -w --no-index"; \ 102 + else \ 103 + DIFFW="diff -w -u"; \ 104 + fi && \ 105 + $$DIFFW '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual 80 106 81 107 test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax \ 82 108 test-lint-filenames 109 + ifneq ($(GIT_TEST_CHAIN_LINT),0) 110 + test-lint: test-chainlint 111 + endif 83 112 84 113 test-lint-duplicates: 85 114 @dups=`echo $(T) $(TPERF) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \ ··· 102 131 test -z "$$bad" || { \ 103 132 echo >&2 "non-portable file name(s): $$bad"; exit 1; } 104 133 134 + test-chainlint: 135 + @$(CHAINLINT) $(T) $(TLIBS) $(TPERF) $(TINTEROP) 136 + 105 137 aggregate-results-and-cleanup: $(T) 106 138 $(MAKE) aggregate-results 107 139 $(MAKE) clean ··· 117 149 perf: 118 150 $(MAKE) -C perf/ all 119 151 120 - .PHONY: pre-clean $(T) aggregate-results clean valgrind perf check-chainlint clean-chainlint 152 + .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \ 153 + check-chainlint clean-chainlint test-chainlint
-5
t/README
··· 196 196 this feature by setting the GIT_TEST_CHAIN_LINT environment 197 197 variable to "1" or "0", respectively. 198 198 199 - A few test scripts disable some of the more advanced 200 - chain-linting detection in the name of efficiency. You can 201 - override this by setting the GIT_TEST_CHAIN_LINT_HARDER 202 - environment variable to "1". 203 - 204 199 --stress:: 205 200 Run the test script repeatedly in multiple parallel jobs until 206 201 one of them fails. Useful for reproducing rare failures in
+770
t/chainlint.pl
··· 1 + #!/usr/bin/env perl 2 + # 3 + # Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com> 4 + # 5 + # This tool scans shell scripts for test definitions and checks those tests for 6 + # problems, such as broken &&-chains, which might hide bugs in the tests 7 + # themselves or in behaviors being exercised by the tests. 8 + # 9 + # Input arguments are pathnames of shell scripts containing test definitions, 10 + # or globs referencing a collection of scripts. For each problem discovered, 11 + # the pathname of the script containing the test is printed along with the test 12 + # name and the test body with a `?!FOO?!` annotation at the location of each 13 + # detected problem, where "FOO" is a tag such as "AMP" which indicates a broken 14 + # &&-chain. Returns zero if no problems are discovered, otherwise non-zero. 15 + 16 + use warnings; 17 + use strict; 18 + use Config; 19 + use File::Glob; 20 + use Getopt::Long; 21 + 22 + my $jobs = -1; 23 + my $show_stats; 24 + my $emit_all; 25 + 26 + # Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3 27 + # "Token Recognition" of POSIX chapter 2 "Shell Command Language". Although 28 + # similar to lexical analyzers for other languages, this one differs in a few 29 + # substantial ways due to quirks of the shell command language. 30 + # 31 + # For instance, in many languages, newline is just whitespace like space or 32 + # TAB, but in shell a newline is a command separator, thus a distinct lexical 33 + # token. A newline is significant and returned as a distinct token even at the 34 + # end of a shell comment. 35 + # 36 + # In other languages, `1+2` would typically be scanned as three tokens 37 + # (`1`, `+`, and `2`), but in shell it is a single token. However, the similar 38 + # `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well. 39 + # In shell, several characters with special meaning lose that meaning when not 40 + # surrounded by whitespace. For instance, the negation operator `!` is special 41 + # when standing alone surrounded by whitespace; whereas in `foo!uucp` it is 42 + # just a plain character in the longer token "foo!uucp". In many other 43 + # languages, `"string"/foo:'string'` might be scanned as five tokens ("string", 44 + # `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token. 45 + # 46 + # The lexical analyzer for the shell command language is also somewhat unusual 47 + # in that it recursively invokes the parser to handle the body of `$(...)` 48 + # expressions which can contain arbitrary shell code. Such expressions may be 49 + # encountered both inside and outside of double-quoted strings. 50 + # 51 + # The lexical analyzer is responsible for consuming shell here-doc bodies which 52 + # extend from the line following a `<<TAG` operator until a line consisting 53 + # solely of `TAG`. Here-doc consumption begins when a newline is encountered. 54 + # It is legal for multiple here-doc `<<TAG` operators to be present on a single 55 + # line, in which case their bodies must be present one following the next, and 56 + # are consumed in the (left-to-right) order the `<<TAG` operators appear on the 57 + # line. A special complication is that the bodies of all here-docs must be 58 + # consumed when the newline is encountered even if the parse context depth has 59 + # changed. For instance, in `cat <<A && x=$(cat <<B &&\n`, bodies of here-docs 60 + # "A" and "B" must be consumed even though "A" was introduced outside the 61 + # recursive parse context in which "B" was introduced and in which the newline 62 + # is encountered. 63 + package Lexer; 64 + 65 + sub new { 66 + my ($class, $parser, $s) = @_; 67 + bless { 68 + parser => $parser, 69 + buff => $s, 70 + heretags => [] 71 + } => $class; 72 + } 73 + 74 + sub scan_heredoc_tag { 75 + my $self = shift @_; 76 + ${$self->{buff}} =~ /\G(-?)/gc; 77 + my $indented = $1; 78 + my $tag = $self->scan_token(); 79 + $tag =~ s/['"\\]//g; 80 + push(@{$self->{heretags}}, $indented ? "\t$tag" : "$tag"); 81 + return "<<$indented$tag"; 82 + } 83 + 84 + sub scan_op { 85 + my ($self, $c) = @_; 86 + my $b = $self->{buff}; 87 + return $c unless $$b =~ /\G(.)/sgc; 88 + my $cc = $c . $1; 89 + return scan_heredoc_tag($self) if $cc eq '<<'; 90 + return $cc if $cc =~ /^(?:&&|\|\||>>|;;|<&|>&|<>|>\|)$/; 91 + pos($$b)--; 92 + return $c; 93 + } 94 + 95 + sub scan_sqstring { 96 + my $self = shift @_; 97 + ${$self->{buff}} =~ /\G([^']*'|.*\z)/sgc; 98 + return "'" . $1; 99 + } 100 + 101 + sub scan_dqstring { 102 + my $self = shift @_; 103 + my $b = $self->{buff}; 104 + my $s = '"'; 105 + while (1) { 106 + # slurp up non-special characters 107 + $s .= $1 if $$b =~ /\G([^"\$\\]+)/gc; 108 + # handle special characters 109 + last unless $$b =~ /\G(.)/sgc; 110 + my $c = $1; 111 + $s .= '"', last if $c eq '"'; 112 + $s .= '$' . $self->scan_dollar(), next if $c eq '$'; 113 + if ($c eq '\\') { 114 + $s .= '\\', last unless $$b =~ /\G(.)/sgc; 115 + $c = $1; 116 + next if $c eq "\n"; # line splice 117 + # backslash escapes only $, `, ", \ in dq-string 118 + $s .= '\\' unless $c =~ /^[\$`"\\]$/; 119 + $s .= $c; 120 + next; 121 + } 122 + die("internal error scanning dq-string '$c'\n"); 123 + } 124 + return $s; 125 + } 126 + 127 + sub scan_balanced { 128 + my ($self, $c1, $c2) = @_; 129 + my $b = $self->{buff}; 130 + my $depth = 1; 131 + my $s = $c1; 132 + while ($$b =~ /\G([^\Q$c1$c2\E]*(?:[\Q$c1$c2\E]|\z))/gc) { 133 + $s .= $1; 134 + $depth++, next if $s =~ /\Q$c1\E$/; 135 + $depth--; 136 + last if $depth == 0; 137 + } 138 + return $s; 139 + } 140 + 141 + sub scan_subst { 142 + my $self = shift @_; 143 + my @tokens = $self->{parser}->parse(qr/^\)$/); 144 + $self->{parser}->next_token(); # closing ")" 145 + return @tokens; 146 + } 147 + 148 + sub scan_dollar { 149 + my $self = shift @_; 150 + my $b = $self->{buff}; 151 + return $self->scan_balanced('(', ')') if $$b =~ /\G\((?=\()/gc; # $((...)) 152 + return '(' . join(' ', $self->scan_subst()) . ')' if $$b =~ /\G\(/gc; # $(...) 153 + return $self->scan_balanced('{', '}') if $$b =~ /\G\{/gc; # ${...} 154 + return $1 if $$b =~ /\G(\w+)/gc; # $var 155 + return $1 if $$b =~ /\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc. 156 + return ''; 157 + } 158 + 159 + sub swallow_heredocs { 160 + my $self = shift @_; 161 + my $b = $self->{buff}; 162 + my $tags = $self->{heretags}; 163 + while (my $tag = shift @$tags) { 164 + my $indent = $tag =~ s/^\t// ? '\\s*' : ''; 165 + $$b =~ /(?:\G|\n)$indent\Q$tag\E(?:\n|\z)/gc; 166 + } 167 + } 168 + 169 + sub scan_token { 170 + my $self = shift @_; 171 + my $b = $self->{buff}; 172 + my $token = ''; 173 + RESTART: 174 + $$b =~ /\G[ \t]+/gc; # skip whitespace (but not newline) 175 + return "\n" if $$b =~ /\G#[^\n]*(?:\n|\z)/gc; # comment 176 + while (1) { 177 + # slurp up non-special characters 178 + $token .= $1 if $$b =~ /\G([^\\;&|<>(){}'"\$\s]+)/gc; 179 + # handle special characters 180 + last unless $$b =~ /\G(.)/sgc; 181 + my $c = $1; 182 + last if $c =~ /^[ \t]$/; # whitespace ends token 183 + pos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\n]$/; 184 + $token .= $self->scan_sqstring(), next if $c eq "'"; 185 + $token .= $self->scan_dqstring(), next if $c eq '"'; 186 + $token .= $c . $self->scan_dollar(), next if $c eq '$'; 187 + $self->swallow_heredocs(), $token = $c, last if $c eq "\n"; 188 + $token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/; 189 + $token = $c, last if $c =~ /^[(){}]$/; 190 + if ($c eq '\\') { 191 + $token .= '\\', last unless $$b =~ /\G(.)/sgc; 192 + $c = $1; 193 + next if $c eq "\n" && length($token); # line splice 194 + goto RESTART if $c eq "\n"; # line splice 195 + $token .= '\\' . $c; 196 + next; 197 + } 198 + die("internal error scanning character '$c'\n"); 199 + } 200 + return length($token) ? $token : undef; 201 + } 202 + 203 + # ShellParser parses POSIX shell scripts (with minor extensions for Bash). It 204 + # is a recursive descent parser very roughly modeled after section 2.10 "Shell 205 + # Grammar" of POSIX chapter 2 "Shell Command Language". 206 + package ShellParser; 207 + 208 + sub new { 209 + my ($class, $s) = @_; 210 + my $self = bless { 211 + buff => [], 212 + stop => [], 213 + output => [] 214 + } => $class; 215 + $self->{lexer} = Lexer->new($self, $s); 216 + return $self; 217 + } 218 + 219 + sub next_token { 220 + my $self = shift @_; 221 + return pop(@{$self->{buff}}) if @{$self->{buff}}; 222 + return $self->{lexer}->scan_token(); 223 + } 224 + 225 + sub untoken { 226 + my $self = shift @_; 227 + push(@{$self->{buff}}, @_); 228 + } 229 + 230 + sub peek { 231 + my $self = shift @_; 232 + my $token = $self->next_token(); 233 + return undef unless defined($token); 234 + $self->untoken($token); 235 + return $token; 236 + } 237 + 238 + sub stop_at { 239 + my ($self, $token) = @_; 240 + return 1 unless defined($token); 241 + my $stop = ${$self->{stop}}[-1] if @{$self->{stop}}; 242 + return defined($stop) && $token =~ $stop; 243 + } 244 + 245 + sub expect { 246 + my ($self, $expect) = @_; 247 + my $token = $self->next_token(); 248 + return $token if defined($token) && $token eq $expect; 249 + push(@{$self->{output}}, "?!ERR?! expected '$expect' but found '" . (defined($token) ? $token : "<end-of-input>") . "'\n"); 250 + $self->untoken($token) if defined($token); 251 + return (); 252 + } 253 + 254 + sub optional_newlines { 255 + my $self = shift @_; 256 + my @tokens; 257 + while (my $token = $self->peek()) { 258 + last unless $token eq "\n"; 259 + push(@tokens, $self->next_token()); 260 + } 261 + return @tokens; 262 + } 263 + 264 + sub parse_group { 265 + my $self = shift @_; 266 + return ($self->parse(qr/^}$/), 267 + $self->expect('}')); 268 + } 269 + 270 + sub parse_subshell { 271 + my $self = shift @_; 272 + return ($self->parse(qr/^\)$/), 273 + $self->expect(')')); 274 + } 275 + 276 + sub parse_case_pattern { 277 + my $self = shift @_; 278 + my @tokens; 279 + while (defined(my $token = $self->next_token())) { 280 + push(@tokens, $token); 281 + last if $token eq ')'; 282 + } 283 + return @tokens; 284 + } 285 + 286 + sub parse_case { 287 + my $self = shift @_; 288 + my @tokens; 289 + push(@tokens, 290 + $self->next_token(), # subject 291 + $self->optional_newlines(), 292 + $self->expect('in'), 293 + $self->optional_newlines()); 294 + while (1) { 295 + my $token = $self->peek(); 296 + last unless defined($token) && $token ne 'esac'; 297 + push(@tokens, 298 + $self->parse_case_pattern(), 299 + $self->optional_newlines(), 300 + $self->parse(qr/^(?:;;|esac)$/)); # item body 301 + $token = $self->peek(); 302 + last unless defined($token) && $token ne 'esac'; 303 + push(@tokens, 304 + $self->expect(';;'), 305 + $self->optional_newlines()); 306 + } 307 + push(@tokens, $self->expect('esac')); 308 + return @tokens; 309 + } 310 + 311 + sub parse_for { 312 + my $self = shift @_; 313 + my @tokens; 314 + push(@tokens, 315 + $self->next_token(), # variable 316 + $self->optional_newlines()); 317 + my $token = $self->peek(); 318 + if (defined($token) && $token eq 'in') { 319 + push(@tokens, 320 + $self->expect('in'), 321 + $self->optional_newlines()); 322 + } 323 + push(@tokens, 324 + $self->parse(qr/^do$/), # items 325 + $self->expect('do'), 326 + $self->optional_newlines(), 327 + $self->parse_loop_body(), 328 + $self->expect('done')); 329 + return @tokens; 330 + } 331 + 332 + sub parse_if { 333 + my $self = shift @_; 334 + my @tokens; 335 + while (1) { 336 + push(@tokens, 337 + $self->parse(qr/^then$/), # if/elif condition 338 + $self->expect('then'), 339 + $self->optional_newlines(), 340 + $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body 341 + my $token = $self->peek(); 342 + last unless defined($token) && $token eq 'elif'; 343 + push(@tokens, $self->expect('elif')); 344 + } 345 + my $token = $self->peek(); 346 + if (defined($token) && $token eq 'else') { 347 + push(@tokens, 348 + $self->expect('else'), 349 + $self->optional_newlines(), 350 + $self->parse(qr/^fi$/)); # else body 351 + } 352 + push(@tokens, $self->expect('fi')); 353 + return @tokens; 354 + } 355 + 356 + sub parse_loop_body { 357 + my $self = shift @_; 358 + return $self->parse(qr/^done$/); 359 + } 360 + 361 + sub parse_loop { 362 + my $self = shift @_; 363 + return ($self->parse(qr/^do$/), # condition 364 + $self->expect('do'), 365 + $self->optional_newlines(), 366 + $self->parse_loop_body(), 367 + $self->expect('done')); 368 + } 369 + 370 + sub parse_func { 371 + my $self = shift @_; 372 + return ($self->expect('('), 373 + $self->expect(')'), 374 + $self->optional_newlines(), 375 + $self->parse_cmd()); # body 376 + } 377 + 378 + sub parse_bash_array_assignment { 379 + my $self = shift @_; 380 + my @tokens = $self->expect('('); 381 + while (defined(my $token = $self->next_token())) { 382 + push(@tokens, $token); 383 + last if $token eq ')'; 384 + } 385 + return @tokens; 386 + } 387 + 388 + my %compound = ( 389 + '{' => \&parse_group, 390 + '(' => \&parse_subshell, 391 + 'case' => \&parse_case, 392 + 'for' => \&parse_for, 393 + 'if' => \&parse_if, 394 + 'until' => \&parse_loop, 395 + 'while' => \&parse_loop); 396 + 397 + sub parse_cmd { 398 + my $self = shift @_; 399 + my $cmd = $self->next_token(); 400 + return () unless defined($cmd); 401 + return $cmd if $cmd eq "\n"; 402 + 403 + my $token; 404 + my @tokens = $cmd; 405 + if ($cmd eq '!') { 406 + push(@tokens, $self->parse_cmd()); 407 + return @tokens; 408 + } elsif (my $f = $compound{$cmd}) { 409 + push(@tokens, $self->$f()); 410 + } elsif (defined($token = $self->peek()) && $token eq '(') { 411 + if ($cmd !~ /\w=$/) { 412 + push(@tokens, $self->parse_func()); 413 + return @tokens; 414 + } 415 + $tokens[-1] .= join(' ', $self->parse_bash_array_assignment()); 416 + } 417 + 418 + while (defined(my $token = $self->next_token())) { 419 + $self->untoken($token), last if $self->stop_at($token); 420 + push(@tokens, $token); 421 + last if $token =~ /^(?:[;&\n|]|&&|\|\|)$/; 422 + } 423 + push(@tokens, $self->next_token()) if $tokens[-1] ne "\n" && defined($token = $self->peek()) && $token eq "\n"; 424 + return @tokens; 425 + } 426 + 427 + sub accumulate { 428 + my ($self, $tokens, $cmd) = @_; 429 + push(@$tokens, @$cmd); 430 + } 431 + 432 + sub parse { 433 + my ($self, $stop) = @_; 434 + push(@{$self->{stop}}, $stop); 435 + goto DONE if $self->stop_at($self->peek()); 436 + my @tokens; 437 + while (my @cmd = $self->parse_cmd()) { 438 + $self->accumulate(\@tokens, \@cmd); 439 + last if $self->stop_at($self->peek()); 440 + } 441 + DONE: 442 + pop(@{$self->{stop}}); 443 + return @tokens; 444 + } 445 + 446 + # TestParser is a subclass of ShellParser which, beyond parsing shell script 447 + # code, is also imbued with semantic knowledge of test construction, and checks 448 + # tests for common problems (such as broken &&-chains) which might hide bugs in 449 + # the tests themselves or in behaviors being exercised by the tests. As such, 450 + # TestParser is only called upon to parse test bodies, not the top-level 451 + # scripts in which the tests are defined. 452 + package TestParser; 453 + 454 + use base 'ShellParser'; 455 + 456 + sub find_non_nl { 457 + my $tokens = shift @_; 458 + my $n = shift @_; 459 + $n = $#$tokens if !defined($n); 460 + $n-- while $n >= 0 && $$tokens[$n] eq "\n"; 461 + return $n; 462 + } 463 + 464 + sub ends_with { 465 + my ($tokens, $needles) = @_; 466 + my $n = find_non_nl($tokens); 467 + for my $needle (reverse(@$needles)) { 468 + return undef if $n < 0; 469 + $n = find_non_nl($tokens, $n), next if $needle eq "\n"; 470 + return undef if $$tokens[$n] !~ $needle; 471 + $n--; 472 + } 473 + return 1; 474 + } 475 + 476 + sub match_ending { 477 + my ($tokens, $endings) = @_; 478 + for my $needles (@$endings) { 479 + next if @$tokens < scalar(grep {$_ ne "\n"} @$needles); 480 + return 1 if ends_with($tokens, $needles); 481 + } 482 + return undef; 483 + } 484 + 485 + sub parse_loop_body { 486 + my $self = shift @_; 487 + my @tokens = $self->SUPER::parse_loop_body(@_); 488 + # did loop signal failure via "|| return" or "|| exit"? 489 + return @tokens if !@tokens || grep(/^(?:return|exit|\$\?)$/, @tokens); 490 + # did loop upstream of a pipe signal failure via "|| echo 'impossible 491 + # text'" as the final command in the loop body? 492 + return @tokens if ends_with(\@tokens, [qr/^\|\|$/, "\n", qr/^echo$/, qr/^.+$/]); 493 + # flag missing "return/exit" handling explicit failure in loop body 494 + my $n = find_non_nl(\@tokens); 495 + splice(@tokens, $n + 1, 0, '?!LOOP?!'); 496 + return @tokens; 497 + } 498 + 499 + my @safe_endings = ( 500 + [qr/^(?:&&|\|\||\||&)$/], 501 + [qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/], 502 + [qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/, qr/^;$/], 503 + [qr/^(?:exit|return|continue)$/], 504 + [qr/^(?:exit|return|continue)$/, qr/^;$/]); 505 + 506 + sub accumulate { 507 + my ($self, $tokens, $cmd) = @_; 508 + goto DONE unless @$tokens; 509 + goto DONE if @$cmd == 1 && $$cmd[0] eq "\n"; 510 + 511 + # did previous command end with "&&", "|", "|| return" or similar? 512 + goto DONE if match_ending($tokens, \@safe_endings); 513 + 514 + # if this command handles "$?" specially, then okay for previous 515 + # command to be missing "&&" 516 + for my $token (@$cmd) { 517 + goto DONE if $token =~ /\$\?/; 518 + } 519 + 520 + # if this command is "false", "return 1", or "exit 1" (which signal 521 + # failure explicitly), then okay for all preceding commands to be 522 + # missing "&&" 523 + if ($$cmd[0] =~ /^(?:false|return|exit)$/) { 524 + @$tokens = grep(!/^\?!AMP\?!$/, @$tokens); 525 + goto DONE; 526 + } 527 + 528 + # flag missing "&&" at end of previous command 529 + my $n = find_non_nl($tokens); 530 + splice(@$tokens, $n + 1, 0, '?!AMP?!') unless $n < 0; 531 + 532 + DONE: 533 + $self->SUPER::accumulate($tokens, $cmd); 534 + } 535 + 536 + # ScriptParser is a subclass of ShellParser which identifies individual test 537 + # definitions within test scripts, and passes each test body through TestParser 538 + # to identify possible problems. ShellParser detects test definitions not only 539 + # at the top-level of test scripts but also within compound commands such as 540 + # loops and function definitions. 541 + package ScriptParser; 542 + 543 + use base 'ShellParser'; 544 + 545 + sub new { 546 + my $class = shift @_; 547 + my $self = $class->SUPER::new(@_); 548 + $self->{ntests} = 0; 549 + return $self; 550 + } 551 + 552 + # extract the raw content of a token, which may be a single string or a 553 + # composition of multiple strings and non-string character runs; for instance, 554 + # `"test body"` unwraps to `test body`; `word"a b"42'c d'` to `worda b42c d` 555 + sub unwrap { 556 + my $token = @_ ? shift @_ : $_; 557 + # simple case: 'sqstring' or "dqstring" 558 + return $token if $token =~ s/^'([^']*)'$/$1/; 559 + return $token if $token =~ s/^"([^"]*)"$/$1/; 560 + 561 + # composite case 562 + my ($s, $q, $escaped); 563 + while (1) { 564 + # slurp up non-special characters 565 + $s .= $1 if $token =~ /\G([^\\'"]*)/gc; 566 + # handle special characters 567 + last unless $token =~ /\G(.)/sgc; 568 + my $c = $1; 569 + $q = undef, next if defined($q) && $c eq $q; 570 + $q = $c, next if !defined($q) && $c =~ /^['"]$/; 571 + if ($c eq '\\') { 572 + last unless $token =~ /\G(.)/sgc; 573 + $c = $1; 574 + $s .= '\\' if $c eq "\n"; # preserve line splice 575 + } 576 + $s .= $c; 577 + } 578 + return $s 579 + } 580 + 581 + sub check_test { 582 + my $self = shift @_; 583 + my ($title, $body) = map(unwrap, @_); 584 + $self->{ntests}++; 585 + my $parser = TestParser->new(\$body); 586 + my @tokens = $parser->parse(); 587 + return unless $emit_all || grep(/\?![^?]+\?!/, @tokens); 588 + my $c = main::fd_colors(1); 589 + my $checked = join(' ', @tokens); 590 + $checked =~ s/^\n//; 591 + $checked =~ s/^ //mg; 592 + $checked =~ s/ $//mg; 593 + $checked =~ s/(\?![^?]+\?!)/$c->{rev}$c->{red}$1$c->{reset}/mg; 594 + $checked .= "\n" unless $checked =~ /\n$/; 595 + push(@{$self->{output}}, "$c->{blue}# chainlint: $title$c->{reset}\n$checked"); 596 + } 597 + 598 + sub parse_cmd { 599 + my $self = shift @_; 600 + my @tokens = $self->SUPER::parse_cmd(); 601 + return @tokens unless @tokens && $tokens[0] =~ /^test_expect_(?:success|failure)$/; 602 + my $n = $#tokens; 603 + $n-- while $n >= 0 && $tokens[$n] =~ /^(?:[;&\n|]|&&|\|\|)$/; 604 + $self->check_test($tokens[1], $tokens[2]) if $n == 2; # title body 605 + $self->check_test($tokens[2], $tokens[3]) if $n > 2; # prereq title body 606 + return @tokens; 607 + } 608 + 609 + # main contains high-level functionality for processing command-line switches, 610 + # feeding input test scripts to ScriptParser, and reporting results. 611 + package main; 612 + 613 + my $getnow = sub { return time(); }; 614 + my $interval = sub { return time() - shift; }; 615 + if (eval {require Time::HiRes; Time::HiRes->import(); 1;}) { 616 + $getnow = sub { return [Time::HiRes::gettimeofday()]; }; 617 + $interval = sub { return Time::HiRes::tv_interval(shift); }; 618 + } 619 + 620 + # Restore TERM if test framework set it to "dumb" so 'tput' will work; do this 621 + # outside of get_colors() since under 'ithreads' all threads use %ENV of main 622 + # thread and ignore %ENV changes in subthreads. 623 + $ENV{TERM} = $ENV{USER_TERM} if $ENV{USER_TERM}; 624 + 625 + my @NOCOLORS = (bold => '', rev => '', reset => '', blue => '', green => '', red => ''); 626 + my %COLORS = (); 627 + sub get_colors { 628 + return \%COLORS if %COLORS; 629 + if (exists($ENV{NO_COLOR}) || 630 + system("tput sgr0 >/dev/null 2>&1") != 0 || 631 + system("tput bold >/dev/null 2>&1") != 0 || 632 + system("tput rev >/dev/null 2>&1") != 0 || 633 + system("tput setaf 1 >/dev/null 2>&1") != 0) { 634 + %COLORS = @NOCOLORS; 635 + return \%COLORS; 636 + } 637 + %COLORS = (bold => `tput bold`, 638 + rev => `tput rev`, 639 + reset => `tput sgr0`, 640 + blue => `tput setaf 4`, 641 + green => `tput setaf 2`, 642 + red => `tput setaf 1`); 643 + chomp(%COLORS); 644 + return \%COLORS; 645 + } 646 + 647 + my %FD_COLORS = (); 648 + sub fd_colors { 649 + my $fd = shift; 650 + return $FD_COLORS{$fd} if exists($FD_COLORS{$fd}); 651 + $FD_COLORS{$fd} = -t $fd ? get_colors() : {@NOCOLORS}; 652 + return $FD_COLORS{$fd}; 653 + } 654 + 655 + sub ncores { 656 + # Windows 657 + return $ENV{NUMBER_OF_PROCESSORS} if exists($ENV{NUMBER_OF_PROCESSORS}); 658 + # Linux / MSYS2 / Cygwin / WSL 659 + do { local @ARGV='/proc/cpuinfo'; return scalar(grep(/^processor\s*:/, <>)); } if -r '/proc/cpuinfo'; 660 + # macOS & BSD 661 + return qx/sysctl -n hw.ncpu/ if $^O =~ /(?:^darwin$|bsd)/; 662 + return 1; 663 + } 664 + 665 + sub show_stats { 666 + my ($start_time, $stats) = @_; 667 + my $walltime = $interval->($start_time); 668 + my ($usertime) = times(); 669 + my ($total_workers, $total_scripts, $total_tests, $total_errs) = (0, 0, 0, 0); 670 + my $c = fd_colors(2); 671 + print(STDERR $c->{green}); 672 + for (@$stats) { 673 + my ($worker, $nscripts, $ntests, $nerrs) = @$_; 674 + print(STDERR "worker $worker: $nscripts scripts, $ntests tests, $nerrs errors\n"); 675 + $total_workers++; 676 + $total_scripts += $nscripts; 677 + $total_tests += $ntests; 678 + $total_errs += $nerrs; 679 + } 680 + 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); 681 + } 682 + 683 + sub check_script { 684 + my ($id, $next_script, $emit) = @_; 685 + my ($nscripts, $ntests, $nerrs) = (0, 0, 0); 686 + while (my $path = $next_script->()) { 687 + $nscripts++; 688 + my $fh; 689 + unless (open($fh, "<", $path)) { 690 + $emit->("?!ERR?! $path: $!\n"); 691 + next; 692 + } 693 + my $s = do { local $/; <$fh> }; 694 + close($fh); 695 + my $parser = ScriptParser->new(\$s); 696 + 1 while $parser->parse_cmd(); 697 + if (@{$parser->{output}}) { 698 + my $c = fd_colors(1); 699 + my $s = join('', @{$parser->{output}}); 700 + $emit->("$c->{bold}$c->{blue}# chainlint: $path$c->{reset}\n" . $s); 701 + $nerrs += () = $s =~ /\?![^?]+\?!/g; 702 + } 703 + $ntests += $parser->{ntests}; 704 + } 705 + return [$id, $nscripts, $ntests, $nerrs]; 706 + } 707 + 708 + sub exit_code { 709 + my $stats = shift @_; 710 + for (@$stats) { 711 + my ($worker, $nscripts, $ntests, $nerrs) = @$_; 712 + return 1 if $nerrs; 713 + } 714 + return 0; 715 + } 716 + 717 + Getopt::Long::Configure(qw{bundling}); 718 + GetOptions( 719 + "emit-all!" => \$emit_all, 720 + "jobs|j=i" => \$jobs, 721 + "stats|show-stats!" => \$show_stats) or die("option error\n"); 722 + $jobs = ncores() if $jobs < 1; 723 + 724 + my $start_time = $getnow->(); 725 + my @stats; 726 + 727 + my @scripts; 728 + push(@scripts, File::Glob::bsd_glob($_)) for (@ARGV); 729 + unless (@scripts) { 730 + show_stats($start_time, \@stats) if $show_stats; 731 + exit; 732 + } 733 + 734 + unless ($Config{useithreads} && eval { 735 + require threads; threads->import(); 736 + require Thread::Queue; Thread::Queue->import(); 737 + 1; 738 + }) { 739 + push(@stats, check_script(1, sub { shift(@scripts); }, sub { print(@_); })); 740 + show_stats($start_time, \@stats) if $show_stats; 741 + exit(exit_code(\@stats)); 742 + } 743 + 744 + my $script_queue = Thread::Queue->new(); 745 + my $output_queue = Thread::Queue->new(); 746 + 747 + sub next_script { return $script_queue->dequeue(); } 748 + sub emit { $output_queue->enqueue(@_); } 749 + 750 + sub monitor { 751 + while (my $s = $output_queue->dequeue()) { 752 + print($s); 753 + } 754 + } 755 + 756 + my $mon = threads->create({'context' => 'void'}, \&monitor); 757 + threads->create({'context' => 'list'}, \&check_script, $_, \&next_script, \&emit) for 1..$jobs; 758 + 759 + $script_queue->enqueue(@scripts); 760 + $script_queue->end(); 761 + 762 + for (threads->list()) { 763 + push(@stats, $_->join()) unless $_ == $mon; 764 + } 765 + 766 + $output_queue->end(); 767 + $mon->join(); 768 + 769 + show_stats($start_time, \@stats) if $show_stats; 770 + exit(exit_code(\@stats));
-399
t/chainlint.sed
··· 1 - #------------------------------------------------------------------------------ 2 - # Detect broken &&-chains in tests. 3 - # 4 - # At present, only &&-chains in subshells are examined by this linter; 5 - # top-level &&-chains are instead checked directly by the test framework. Like 6 - # the top-level &&-chain linter, the subshell linter (intentionally) does not 7 - # check &&-chains within {...} blocks. 8 - # 9 - # Checking for &&-chain breakage is done line-by-line by pure textual 10 - # inspection. 11 - # 12 - # Incomplete lines (those ending with "\") are stitched together with following 13 - # lines to simplify processing, particularly of "one-liner" statements. 14 - # Top-level here-docs are swallowed to avoid false positives within the 15 - # here-doc body, although the statement to which the here-doc is attached is 16 - # retained. 17 - # 18 - # Heuristics are used to detect end-of-subshell when the closing ")" is cuddled 19 - # with the final subshell statement on the same line: 20 - # 21 - # (cd foo && 22 - # bar) 23 - # 24 - # in order to avoid misinterpreting the ")" in constructs such as "x=$(...)" 25 - # and "case $x in *)" as ending the subshell. 26 - # 27 - # Lines missing a final "&&" are flagged with "?!AMP?!", as are lines which 28 - # chain commands with ";" internally rather than "&&". A line may be flagged 29 - # for both violations. 30 - # 31 - # Detection of a missing &&-link in a multi-line subshell is complicated by the 32 - # fact that the last statement before the closing ")" must not end with "&&". 33 - # Since processing is line-by-line, it is not known whether a missing "&&" is 34 - # legitimate or not until the _next_ line is seen. To accommodate this, within 35 - # multi-line subshells, each line is stored in sed's "hold" area until after 36 - # the next line is seen and processed. If the next line is a stand-alone ")", 37 - # then a missing "&&" on the previous line is legitimate; otherwise a missing 38 - # "&&" is a break in the &&-chain. 39 - # 40 - # ( 41 - # cd foo && 42 - # bar 43 - # ) 44 - # 45 - # In practical terms, when "bar" is encountered, it is flagged with "?!AMP?!", 46 - # but when the stand-alone ")" line is seen which closes the subshell, the 47 - # "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold" 48 - # area) since the final statement of a subshell must not end with "&&". The 49 - # final line of a subshell may still break the &&-chain by using ";" internally 50 - # to chain commands together rather than "&&", but an internal "?!AMP?!" is 51 - # never removed from a line even though a line-ending "?!AMP?!" might be. 52 - # 53 - # Care is taken to recognize the last _statement_ of a multi-line subshell, not 54 - # necessarily the last textual _line_ within the subshell, since &&-chaining 55 - # applies to statements, not to lines. Consequently, blank lines, comment 56 - # lines, and here-docs are swallowed (but not the command to which the here-doc 57 - # is attached), leaving the last statement in the "hold" area, not the last 58 - # line, thus simplifying &&-link checking. 59 - # 60 - # The final statement before "done" in for- and while-loops, and before "elif", 61 - # "else", and "fi" in if-then-else likewise must not end with "&&", thus 62 - # receives similar treatment. 63 - # 64 - # Swallowing here-docs with arbitrary tags requires a bit of finesse. When a 65 - # line such as "cat <<EOF" is seen, the here-doc tag is copied to the front of 66 - # the line enclosed in angle brackets as a sentinel, giving "<EOF>cat <<EOF". 67 - # As each subsequent line is read, it is appended to the target line and a 68 - # (whitespace-loose) back-reference match /^<(.*)>\n\1$/ is attempted to see if 69 - # the content inside "<...>" matches the entirety of the newly-read line. For 70 - # instance, if the next line read is "some data", when concatenated with the 71 - # target line, it becomes "<EOF>cat <<EOF\nsome data", and a match is attempted 72 - # to see if "EOF" matches "some data". Since it doesn't, the next line is 73 - # attempted. When a line consisting of only "EOF" (and possible whitespace) is 74 - # encountered, it is appended to the target line giving "<EOF>cat <<EOF\nEOF", 75 - # in which case the "EOF" inside "<...>" does match the text following the 76 - # newline, thus the closing here-doc tag has been found. The closing tag line 77 - # and the "<...>" prefix on the target line are then discarded, leaving just 78 - # the target line "cat <<EOF". 79 - #------------------------------------------------------------------------------ 80 - 81 - # incomplete line -- slurp up next line 82 - :squash 83 - /\\$/ { 84 - N 85 - s/\\\n// 86 - bsquash 87 - } 88 - 89 - # here-doc -- swallow it to avoid false hits within its body (but keep the 90 - # command to which it was attached) 91 - /<<-*[ ]*[\\'"]*[A-Za-z0-9_]/ { 92 - /"[^"]*<<[^"]*"/bnotdoc 93 - s/^\(.*<<-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1\2/ 94 - :hered 95 - N 96 - /^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{ 97 - s/\n.*$// 98 - bhered 99 - } 100 - s/^<[^>]*>// 101 - s/\n.*$// 102 - } 103 - :notdoc 104 - 105 - # one-liner "(...) &&" 106 - /^[ ]*!*[ ]*(..*)[ ]*&&[ ]*$/boneline 107 - 108 - # same as above but without trailing "&&" 109 - /^[ ]*!*[ ]*(..*)[ ]*$/boneline 110 - 111 - # one-liner "(...) >x" (or "2>x" or "<x" or "|x" or "&" 112 - /^[ ]*!*[ ]*(..*)[ ]*[0-9]*[<>|&]/boneline 113 - 114 - # multi-line "(...\n...)" 115 - /^[ ]*(/bsubsh 116 - 117 - # innocuous line -- print it and advance to next line 118 - b 119 - 120 - # found one-liner "(...)" -- mark suspect if it uses ";" internally rather than 121 - # "&&" (but not ";" in a string) 122 - :oneline 123 - /;/{ 124 - /"[^"]*;[^"]*"/!s/;/; ?!AMP?!/ 125 - } 126 - b 127 - 128 - :subsh 129 - # bare "(" line? -- stash for later printing 130 - /^[ ]*([ ]*$/ { 131 - h 132 - bnextln 133 - } 134 - # "(..." line -- "(" opening subshell cuddled with command; temporarily replace 135 - # "(" with sentinel "^" and process the line as if "(" had been seen solo on 136 - # the preceding line; this temporary replacement prevents several rules from 137 - # accidentally thinking "(" introduces a nested subshell; "^" is changed back 138 - # to "(" at output time 139 - x 140 - s/.*// 141 - x 142 - s/(/^/ 143 - bslurp 144 - 145 - :nextln 146 - N 147 - s/.*\n// 148 - 149 - :slurp 150 - # incomplete line "...\" 151 - /\\$/bicmplte 152 - # multi-line quoted string "...\n..."? 153 - /"/bdqstr 154 - # multi-line quoted string '...\n...'? (but not contraction in string "it's") 155 - /'/{ 156 - /"[^'"]*'[^'"]*"/!bsqstr 157 - } 158 - :folded 159 - # here-doc -- swallow it (but not "<<" in a string) 160 - /<<-*[ ]*[\\'"]*[A-Za-z0-9_]/{ 161 - /"[^"]*<<[^"]*"/!bheredoc 162 - } 163 - # comment or empty line -- discard since final non-comment, non-empty line 164 - # before closing ")", "done", "elsif", "else", or "fi" will need to be 165 - # re-visited to drop "suspect" marking since final line of those constructs 166 - # legitimately lacks "&&", so "suspect" mark must be removed 167 - /^[ ]*#/bnextln 168 - /^[ ]*$/bnextln 169 - # in-line comment -- strip it (but not "#" in a string, Bash ${#...} array 170 - # length, or Perforce "//depot/path#42" revision in filespec) 171 - /[ ]#/{ 172 - /"[^"]*#[^"]*"/!s/[ ]#.*$// 173 - } 174 - # one-liner "case ... esac" 175 - /^[ ^]*case[ ]*..*esac/bchkchn 176 - # multi-line "case ... esac" 177 - /^[ ^]*case[ ]..*[ ]in/bcase 178 - # multi-line "for ... done" or "while ... done" 179 - /^[ ^]*for[ ]..*[ ]in/bcont 180 - /^[ ^]*while[ ]/bcont 181 - /^[ ]*do[ ]/bcont 182 - /^[ ]*do[ ]*$/bcont 183 - /;[ ]*do/bcont 184 - /^[ ]*done[ ]*&&[ ]*$/bdone 185 - /^[ ]*done[ ]*$/bdone 186 - /^[ ]*done[ ]*[<>|]/bdone 187 - /^[ ]*done[ ]*)/bdone 188 - /||[ ]*exit[ ]/bcont 189 - /||[ ]*exit[ ]*$/bcont 190 - # multi-line "if...elsif...else...fi" 191 - /^[ ^]*if[ ]/bcont 192 - /^[ ]*then[ ]/bcont 193 - /^[ ]*then[ ]*$/bcont 194 - /;[ ]*then/bcont 195 - /^[ ]*elif[ ]/belse 196 - /^[ ]*elif[ ]*$/belse 197 - /^[ ]*else[ ]/belse 198 - /^[ ]*else[ ]*$/belse 199 - /^[ ]*fi[ ]*&&[ ]*$/bdone 200 - /^[ ]*fi[ ]*$/bdone 201 - /^[ ]*fi[ ]*[<>|]/bdone 202 - /^[ ]*fi[ ]*)/bdone 203 - # nested one-liner "(...) &&" 204 - /^[ ^]*(.*)[ ]*&&[ ]*$/bchkchn 205 - # nested one-liner "(...)" 206 - /^[ ^]*(.*)[ ]*$/bchkchn 207 - # nested one-liner "(...) >x" (or "2>x" or "<x" or "|x") 208 - /^[ ^]*(.*)[ ]*[0-9]*[<>|]/bchkchn 209 - # nested multi-line "(...\n...)" 210 - /^[ ^]*(/bnest 211 - # multi-line "{...\n...}" 212 - /^[ ^]*{/bblock 213 - # closing ")" on own line -- exit subshell 214 - /^[ ]*)/bclssolo 215 - # "$((...))" -- arithmetic expansion; not closing ")" 216 - /\$(([^)][^)]*))[^)]*$/bchkchn 217 - # "$(...)" -- command substitution; not closing ")" 218 - /\$([^)][^)]*)[^)]*$/bchkchn 219 - # multi-line "$(...\n...)" -- command substitution; treat as nested subshell 220 - /\$([^)]*$/bnest 221 - # "=(...)" -- Bash array assignment; not closing ")" 222 - /=(/bchkchn 223 - # closing "...) &&" 224 - /)[ ]*&&[ ]*$/bclose 225 - # closing "...)" 226 - /)[ ]*$/bclose 227 - # closing "...) >x" (or "2>x" or "<x" or "|x") 228 - /)[ ]*[<>|]/bclose 229 - :chkchn 230 - # mark suspect if line uses ";" internally rather than "&&" (but not ";" in a 231 - # string and not ";;" in one-liner "case...esac") 232 - /;/{ 233 - /;;/!{ 234 - /"[^"]*;[^"]*"/!s/;/; ?!AMP?!/ 235 - } 236 - } 237 - # line ends with pipe "...|" -- valid; not missing "&&" 238 - /|[ ]*$/bcont 239 - # missing end-of-line "&&" -- mark suspect 240 - /&&[ ]*$/!s/$/ ?!AMP?!/ 241 - :cont 242 - # retrieve and print previous line 243 - x 244 - s/^\([ ]*\)^/\1(/ 245 - s/?!HERE?!/<</g 246 - n 247 - bslurp 248 - 249 - # found incomplete line "...\" -- slurp up next line 250 - :icmplte 251 - N 252 - s/\\\n// 253 - bslurp 254 - 255 - # check for multi-line double-quoted string "...\n..." -- fold to one line 256 - :dqstr 257 - # remove all quote pairs 258 - s/"\([^"]*\)"/@!\1@!/g 259 - # done if no dangling quote 260 - /"/!bdqdone 261 - # otherwise, slurp next line and try again 262 - N 263 - s/\n// 264 - bdqstr 265 - :dqdone 266 - s/@!/"/g 267 - bfolded 268 - 269 - # check for multi-line single-quoted string '...\n...' -- fold to one line 270 - :sqstr 271 - # remove all quote pairs 272 - s/'\([^']*\)'/@!\1@!/g 273 - # done if no dangling quote 274 - /'/!bsqdone 275 - # otherwise, slurp next line and try again 276 - N 277 - s/\n// 278 - bsqstr 279 - :sqdone 280 - s/@!/'/g 281 - bfolded 282 - 283 - # found here-doc -- swallow it to avoid false hits within its body (but keep 284 - # the command to which it was attached) 285 - :heredoc 286 - s/^\(.*\)<<\(-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\3>\1?!HERE?!\2\3/ 287 - :hdocsub 288 - N 289 - /^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{ 290 - s/\n.*$// 291 - bhdocsub 292 - } 293 - s/^<[^>]*>// 294 - s/\n.*$// 295 - bfolded 296 - 297 - # found "case ... in" -- pass through untouched 298 - :case 299 - x 300 - s/^\([ ]*\)^/\1(/ 301 - s/?!HERE?!/<</g 302 - n 303 - :cascom 304 - /^[ ]*#/{ 305 - N 306 - s/.*\n// 307 - bcascom 308 - } 309 - /^[ ]*esac/bslurp 310 - bcase 311 - 312 - # found "else" or "elif" -- drop "suspect" from final line before "else" since 313 - # that line legitimately lacks "&&" 314 - :else 315 - x 316 - s/\( ?!AMP?!\)* ?!AMP?!$// 317 - x 318 - bcont 319 - 320 - # found "done" closing for-loop or while-loop, or "fi" closing if-then -- drop 321 - # "suspect" from final contained line since that line legitimately lacks "&&" 322 - :done 323 - x 324 - s/\( ?!AMP?!\)* ?!AMP?!$// 325 - x 326 - # is 'done' or 'fi' cuddled with ")" to close subshell? 327 - /done.*)/bclose 328 - /fi.*)/bclose 329 - bchkchn 330 - 331 - # found nested multi-line "(...\n...)" -- pass through untouched 332 - :nest 333 - x 334 - :nstslrp 335 - s/^\([ ]*\)^/\1(/ 336 - s/?!HERE?!/<</g 337 - n 338 - :nstcom 339 - # comment -- not closing ")" if in comment 340 - /^[ ]*#/{ 341 - N 342 - s/.*\n// 343 - bnstcom 344 - } 345 - # closing ")" on own line -- stop nested slurp 346 - /^[ ]*)/bnstcl 347 - # "$((...))" -- arithmetic expansion; not closing ")" 348 - /\$(([^)][^)]*))[^)]*$/bnstcnt 349 - # "$(...)" -- command substitution; not closing ")" 350 - /\$([^)][^)]*)[^)]*$/bnstcnt 351 - # closing "...)" -- stop nested slurp 352 - /)/bnstcl 353 - :nstcnt 354 - x 355 - bnstslrp 356 - :nstcl 357 - # is it "))" which closes nested and parent subshells? 358 - /)[ ]*)/bslurp 359 - bchkchn 360 - 361 - # found multi-line "{...\n...}" block -- pass through untouched 362 - :block 363 - x 364 - s/^\([ ]*\)^/\1(/ 365 - s/?!HERE?!/<</g 366 - n 367 - :blkcom 368 - /^[ ]*#/{ 369 - N 370 - s/.*\n// 371 - bblkcom 372 - } 373 - # closing "}" -- stop block slurp 374 - /}/bchkchn 375 - bblock 376 - 377 - # found closing ")" on own line -- drop "suspect" from final line of subshell 378 - # since that line legitimately lacks "&&" and exit subshell loop 379 - :clssolo 380 - x 381 - s/\( ?!AMP?!\)* ?!AMP?!$// 382 - s/^\([ ]*\)^/\1(/ 383 - s/?!HERE?!/<</g 384 - p 385 - x 386 - s/^\([ ]*\)^/\1(/ 387 - s/?!HERE?!/<</g 388 - b 389 - 390 - # found closing "...)" -- exit subshell loop 391 - :close 392 - x 393 - s/^\([ ]*\)^/\1(/ 394 - s/?!HERE?!/<</g 395 - p 396 - x 397 - s/^\([ ]*\)^/\1(/ 398 - s/?!HERE?!/<</g 399 - b
+18
t/chainlint/blank-line-before-esac.expect
··· 1 + test_done ( ) { 2 + case "$test_failure" in 3 + 0 ) 4 + test_at_end_hook_ 5 + 6 + exit 0 ;; 7 + 8 + * ) 9 + if test $test_external_has_tap -eq 0 10 + then 11 + say_color error "# failed $test_failure among $msg" 12 + say "1..$test_count" 13 + fi 14 + 15 + exit 1 ;; 16 + 17 + esac 18 + }
+19
t/chainlint/blank-line-before-esac.test
··· 1 + # LINT: blank line before "esac" 2 + test_done () { 3 + case "$test_failure" in 4 + 0) 5 + test_at_end_hook_ 6 + 7 + exit 0 ;; 8 + 9 + *) 10 + if test $test_external_has_tap -eq 0 11 + then 12 + say_color error "# failed $test_failure among $msg" 13 + say "1..$test_count" 14 + fi 15 + 16 + exit 1 ;; 17 + 18 + esac 19 + }
+13 -2
t/chainlint/block.expect
··· 1 1 ( 2 2 foo && 3 3 { 4 - echo a 4 + echo a ?!AMP?! 5 5 echo b 6 6 } && 7 7 bar && ··· 9 9 echo c 10 10 } ?!AMP?! 11 11 baz 12 - ) 12 + ) && 13 + 14 + { 15 + echo a ; ?!AMP?! echo b 16 + } && 17 + { echo a ; ?!AMP?! echo b ; } && 18 + 19 + { 20 + echo "${var}9" && 21 + echo "done" 22 + } && 23 + finis
+14 -1
t/chainlint/block.test
··· 11 11 echo c 12 12 } 13 13 baz 14 - ) 14 + ) && 15 + 16 + # LINT: ";" not allowed in place of "&&" 17 + { 18 + echo a; echo b 19 + } && 20 + { echo a; echo b; } && 21 + 22 + # LINT: "}" inside string not mistaken as end of block 23 + { 24 + echo "${var}9" && 25 + echo "done" 26 + } && 27 + finis
+9
t/chainlint/chain-break-background.expect
··· 1 + JGIT_DAEMON_PID= && 2 + git init --bare empty.git && 3 + > empty.git/git-daemon-export-ok && 4 + mkfifo jgit_daemon_output && 5 + { 6 + jgit daemon --port="$JGIT_DAEMON_PORT" . > jgit_daemon_output & 7 + JGIT_DAEMON_PID=$! 8 + } && 9 + test_expect_code 2 git ls-remote --exit-code git://localhost:$JGIT_DAEMON_PORT/empty.git
+10
t/chainlint/chain-break-background.test
··· 1 + JGIT_DAEMON_PID= && 2 + git init --bare empty.git && 3 + >empty.git/git-daemon-export-ok && 4 + mkfifo jgit_daemon_output && 5 + { 6 + # LINT: exit status of "&" is always 0 so &&-chaining immaterial 7 + jgit daemon --port="$JGIT_DAEMON_PORT" . >jgit_daemon_output & 8 + JGIT_DAEMON_PID=$! 9 + } && 10 + test_expect_code 2 git ls-remote --exit-code git://localhost:$JGIT_DAEMON_PORT/empty.git
+12
t/chainlint/chain-break-continue.expect
··· 1 + git ls-tree --name-only -r refs/notes/many_notes | 2 + while read path 3 + do 4 + test "$path" = "foobar/non-note.txt" && continue 5 + test "$path" = "deadbeef" && continue 6 + test "$path" = "de/adbeef" && continue 7 + 8 + if test $(expr length "$path") -ne $hexsz 9 + then 10 + return 1 11 + fi 12 + done
+13
t/chainlint/chain-break-continue.test
··· 1 + git ls-tree --name-only -r refs/notes/many_notes | 2 + while read path 3 + do 4 + # LINT: broken &&-chain okay if explicit "continue" 5 + test "$path" = "foobar/non-note.txt" && continue 6 + test "$path" = "deadbeef" && continue 7 + test "$path" = "de/adbeef" && continue 8 + 9 + if test $(expr length "$path") -ne $hexsz 10 + then 11 + return 1 12 + fi 13 + done
+9
t/chainlint/chain-break-false.expect
··· 1 + if condition not satisified 2 + then 3 + echo it did not work... 4 + echo failed! 5 + false 6 + else 7 + echo it went okay ?!AMP?! 8 + congratulate user 9 + fi
+10
t/chainlint/chain-break-false.test
··· 1 + # LINT: broken &&-chain okay if explicit "false" signals failure 2 + if condition not satisified 3 + then 4 + echo it did not work... 5 + echo failed! 6 + false 7 + else 8 + echo it went okay 9 + congratulate user 10 + fi
+19
t/chainlint/chain-break-return-exit.expect
··· 1 + case "$(git ls-files)" in 2 + one ) echo pass one ;; 3 + * ) echo bad one ; return 1 ;; 4 + esac && 5 + ( 6 + case "$(git ls-files)" in 7 + two ) echo pass two ;; 8 + * ) echo bad two ; exit 1 ;; 9 + esac 10 + ) && 11 + case "$(git ls-files)" in 12 + dir/two"$LF"one ) echo pass both ;; 13 + * ) echo bad ; return 1 ;; 14 + esac && 15 + 16 + for i in 1 2 3 4 ; do 17 + git checkout main -b $i || return $? 18 + test_commit $i $i $i tag$i || return $? 19 + done
+23
t/chainlint/chain-break-return-exit.test
··· 1 + case "$(git ls-files)" in 2 + one) echo pass one ;; 3 + # LINT: broken &&-chain okay if explicit "return 1" signals failuire 4 + *) echo bad one; return 1 ;; 5 + esac && 6 + ( 7 + case "$(git ls-files)" in 8 + two) echo pass two ;; 9 + # LINT: broken &&-chain okay if explicit "exit 1" signals failuire 10 + *) echo bad two; exit 1 ;; 11 + esac 12 + ) && 13 + case "$(git ls-files)" in 14 + dir/two"$LF"one) echo pass both ;; 15 + # LINT: broken &&-chain okay if explicit "return 1" signals failuire 16 + *) echo bad; return 1 ;; 17 + esac && 18 + 19 + for i in 1 2 3 4 ; do 20 + # LINT: broken &&-chain okay if explicit "return $?" signals failure 21 + git checkout main -b $i || return $? 22 + test_commit $i $i $i tag$i || return $? 23 + done
+9
t/chainlint/chain-break-status.expect
··· 1 + OUT=$(( ( large_git ; echo $? 1 >& 3 ) | : ) 3 >& 1) && 2 + test_match_signal 13 "$OUT" && 3 + 4 + { test-tool sigchain > actual ; ret=$? ; } && 5 + { 6 + test_match_signal 15 "$ret" || 7 + test "$ret" = 3 8 + } && 9 + test_cmp expect actual
+11
t/chainlint/chain-break-status.test
··· 1 + # LINT: broken &&-chain okay if next command handles "$?" explicitly 2 + OUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) && 3 + test_match_signal 13 "$OUT" && 4 + 5 + # LINT: broken &&-chain okay if next command handles "$?" explicitly 6 + { test-tool sigchain >actual; ret=$?; } && 7 + { 8 + test_match_signal 15 "$ret" || 9 + test "$ret" = 3 10 + } && 11 + test_cmp expect actual
+9
t/chainlint/chained-block.expect
··· 1 + echo nobody home && { 2 + test the doohicky ?!AMP?! 3 + right now 4 + } && 5 + 6 + GIT_EXTERNAL_DIFF=echo git diff | { 7 + read path oldfile oldhex oldmode newfile newhex newmode && 8 + test "z$oh" = "z$oldhex" 9 + }
+11
t/chainlint/chained-block.test
··· 1 + # LINT: start of block chained to preceding command 2 + echo nobody home && { 3 + test the doohicky 4 + right now 5 + } && 6 + 7 + # LINT: preceding command pipes to block on same line 8 + GIT_EXTERNAL_DIFF=echo git diff | { 9 + read path oldfile oldhex oldmode newfile newhex newmode && 10 + test "z$oh" = "z$oldhex" 11 + }
+10
t/chainlint/chained-subshell.expect
··· 1 + mkdir sub && ( 2 + cd sub && 3 + foo the bar ?!AMP?! 4 + nuff said 5 + ) && 6 + 7 + cut "-d " -f actual | ( read s1 s2 s3 && 8 + test -f $s1 ?!AMP?! 9 + test $(cat $s2) = tree2path1 && 10 + test $(cat $s3) = tree3path1 )
+13
t/chainlint/chained-subshell.test
··· 1 + # LINT: start of subshell chained to preceding command 2 + mkdir sub && ( 3 + cd sub && 4 + foo the bar 5 + nuff said 6 + ) && 7 + 8 + # LINT: preceding command pipes to subshell on same line 9 + cut "-d " -f actual | (read s1 s2 s3 && 10 + test -f $s1 11 + test $(cat $s2) = tree2path1 && 12 + # LINT: closing subshell ")" correctly detected on same line as "$(...)" 13 + test $(cat $s3) = tree3path1)
+2
t/chainlint/command-substitution-subsubshell.expect
··· 1 + OUT=$(( ( large_git 1 >& 3 ) | : ) 3 >& 1) && 2 + test_match_signal 13 "$OUT"
+3
t/chainlint/command-substitution-subsubshell.test
··· 1 + # LINT: subshell nested in subshell nested in command substitution 2 + OUT=$( ((large_git 1>&3) | :) 3>&1 ) && 3 + test_match_signal 13 "$OUT"
+1 -1
t/chainlint/complex-if-in-cuddled-loop.expect
··· 4 4 : 5 5 else 6 6 echo >file 7 - fi 7 + fi ?!LOOP?! 8 8 done) && 9 9 test ! -f file
+2
t/chainlint/double-here-doc.expect
··· 1 + run_sub_test_lib_test_err run-inv-range-start "--run invalid range start" --run="a-5" <<-EOF && 2 + check_sub_test_lib_test_err run-inv-range-start <<-EOF_OUT 3 <<-EOF_ERR
+12
t/chainlint/double-here-doc.test
··· 1 + run_sub_test_lib_test_err run-inv-range-start \ 2 + "--run invalid range start" \ 3 + --run="a-5" <<-\EOF && 4 + test_expect_success "passing test #1" "true" 5 + test_done 6 + EOF 7 + check_sub_test_lib_test_err run-inv-range-start \ 8 + <<-\EOF_OUT 3<<-EOF_ERR 9 + > FATAL: Unexpected exit with code 1 10 + EOF_OUT 11 + > error: --run: invalid non-numeric in range start: ${SQ}a-5${SQ} 12 + EOF_ERR
+3
t/chainlint/dqstring-line-splice.expect
··· 1 + echo 'fatal: reword option of --fixup is mutually exclusive with' '--patch/--interactive/--all/--include/--only' > expect && 2 + test_must_fail git commit --fixup=reword:HEAD~ $1 2 > actual && 3 + test_cmp expect actual
+7
t/chainlint/dqstring-line-splice.test
··· 1 + # LINT: line-splice within DQ-string 2 + '" 3 + echo 'fatal: reword option of --fixup is mutually exclusive with'\ 4 + '--patch/--interactive/--all/--include/--only' >expect && 5 + test_must_fail git commit --fixup=reword:HEAD~ $1 2>actual && 6 + test_cmp expect actual 7 + "'
+11
t/chainlint/dqstring-no-interpolate.expect
··· 1 + grep "^ ! [rejected][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" out && 2 + 3 + grep "^\.git$" output.txt && 4 + 5 + 6 + ( 7 + cd client$version && 8 + GIT_TEST_PROTOCOL_VERSION=$version git fetch-pack --no-progress .. $(cat ../input) 9 + ) > output && 10 + cut -d ' ' -f 2 < output | sort > actual && 11 + test_cmp expect actual
+15
t/chainlint/dqstring-no-interpolate.test
··· 1 + # LINT: regex dollar-sign eol anchor in double-quoted string not special 2 + grep "^ ! \[rejected\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" out && 3 + 4 + # LINT: escaped "$" not mistaken for variable expansion 5 + grep "^\\.git\$" output.txt && 6 + 7 + '" 8 + ( 9 + cd client$version && 10 + # LINT: escaped dollar-sign in double-quoted test body 11 + GIT_TEST_PROTOCOL_VERSION=$version git fetch-pack --no-progress .. \$(cat ../input) 12 + ) >output && 13 + cut -d ' ' -f 2 <output | sort >actual && 14 + test_cmp expect actual 15 + "'
+3
t/chainlint/empty-here-doc.expect
··· 1 + git ls-tree $tree path > current && 2 + cat > expected <<EOF && 3 + test_output
+5
t/chainlint/empty-here-doc.test
··· 1 + git ls-tree $tree path >current && 2 + # LINT: empty here-doc 3 + cat >expected <<\EOF && 4 + EOF 5 + test_output
+4
t/chainlint/exclamation.expect
··· 1 + if ! condition ; then echo nope ; else yep ; fi && 2 + test_prerequisite !MINGW && 3 + mail uucp!address && 4 + echo !whatever!
+8
t/chainlint/exclamation.test
··· 1 + # LINT: "! word" is two tokens 2 + if ! condition; then echo nope; else yep; fi && 3 + # LINT: "!word" is single token, not two tokens "!" and "word" 4 + test_prerequisite !MINGW && 5 + # LINT: "word!word" is single token, not three tokens "word", "!", and "word" 6 + mail uucp!address && 7 + # LINT: "!word!" is single token, not three tokens "!", "word", and "!" 8 + echo !whatever!
+5
t/chainlint/for-loop-abbreviated.expect
··· 1 + for it 2 + do 3 + path=$(expr "$it" : ( [^:]*) ) && 4 + git update-index --add "$path" || exit 5 + done
+6
t/chainlint/for-loop-abbreviated.test
··· 1 + # LINT: for-loop lacking optional "in [word...]" before "do" 2 + for it 3 + do 4 + path=$(expr "$it" : '\([^:]*\)') && 5 + git update-index --add "$path" || exit 6 + done
+2 -2
t/chainlint/for-loop.expect
··· 2 2 for i in a b c 3 3 do 4 4 echo $i ?!AMP?! 5 - cat <<-EOF 5 + cat <<-EOF ?!LOOP?! 6 6 done ?!AMP?! 7 7 for i in a b c; do 8 8 echo $i && 9 - cat $i 9 + cat $i ?!LOOP?! 10 10 done 11 11 )
+11
t/chainlint/function.expect
··· 1 + sha1_file ( ) { 2 + echo "$*" | sed "s#..#.git/objects/&/#" 3 + } && 4 + 5 + remove_object ( ) { 6 + file=$(sha1_file "$*") && 7 + test -e "$file" ?!AMP?! 8 + rm -f "$file" 9 + } ?!AMP?! 10 + 11 + sha1_file arg && remove_object arg
+13
t/chainlint/function.test
··· 1 + # LINT: "()" in function definition not mistaken for subshell 2 + sha1_file() { 3 + echo "$*" | sed "s#..#.git/objects/&/#" 4 + } && 5 + 6 + # LINT: broken &&-chain in function and after function 7 + remove_object() { 8 + file=$(sha1_file "$*") && 9 + test -e "$file" 10 + rm -f "$file" 11 + } 12 + 13 + sha1_file arg && remove_object arg
+5
t/chainlint/here-doc-indent-operator.expect
··· 1 + cat > expect <<-EOF && 2 + 3 + cat > expect <<-EOF ?!AMP?! 4 + 5 + cleanup
+13
t/chainlint/here-doc-indent-operator.test
··· 1 + # LINT: whitespace between operator "<<-" and tag legal 2 + cat >expect <<- EOF && 3 + header: 43475048 1 $(test_oid oid_version) $NUM_CHUNKS 0 4 + num_commits: $1 5 + chunks: oid_fanout oid_lookup commit_metadata generation_data bloom_indexes bloom_data 6 + EOF 7 + 8 + # LINT: not an indented here-doc; just a plain here-doc with tag named "-EOF" 9 + cat >expect << -EOF 10 + this is not indented 11 + -EOF 12 + 13 + cleanup
+2 -1
t/chainlint/here-doc-multi-line-string.expect
··· 1 1 ( 2 - cat <<-TXT && echo "multi-line string" ?!AMP?! 2 + cat <<-TXT && echo "multi-line 3 + string" ?!AMP?! 3 4 bap 4 5 )
+7
t/chainlint/if-condition-split.expect
··· 1 + if bob && 2 + marcia || 3 + kevin 4 + then 5 + echo "nomads" ?!AMP?! 6 + echo "for sure" 7 + fi
+8
t/chainlint/if-condition-split.test
··· 1 + # LINT: "if" condition split across multiple lines at "&&" or "||" 2 + if bob && 3 + marcia || 4 + kevin 5 + then 6 + echo "nomads" 7 + echo "for sure" 8 + fi
+1 -1
t/chainlint/if-in-loop.expect
··· 3 3 do 4 4 if false 5 5 then 6 - echo "err" ?!AMP?! 6 + echo "err" 7 7 exit 1 8 8 fi ?!AMP?! 9 9 foo
+1 -1
t/chainlint/if-in-loop.test
··· 3 3 do 4 4 if false 5 5 then 6 - # LINT: missing "&&" on "echo" 6 + # LINT: missing "&&" on "echo" okay since "exit 1" signals error explicitly 7 7 echo "err" 8 8 exit 1 9 9 # LINT: missing "&&" on "fi"
+15
t/chainlint/loop-detect-failure.expect
··· 1 + git init r1 && 2 + for n in 1 2 3 4 5 3 + do 4 + echo "This is file: $n" > r1/file.$n && 5 + git -C r1 add file.$n && 6 + git -C r1 commit -m "$n" || return 1 7 + done && 8 + 9 + git init r2 && 10 + for n in 1000 10000 11 + do 12 + printf "%"$n"s" X > r2/large.$n && 13 + git -C r2 add large.$n && 14 + git -C r2 commit -m "$n" ?!LOOP?! 15 + done
+17
t/chainlint/loop-detect-failure.test
··· 1 + git init r1 && 2 + # LINT: loop handles failure explicitly with "|| return 1" 3 + for n in 1 2 3 4 5 4 + do 5 + echo "This is file: $n" > r1/file.$n && 6 + git -C r1 add file.$n && 7 + git -C r1 commit -m "$n" || return 1 8 + done && 9 + 10 + git init r2 && 11 + # LINT: loop fails to handle failure explicitly with "|| return 1" 12 + for n in 1000 10000 13 + do 14 + printf "%"$n"s" X > r2/large.$n && 15 + git -C r2 add large.$n && 16 + git -C r2 commit -m "$n" 17 + done
+18
t/chainlint/loop-detect-status.expect
··· 1 + ( while test $i -le $blobcount 2 + do 3 + printf "Generating blob $i/$blobcount\r" >& 2 && 4 + printf "blob\nmark :$i\ndata $blobsize\n" && 5 + 6 + printf "%-${blobsize}s" $i && 7 + echo "M 100644 :$i $i" >> commit && 8 + i=$(($i+1)) || 9 + echo $? > exit-status 10 + done && 11 + echo "commit refs/heads/main" && 12 + echo "author A U Thor <author@email.com> 123456789 +0000" && 13 + echo "committer C O Mitter <committer@email.com> 123456789 +0000" && 14 + echo "data 5" && 15 + echo ">2gb" && 16 + cat commit ) | 17 + git fast-import --big-file-threshold=2 && 18 + test ! -f exit-status
+19
t/chainlint/loop-detect-status.test
··· 1 + # LINT: "$?" handled explicitly within loop body 2 + (while test $i -le $blobcount 3 + do 4 + printf "Generating blob $i/$blobcount\r" >&2 && 5 + printf "blob\nmark :$i\ndata $blobsize\n" && 6 + #test-tool genrandom $i $blobsize && 7 + printf "%-${blobsize}s" $i && 8 + echo "M 100644 :$i $i" >> commit && 9 + i=$(($i+1)) || 10 + echo $? > exit-status 11 + done && 12 + echo "commit refs/heads/main" && 13 + echo "author A U Thor <author@email.com> 123456789 +0000" && 14 + echo "committer C O Mitter <committer@email.com> 123456789 +0000" && 15 + echo "data 5" && 16 + echo ">2gb" && 17 + cat commit) | 18 + git fast-import --big-file-threshold=2 && 19 + test ! -f exit-status
+1 -1
t/chainlint/loop-in-if.expect
··· 4 4 while true 5 5 do 6 6 echo "pop" ?!AMP?! 7 - echo "glup" 7 + echo "glup" ?!LOOP?! 8 8 done ?!AMP?! 9 9 foo 10 10 fi ?!AMP?!
+10
t/chainlint/loop-upstream-pipe.expect
··· 1 + ( 2 + git rev-list --objects --no-object-names base..loose | 3 + while read oid 4 + do 5 + path="$objdir/$(test_oid_to_path "$oid")" && 6 + printf "%s %d\n" "$oid" "$(test-tool chmtime --get "$path")" || 7 + echo "object list generation failed for $oid" 8 + done | 9 + sort -k1 10 + ) >expect &&
+11
t/chainlint/loop-upstream-pipe.test
··· 1 + ( 2 + git rev-list --objects --no-object-names base..loose | 3 + while read oid 4 + do 5 + # LINT: "|| echo" signals failure in loop upstream of a pipe 6 + path="$objdir/$(test_oid_to_path "$oid")" && 7 + printf "%s %d\n" "$oid" "$(test-tool chmtime --get "$path")" || 8 + echo "object list generation failed for $oid" 9 + done | 10 + sort -k1 11 + ) >expect &&
+8 -3
t/chainlint/multi-line-string.expect
··· 1 1 ( 2 - x="line 1 line 2 line 3" && 3 - y="line 1 line2" ?!AMP?! 2 + x="line 1 3 + line 2 4 + line 3" && 5 + y="line 1 6 + line2" ?!AMP?! 4 7 foobar 5 8 ) && 6 9 ( 7 - echo "xyz" "abc def ghi" && 10 + echo "xyz" "abc 11 + def 12 + ghi" && 8 13 barfoo 9 14 )
+31
t/chainlint/nested-loop-detect-failure.expect
··· 1 + for i in 0 1 2 3 4 5 6 7 8 9 ; 2 + do 3 + for j in 0 1 2 3 4 5 6 7 8 9 ; 4 + do 5 + echo "$i$j" > "path$i$j" ?!LOOP?! 6 + done ?!LOOP?! 7 + done && 8 + 9 + for i in 0 1 2 3 4 5 6 7 8 9 ; 10 + do 11 + for j in 0 1 2 3 4 5 6 7 8 9 ; 12 + do 13 + echo "$i$j" > "path$i$j" || return 1 14 + done 15 + done && 16 + 17 + for i in 0 1 2 3 4 5 6 7 8 9 ; 18 + do 19 + for j in 0 1 2 3 4 5 6 7 8 9 ; 20 + do 21 + echo "$i$j" > "path$i$j" ?!LOOP?! 22 + done || return 1 23 + done && 24 + 25 + for i in 0 1 2 3 4 5 6 7 8 9 ; 26 + do 27 + for j in 0 1 2 3 4 5 6 7 8 9 ; 28 + do 29 + echo "$i$j" > "path$i$j" || return 1 30 + done || return 1 31 + done
+35
t/chainlint/nested-loop-detect-failure.test
··· 1 + # LINT: neither loop handles failure explicitly with "|| return 1" 2 + for i in 0 1 2 3 4 5 6 7 8 9; 3 + do 4 + for j in 0 1 2 3 4 5 6 7 8 9; 5 + do 6 + echo "$i$j" >"path$i$j" 7 + done 8 + done && 9 + 10 + # LINT: inner loop handles failure explicitly with "|| return 1" 11 + for i in 0 1 2 3 4 5 6 7 8 9; 12 + do 13 + for j in 0 1 2 3 4 5 6 7 8 9; 14 + do 15 + echo "$i$j" >"path$i$j" || return 1 16 + done 17 + done && 18 + 19 + # LINT: outer loop handles failure explicitly with "|| return 1" 20 + for i in 0 1 2 3 4 5 6 7 8 9; 21 + do 22 + for j in 0 1 2 3 4 5 6 7 8 9; 23 + do 24 + echo "$i$j" >"path$i$j" 25 + done || return 1 26 + done && 27 + 28 + # LINT: inner & outer loops handles failure explicitly with "|| return 1" 29 + for i in 0 1 2 3 4 5 6 7 8 9; 30 + do 31 + for j in 0 1 2 3 4 5 6 7 8 9; 32 + do 33 + echo "$i$j" >"path$i$j" || return 1 34 + done || return 1 35 + done
+1 -1
t/chainlint/nested-subshell.expect
··· 6 6 ) >file && 7 7 cd foo && 8 8 ( 9 - echo a 9 + echo a ?!AMP?! 10 10 echo b 11 11 ) >file 12 12 )
+9
t/chainlint/one-liner-for-loop.expect
··· 1 + git init dir-rename-and-content && 2 + ( 3 + cd dir-rename-and-content && 4 + test_write_lines 1 2 3 4 5 >foo && 5 + mkdir olddir && 6 + for i in a b c; do echo $i >olddir/$i; ?!LOOP?! done ?!AMP?! 7 + git add foo olddir && 8 + git commit -m "original" && 9 + )
+10
t/chainlint/one-liner-for-loop.test
··· 1 + git init dir-rename-and-content && 2 + ( 3 + cd dir-rename-and-content && 4 + test_write_lines 1 2 3 4 5 >foo && 5 + mkdir olddir && 6 + # LINT: one-liner for-loop missing "|| exit"; also broken &&-chain 7 + for i in a b c; do echo $i >olddir/$i; done 8 + git add foo olddir && 9 + git commit -m "original" && 10 + )
+5
t/chainlint/return-loop.expect
··· 1 + while test $i -lt $((num - 5)) 2 + do 3 + git notes add -m "notes for commit$i" HEAD~$i || return 1 4 + i=$((i + 1)) 5 + done
+6
t/chainlint/return-loop.test
··· 1 + while test $i -lt $((num - 5)) 2 + do 3 + # LINT: "|| return {n}" valid loop escape outside subshell; no "&&" needed 4 + git notes add -m "notes for commit$i" HEAD~$i || return 1 5 + i=$((i + 1)) 6 + done
+1 -1
t/chainlint/semicolon.expect
··· 15 15 ) && 16 16 (cd foo && 17 17 for i in a b c; do 18 - echo; 18 + echo; ?!LOOP?! 19 19 done)
+4
t/chainlint/sqstring-in-sqstring.expect
··· 1 + perl -e ' 2 + defined($_ = -s $_) or die for @ARGV; 3 + exit 1 if $ARGV[0] <= $ARGV[1]; 4 + ' test-2-$packname_2.pack test-3-$packname_3.pack
+5
t/chainlint/sqstring-in-sqstring.test
··· 1 + # LINT: SQ-string Perl code fragment within SQ-string 2 + perl -e '\'' 3 + defined($_ = -s $_) or die for @ARGV; 4 + exit 1 if $ARGV[0] <= $ARGV[1]; 5 + '\'' test-2-$packname_2.pack test-3-$packname_3.pack
+10 -3
t/chainlint/t7900-subtree.expect
··· 1 1 ( 2 - chks="sub1sub2sub3sub4" && 2 + chks="sub1 3 + sub2 4 + sub3 5 + sub4" && 3 6 chks_sub=$(cat <<TXT | sed "s,^,sub dir/," 4 7 ) && 5 - chkms="main-sub1main-sub2main-sub3main-sub4" && 8 + chkms="main-sub1 9 + main-sub2 10 + main-sub3 11 + main-sub4" && 6 12 chkms_sub=$(cat <<TXT | sed "s,^,sub dir/," 7 13 ) && 8 14 subfiles=$(git ls-files) && 9 - check_equal "$subfiles" "$chkms$chks" 15 + check_equal "$subfiles" "$chkms 16 + $chks" 10 17 )
+27
t/chainlint/token-pasting.expect
··· 1 + git config filter.rot13.smudge ./rot13.sh && 2 + git config filter.rot13.clean ./rot13.sh && 3 + 4 + { 5 + echo "*.t filter=rot13" ?!AMP?! 6 + echo "*.i ident" 7 + } > .gitattributes && 8 + 9 + { 10 + echo a b c d e f g h i j k l m ?!AMP?! 11 + echo n o p q r s t u v w x y z ?!AMP?! 12 + echo '$Id$' 13 + } > test && 14 + cat test > test.t && 15 + cat test > test.o && 16 + cat test > test.i && 17 + git add test test.t test.i && 18 + rm -f test test.t test.i && 19 + git checkout -- test test.t test.i && 20 + 21 + echo "content-test2" > test2.o && 22 + echo "content-test3 - filename with special characters" > "test3 'sq',$x=.o" ?!AMP?! 23 + 24 + downstream_url_for_sed=$( 25 + printf "%sn" "$downstream_url" | 26 + sed -e 's/\/\\/g' -e 's/[[/.*^$]/\&/g' 27 + )
+32
t/chainlint/token-pasting.test
··· 1 + # LINT: single token; composite of multiple strings 2 + git config filter.rot13.smudge ./rot13.sh && 3 + git config filter.rot13.clean ./rot13.sh && 4 + 5 + { 6 + echo "*.t filter=rot13" 7 + echo "*.i ident" 8 + } >.gitattributes && 9 + 10 + { 11 + echo a b c d e f g h i j k l m 12 + echo n o p q r s t u v w x y z 13 + # LINT: exit/enter string context and escaped-quote outside of string 14 + echo '\''$Id$'\'' 15 + } >test && 16 + cat test >test.t && 17 + cat test >test.o && 18 + cat test >test.i && 19 + git add test test.t test.i && 20 + rm -f test test.t test.i && 21 + git checkout -- test test.t test.i && 22 + 23 + echo "content-test2" >test2.o && 24 + # LINT: exit/enter string context and escaped-quote outside of string 25 + echo "content-test3 - filename with special characters" >"test3 '\''sq'\'',\$x=.o" 26 + 27 + # LINT: single token; composite of multiple strings 28 + downstream_url_for_sed=$( 29 + printf "%s\n" "$downstream_url" | 30 + # LINT: exit/enter string context; "&" inside string not command terminator 31 + sed -e '\''s/\\/\\\\/g'\'' -e '\''s/[[/.*^$]/\\&/g'\'' 32 + )
+2 -2
t/chainlint/while-loop.expect
··· 2 2 while true 3 3 do 4 4 echo foo ?!AMP?! 5 - cat <<-EOF 5 + cat <<-EOF ?!LOOP?! 6 6 done ?!AMP?! 7 7 while true; do 8 8 echo foo && 9 - cat bar 9 + cat bar ?!LOOP?! 10 10 done 11 11 )
+1 -6
t/t0027-auto-crlf.sh
··· 387 387 test_tick 388 388 ' 389 389 390 - # Disable extra chain-linting for the next set of tests. There are many 391 - # auto-generated ones that are not worth checking over and over. 392 - GIT_TEST_CHAIN_LINT_HARDER_DEFAULT=0 390 + 393 391 394 392 warn_LF_CRLF="LF will be replaced by CRLF" 395 393 warn_CRLF_LF="CRLF will be replaced by LF" ··· 605 603 checkout_files "" "$id" "lf" true "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul 606 604 checkout_files "" "$id" "crlf" true "" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul 607 605 done 608 - 609 - # The rest of the tests are unique; do the usual linting. 610 - unset GIT_TEST_CHAIN_LINT_HARDER_DEFAULT 611 606 612 607 # Should be the last test case: remove some files from the worktree 613 608 test_expect_success 'ls-files --eol -d -z' '
-5
t/t3070-wildmatch.sh
··· 5 5 TEST_PASSES_SANITIZE_LEAK=true 6 6 . ./test-lib.sh 7 7 8 - # Disable expensive chain-lint tests; all of the tests in this script 9 - # are variants of a few trivial test-tool invocations, and there are a lot of 10 - # them. 11 - GIT_TEST_CHAIN_LINT_HARDER_DEFAULT=0 12 - 13 8 should_create_test_file() { 14 9 file=$1 15 10
+7 -5
t/test-lib.sh
··· 1091 1091 trace= 1092 1092 # 117 is magic because it is unlikely to match the exit 1093 1093 # code of other programs 1094 - if test "OK-117" != "$(test_eval_ "(exit 117) && $1${LF}${LF}echo OK-\$?" 3>&1)" || 1095 - { 1096 - test "${GIT_TEST_CHAIN_LINT_HARDER:-${GIT_TEST_CHAIN_LINT_HARDER_DEFAULT:-1}}" != 0 && 1097 - $(printf '%s\n' "$1" | sed -f "$GIT_BUILD_DIR/t/chainlint.sed" | grep -q '?![A-Z][A-Z]*?!') 1098 - } 1094 + if test "OK-117" != "$(test_eval_ "(exit 117) && $1${LF}${LF}echo OK-\$?" 3>&1)" 1099 1095 then 1100 1096 BUG "broken &&-chain or run-away HERE-DOC: $1" 1101 1097 fi ··· 1589 1585 elif test_bool_env GIT_TEST_SANITIZE_LEAK_LOG false 1590 1586 then 1591 1587 BAIL_OUT_ENV_NEEDS_SANITIZE_LEAK "GIT_TEST_SANITIZE_LEAK_LOG=true" 1588 + fi 1589 + 1590 + if test "${GIT_TEST_CHAIN_LINT:-1}" != 0 1591 + then 1592 + "$PERL_PATH" "$TEST_DIRECTORY/chainlint.pl" "$0" || 1593 + BUG "lint error (see '?!...!? annotations above)" 1592 1594 fi 1593 1595 1594 1596 # Last-minute variable setup