3 # Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>
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.
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.
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.
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.
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.
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.
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
66 my ($class, $parser, $s) = @_;
75 sub scan_heredoc_tag
{
77 ${$self->{buff
}} =~ /\G(-?)/gc;
79 my $token = $self->scan_token();
80 return "<<$indented" unless $token;
81 my $tag = $token->[0];
83 $$token[0] = $indented ?
"\t$tag" : "$tag";
84 push(@
{$self->{heretags
}}, $token);
85 return "<<$indented$tag";
90 my $b = $self->{buff
};
91 return $c unless $$b =~ /\G(.)/sgc;
93 return scan_heredoc_tag
($self) if $cc eq '<<';
94 return $cc if $cc =~ /^(?:&&|\|\||>>|;;|<&|>&|<>|>\|)$/;
101 ${$self->{buff
}} =~ /\G([^']*'|.*\z)/sgc;
103 $self->{lineno
} += () = $s =~ /\n/sg;
109 my $b = $self->{buff
};
112 # slurp up non-special characters
113 $s .= $1 if $$b =~ /\G([^"\$\\]+)/gc;
114 # handle special characters
115 last unless $$b =~ /\G(.)/sgc;
117 $s .= '"', last if $c eq '"';
118 $s .= '$' . $self->scan_dollar(), next if $c eq '$';
120 $s .= '\\', last unless $$b =~ /\G(.)/sgc;
122 $self->{lineno
}++, next if $c eq "\n"; # line splice
123 # backslash escapes only $, `, ", \ in dq-string
124 $s .= '\\' unless $c =~ /^[\$`"\\]$/;
128 die("internal error scanning dq-string '$c'\n");
130 $self->{lineno
} += () = $s =~ /\n/sg;
135 my ($self, $c1, $c2) = @_;
136 my $b = $self->{buff
};
139 while ($$b =~ /\G([^\Q$c1$c2\E]*(?:[\Q$c1$c2\E]|\z))/gc) {
141 $depth++, next if $s =~ /\Q$c1\E$/;
145 $self->{lineno
} += () = $s =~ /\n/sg;
151 my @tokens = $self->{parser
}->parse(qr/^\)$/);
152 $self->{parser
}->next_token(); # closing ")"
158 my $b = $self->{buff
};
159 return $self->scan_balanced('(', ')') if $$b =~ /\G\((?=\()/gc; # $((...))
160 return '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\G\(/gc; # $(...)
161 return $self->scan_balanced('{', '}') if $$b =~ /\G\{/gc; # ${...}
162 return $1 if $$b =~ /\G(\w+)/gc; # $var
163 return $1 if $$b =~ /\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.
167 sub swallow_heredocs
{
169 my $b = $self->{buff
};
170 my $tags = $self->{heretags
};
171 while (my $tag = shift @
$tags) {
172 my $start = pos($$b);
173 my $indent = $$tag[0] =~ s/^\t// ?
'\\s*' : '';
174 $$b =~ /(?:\G|\n)$indent\Q$$tag[0]\E(?:\n|\z)/gc;
175 if (pos($$b) > $start) {
176 my $body = substr($$b, $start, pos($$b) - $start);
177 $self->{lineno
} += () = $body =~ /\n/sg;
180 push(@
{$self->{parser
}->{problems
}}, ['UNCLOSED-HEREDOC', $tag]);
181 $$b =~ /(?:\G|\n).*\z/gc; # consume rest of input
182 my $body = substr($$b, $start, pos($$b) - $start);
183 $self->{lineno
} += () = $body =~ /\n/sg;
190 my $b = $self->{buff
};
192 my ($start, $startln);
194 $startln = $self->{lineno
};
195 $$b =~ /\G[ \t]+/gc; # skip whitespace (but not newline)
196 $start = pos($$b) || 0;
197 $self->{lineno
}++, return ["\n", $start, pos($$b), $startln, $startln] if $$b =~ /\G#[^\n]*(?:\n|\z)/gc; # comment
199 # slurp up non-special characters
200 $token .= $1 if $$b =~ /\G([^\\;&|<>(){}'"\$\s]+)/gc;
201 # handle special characters
202 last unless $$b =~ /\G(.)/sgc;
204 pos($$b)--, last if $c =~ /^[ \t]$/; # whitespace ends token
205 pos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\n]$/;
206 $token .= $self->scan_sqstring(), next if $c eq "'";
207 $token .= $self->scan_dqstring(), next if $c eq '"';
208 $token .= $c . $self->scan_dollar(), next if $c eq '$';
209 $self->{lineno
}++, $self->swallow_heredocs(), $token = $c, last if $c eq "\n";
210 $token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;
211 $token = $c, last if $c =~ /^[(){}]$/;
213 $token .= '\\', last unless $$b =~ /\G(.)/sgc;
215 $self->{lineno
}++, next if $c eq "\n" && length($token); # line splice
216 $self->{lineno
}++, goto RESTART
if $c eq "\n"; # line splice
220 die("internal error scanning character '$c'\n");
222 return length($token) ?
[$token, $start, pos($$b), $startln, $self->{lineno
}] : undef;
225 # ShellParser parses POSIX shell scripts (with minor extensions for Bash). It
226 # is a recursive descent parser very roughly modeled after section 2.10 "Shell
227 # Grammar" of POSIX chapter 2 "Shell Command Language".
231 my ($class, $s) = @_;
237 $self->{lexer
} = Lexer
->new($self, $s);
243 return pop(@
{$self->{buff
}}) if @
{$self->{buff
}};
244 return $self->{lexer
}->scan_token();
249 push(@
{$self->{buff
}}, @_);
254 my $token = $self->next_token();
255 return undef unless defined($token);
256 $self->untoken($token);
261 my ($self, $token) = @_;
262 return 1 unless defined($token);
263 my $stop = ${$self->{stop
}}[-1] if @
{$self->{stop
}};
264 return defined($stop) && $token->[0] =~ $stop;
268 my ($self, $expect) = @_;
269 my $token = $self->next_token();
270 return $token if defined($token) && $token->[0] eq $expect;
271 push(@
{$self->{output
}}, "?!ERR?! expected '$expect' but found '" . (defined($token) ?
$token->[0] : "<end-of-input>") . "'\n");
272 $self->untoken($token) if defined($token);
276 sub optional_newlines
{
279 while (my $token = $self->peek()) {
280 last unless $token->[0] eq "\n";
281 push(@tokens, $self->next_token());
288 return ($self->parse(qr/^}$/),
294 return ($self->parse(qr/^\)$/),
298 sub parse_case_pattern
{
301 while (defined(my $token = $self->next_token())) {
302 push(@tokens, $token);
303 last if $token->[0] eq ')';
312 $self->next_token(), # subject
313 $self->optional_newlines(),
315 $self->optional_newlines());
317 my $token = $self->peek();
318 last unless defined($token) && $token->[0] ne 'esac';
320 $self->parse_case_pattern(),
321 $self->optional_newlines(),
322 $self->parse(qr/^(?:;;|esac)$/)); # item body
323 $token = $self->peek();
324 last unless defined($token) && $token->[0] ne 'esac';
327 $self->optional_newlines());
329 push(@tokens, $self->expect('esac'));
337 $self->next_token(), # variable
338 $self->optional_newlines());
339 my $token = $self->peek();
340 if (defined($token) && $token->[0] eq 'in') {
343 $self->optional_newlines());
346 $self->parse(qr/^do$/), # items
348 $self->optional_newlines(),
349 $self->parse_loop_body(),
350 $self->expect('done'));
359 $self->parse(qr/^then$/), # if/elif condition
360 $self->expect('then'),
361 $self->optional_newlines(),
362 $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body
363 my $token = $self->peek();
364 last unless defined($token) && $token->[0] eq 'elif';
365 push(@tokens, $self->expect('elif'));
367 my $token = $self->peek();
368 if (defined($token) && $token->[0] eq 'else') {
370 $self->expect('else'),
371 $self->optional_newlines(),
372 $self->parse(qr/^fi$/)); # else body
374 push(@tokens, $self->expect('fi'));
378 sub parse_loop_body
{
380 return $self->parse(qr/^done$/);
385 return ($self->parse(qr/^do$/), # condition
387 $self->optional_newlines(),
388 $self->parse_loop_body(),
389 $self->expect('done'));
394 return ($self->expect('('),
396 $self->optional_newlines(),
397 $self->parse_cmd()); # body
400 sub parse_bash_array_assignment
{
402 my @tokens = $self->expect('(');
403 while (defined(my $token = $self->next_token())) {
404 push(@tokens, $token);
405 last if $token->[0] eq ')';
411 '{' => \
&parse_group
,
412 '(' => \
&parse_subshell
,
413 'case' => \
&parse_case
,
414 'for' => \
&parse_for
,
416 'until' => \
&parse_loop
,
417 'while' => \
&parse_loop
);
421 my $cmd = $self->next_token();
422 return () unless defined($cmd);
423 return $cmd if $cmd->[0] eq "\n";
427 if ($cmd->[0] eq '!') {
428 push(@tokens, $self->parse_cmd());
430 } elsif (my $f = $compound{$cmd->[0]}) {
431 push(@tokens, $self->$f());
432 } elsif (defined($token = $self->peek()) && $token->[0] eq '(') {
433 if ($cmd->[0] !~ /\w=$/) {
434 push(@tokens, $self->parse_func());
437 my @array = $self->parse_bash_array_assignment();
438 $tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);
439 $tokens[-1]->[2] = $array[$#array][2] if @array;
442 while (defined(my $token = $self->next_token())) {
443 $self->untoken($token), last if $self->stop_at($token);
444 push(@tokens, $token);
445 last if $token->[0] =~ /^(?:[;&\n|]|&&|\|\|)$/;
447 push(@tokens, $self->next_token()) if $tokens[-1]->[0] ne "\n" && defined($token = $self->peek()) && $token->[0] eq "\n";
452 my ($self, $tokens, $cmd) = @_;
453 push(@
$tokens, @
$cmd);
457 my ($self, $stop) = @_;
458 push(@
{$self->{stop
}}, $stop);
459 goto DONE
if $self->stop_at($self->peek());
461 while (my @cmd = $self->parse_cmd()) {
462 $self->accumulate(\
@tokens, \
@cmd);
463 last if $self->stop_at($self->peek());
466 pop(@
{$self->{stop
}});
470 # TestParser is a subclass of ShellParser which, beyond parsing shell script
471 # code, is also imbued with semantic knowledge of test construction, and checks
472 # tests for common problems (such as broken &&-chains) which might hide bugs in
473 # the tests themselves or in behaviors being exercised by the tests. As such,
474 # TestParser is only called upon to parse test bodies, not the top-level
475 # scripts in which the tests are defined.
478 use base
'ShellParser';
481 my $class = shift @_;
482 my $self = $class->SUPER::new
(@_);
483 $self->{problems
} = [];
488 my $tokens = shift @_;
490 $n = $#$tokens if !defined($n);
491 $n-- while $n >= 0 && $$tokens[$n]->[0] eq "\n";
496 my ($tokens, $needles) = @_;
497 my $n = find_non_nl
($tokens);
498 for my $needle (reverse(@
$needles)) {
499 return undef if $n < 0;
500 $n = find_non_nl
($tokens, $n), next if $needle eq "\n";
501 return undef if $$tokens[$n]->[0] !~ $needle;
508 my ($tokens, $endings) = @_;
509 for my $needles (@
$endings) {
510 next if @
$tokens < scalar(grep {$_ ne "\n"} @
$needles);
511 return 1 if ends_with
($tokens, $needles);
516 sub parse_loop_body
{
518 my @tokens = $self->SUPER::parse_loop_body
(@_);
519 # did loop signal failure via "|| return" or "|| exit"?
520 return @tokens if !@tokens || grep {$_->[0] =~ /^(?:return|exit|\$\?)$/} @tokens;
521 # did loop upstream of a pipe signal failure via "|| echo 'impossible
522 # text'" as the final command in the loop body?
523 return @tokens if ends_with
(\
@tokens, [qr/^\|\|$/, "\n", qr/^echo$/, qr/^.+$/]);
524 # flag missing "return/exit" handling explicit failure in loop body
525 my $n = find_non_nl
(\
@tokens);
526 push(@
{$self->{problems
}}, ['LOOP', $tokens[$n]]);
531 [qr/^(?:&&|\|\||\||&)$/],
532 [qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/],
533 [qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/, qr/^;$/],
534 [qr/^(?:exit|return|continue)$/],
535 [qr/^(?:exit|return|continue)$/, qr/^;$/]);
538 my ($self, $tokens, $cmd) = @_;
539 my $problems = $self->{problems
};
541 # no previous command to check for missing "&&"
542 goto DONE
unless @
$tokens;
544 # new command is empty line; can't yet check if previous is missing "&&"
545 goto DONE
if @
$cmd == 1 && $$cmd[0]->[0] eq "\n";
547 # did previous command end with "&&", "|", "|| return" or similar?
548 goto DONE
if match_ending
($tokens, \
@safe_endings);
550 # if this command handles "$?" specially, then okay for previous
551 # command to be missing "&&"
552 for my $token (@
$cmd) {
553 goto DONE
if $token->[0] =~ /\$\?/;
556 # if this command is "false", "return 1", or "exit 1" (which signal
557 # failure explicitly), then okay for all preceding commands to be
559 if ($$cmd[0]->[0] =~ /^(?:false|return|exit)$/) {
560 @
$problems = grep {$_->[0] ne 'AMP'} @
$problems;
564 # flag missing "&&" at end of previous command
565 my $n = find_non_nl
($tokens);
566 push(@
$problems, ['AMP', $tokens->[$n]]) unless $n < 0;
569 $self->SUPER::accumulate
($tokens, $cmd);
572 # ScriptParser is a subclass of ShellParser which identifies individual test
573 # definitions within test scripts, and passes each test body through TestParser
574 # to identify possible problems. ShellParser detects test definitions not only
575 # at the top-level of test scripts but also within compound commands such as
576 # loops and function definitions.
577 package ScriptParser
;
579 use base
'ShellParser';
582 my $class = shift @_;
583 my $self = $class->SUPER::new
(@_);
588 # extract the raw content of a token, which may be a single string or a
589 # composition of multiple strings and non-string character runs; for instance,
590 # `"test body"` unwraps to `test body`; `word"a b"42'c d'` to `worda b42c d`
592 my $token = (@_ ?
shift @_ : $_)->[0];
593 # simple case: 'sqstring' or "dqstring"
594 return $token if $token =~ s/^'([^']*)'$/$1/;
595 return $token if $token =~ s/^"([^"]*)"$/$1/;
598 my ($s, $q, $escaped);
600 # slurp up non-special characters
601 $s .= $1 if $token =~ /\G([^\\'"]*)/gc;
602 # handle special characters
603 last unless $token =~ /\G(.)/sgc;
605 $q = undef, next if defined($q) && $c eq $q;
606 $q = $c, next if !defined($q) && $c =~ /^['"]$/;
608 last unless $token =~ /\G(.)/sgc;
610 $s .= '\\' if $c eq "\n"; # preserve line splice
619 my ($title, $body) = map(unwrap
, @_);
621 my $parser = TestParser
->new(\
$body);
622 my @tokens = $parser->parse();
623 my $problems = $parser->{problems
};
624 return unless $emit_all || @
$problems;
625 my $c = main
::fd_colors
(1);
626 my $lineno = $_[1]->[3];
629 for (sort {$a->[1]->[2] <=> $b->[1]->[2]} @
$problems) {
630 my ($label, $token) = @
$_;
631 my $pos = $token->[2];
632 $checked .= substr($body, $start, $pos - $start) . " ?!$label?! ";
635 $checked .= substr($body, $start);
636 $checked =~ s/^/$lineno++ . ' '/mge;
637 $checked =~ s/^\d+ \n//;
638 $checked =~ s/(\s) \?!/$1?!/mg;
639 $checked =~ s/\?! (\s)/?!$1/mg;
640 $checked =~ s/(\?![^?]+\?!)/$c->{rev}$c->{red}$1$c->{reset}/mg;
641 $checked =~ s/^\d+/$c->{dim}$&$c->{reset}/mg;
642 $checked .= "\n" unless $checked =~ /\n$/;
643 push(@
{$self->{output
}}, "$c->{blue}# chainlint: $title$c->{reset}\n$checked");
648 my @tokens = $self->SUPER::parse_cmd
();
649 return @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;
651 $n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\n|]|&&|\|\|)$/;
652 $self->check_test($tokens[1], $tokens[2]) if $n == 2; # title body
653 $self->check_test($tokens[2], $tokens[3]) if $n > 2; # prereq title body
657 # main contains high-level functionality for processing command-line switches,
658 # feeding input test scripts to ScriptParser, and reporting results.
661 my $getnow = sub { return time(); };
662 my $interval = sub { return time() - shift; };
663 if (eval {require Time
::HiRes
; Time
::HiRes
->import(); 1;}) {
664 $getnow = sub { return [Time
::HiRes
::gettimeofday
()]; };
665 $interval = sub { return Time
::HiRes
::tv_interval
(shift); };
668 # Restore TERM if test framework set it to "dumb" so 'tput' will work; do this
669 # outside of get_colors() since under 'ithreads' all threads use %ENV of main
670 # thread and ignore %ENV changes in subthreads.
671 $ENV{TERM
} = $ENV{USER_TERM
} if $ENV{USER_TERM
};
673 my @NOCOLORS = (bold
=> '', rev
=> '', dim
=> '', reset => '', blue
=> '', green
=> '', red
=> '');
676 return \
%COLORS if %COLORS;
677 if (exists($ENV{NO_COLOR
})) {
681 if ($ENV{TERM
} =~ /xterm|xterm-\d+color|xterm-new|xterm-direct|nsterm|nsterm-\d+color|nsterm-direct/) {
682 %COLORS = (bold
=> "\e[1m",
691 if (system("tput sgr0 >/dev/null 2>&1") == 0 &&
692 system("tput bold >/dev/null 2>&1") == 0 &&
693 system("tput rev >/dev/null 2>&1") == 0 &&
694 system("tput dim >/dev/null 2>&1") == 0 &&
695 system("tput setaf 1 >/dev/null 2>&1") == 0) {
696 %COLORS = (bold
=> `tput bold`,
699 reset => `tput sgr0`,
700 blue
=> `tput setaf 4`,
701 green
=> `tput setaf 2`,
702 red
=> `tput setaf 1`);
712 return $FD_COLORS{$fd} if exists($FD_COLORS{$fd});
713 $FD_COLORS{$fd} = -t
$fd ? get_colors
() : {@NOCOLORS};
714 return $FD_COLORS{$fd};
719 return $ENV{NUMBER_OF_PROCESSORS
} if exists($ENV{NUMBER_OF_PROCESSORS
});
720 # Linux / MSYS2 / Cygwin / WSL
721 do { local @ARGV='/proc/cpuinfo'; return scalar(grep(/^processor[\s\d]*:/, <>)); } if -r
'/proc/cpuinfo';
723 return qx/sysctl -n hw.ncpu/ if $^O
=~ /(?:^darwin$|bsd)/;
728 my ($start_time, $stats) = @_;
729 my $walltime = $interval->($start_time);
730 my ($usertime) = times();
731 my ($total_workers, $total_scripts, $total_tests, $total_errs) = (0, 0, 0, 0);
732 my $c = fd_colors
(2);
733 print(STDERR
$c->{green
});
735 my ($worker, $nscripts, $ntests, $nerrs) = @
$_;
736 print(STDERR
"worker $worker: $nscripts scripts, $ntests tests, $nerrs errors\n");
738 $total_scripts += $nscripts;
739 $total_tests += $ntests;
740 $total_errs += $nerrs;
742 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);
746 my ($id, $next_script, $emit) = @_;
747 my ($nscripts, $ntests, $nerrs) = (0, 0, 0);
748 while (my $path = $next_script->()) {
751 unless (open($fh, "<", $path)) {
752 $emit->("?!ERR?! $path: $!\n");
755 my $s = do { local $/; <$fh> };
757 my $parser = ScriptParser
->new(\
$s);
758 1 while $parser->parse_cmd();
759 if (@
{$parser->{output
}}) {
760 my $c = fd_colors
(1);
761 my $s = join('', @
{$parser->{output
}});
762 $emit->("$c->{bold}$c->{blue}# chainlint: $path$c->{reset}\n" . $s);
763 $nerrs += () = $s =~ /\?![^?]+\?!/g;
765 $ntests += $parser->{ntests
};
767 return [$id, $nscripts, $ntests, $nerrs];
771 my $stats = shift @_;
773 my ($worker, $nscripts, $ntests, $nerrs) = @
$_;
779 Getopt
::Long
::Configure
(qw{bundling
});
781 "emit-all!" => \
$emit_all,
782 "jobs|j=i" => \
$jobs,
783 "stats|show-stats!" => \
$show_stats) or die("option error\n");
784 $jobs = ncores
() if $jobs < 1;
786 my $start_time = $getnow->();
790 push(@scripts, File
::Glob
::bsd_glob
($_)) for (@ARGV);
792 show_stats
($start_time, \
@stats) if $show_stats;
796 unless ($Config{useithreads
} && eval {
797 require threads
; threads
->import();
798 require Thread
::Queue
; Thread
::Queue
->import();
801 push(@stats, check_script
(1, sub { shift(@scripts); }, sub { print(@_); }));
802 show_stats
($start_time, \
@stats) if $show_stats;
803 exit(exit_code
(\
@stats));
806 my $script_queue = Thread
::Queue
->new();
807 my $output_queue = Thread
::Queue
->new();
809 sub next_script
{ return $script_queue->dequeue(); }
810 sub emit
{ $output_queue->enqueue(@_); }
813 while (my $s = $output_queue->dequeue()) {
818 my $mon = threads
->create({'context' => 'void'}, \
&monitor
);
819 threads
->create({'context' => 'list'}, \
&check_script
, $_, \
&next_script
, \
&emit
) for 1..$jobs;
821 $script_queue->enqueue(@scripts);
822 $script_queue->end();
824 for (threads
->list()) {
825 push(@stats, $_->join()) unless $_ == $mon;
828 $output_queue->end();
831 show_stats
($start_time, \
@stats) if $show_stats;
832 exit(exit_code
(\
@stats));