stdfilt to access child process pid
[hband-tools.git] / user-tools / stdfilt
blob2b007346adf18b3b643a17315f9bf942f6aa2c09
1 #!/usr/bin/env perl
3 use Data::Dumper;
4 use Getopt::Long qw/:config no_ignore_case bundling no_getopt_compat/;
5 use Fcntl qw/F_GETFL F_SETFL O_NONBLOCK/;
6 use POSIX;
7 use Pod::Usage;
8 use List::MoreUtils qw/all any none/;
9 no if ($] >= 5.018), 'warnings' => 'experimental::smartmatch';
12 @Filters = ();
13 @initialARGV = @ARGV;
16 sub debug
18 warn @_ if $ENV{DEBUG};
21 sub process_args
23 @ARGV = @initialARGV;
24 pop @Filters while @Filters;
25 return GetOptions(
26 'F|filter-file=s@' => sub { parse_filter_file($_[1]); },
27 'f|filter=s@' => sub {
28 eval { push @Filters, parse_filter_expr($_[1], \@Filters); 1; }
29 or die sprintf "%s: error in filter expression %d: %s\n%s", $0, scalar @Filters, $_[1], $@;
31 'help' => sub {
32 pod2usage(-exitval=>0, -verbose=>99);
33 exit 0;
35 '<>' => sub { unshift @ARGV, @_[0]; die '!FINISH'; },
39 process_args or exit 2;
41 if(not @ARGV)
43 pod2usage(-exitval=>2, -verbose=>99);
47 sub parse_filter_file
49 my $filepath = shift;
50 open my $fh, '<', $filepath or die "$filepath: $!\n";
51 while(<$fh>)
53 chomp;
54 s/(^|\s+)#.*$//;
55 next if /^$/;
56 eval {
57 push @Filters, parse_filter_expr($_, \@Filters);
59 } or die "$0: filter expression error in $filepath in line $.:\n$@";
61 close $fh;
64 sub parse_filter_expr
66 local $_ = shift;
67 my $prev_filters = shift;
68 my $prev_filter = $prev_filters->[-1];
69 my $filt = { expr=>$_, };
71 if(@$prev_filters and %{$prev_filter->{section_tags}})
73 $filt->{section_tags}->{$_} = $filt->{match_tags}->{$_} = $prev_filter->{section_tags}->{$_} for keys %{$prev_filter->{section_tags}};
76 my $remainder = $_;
77 while($_)
79 s/^\s*//;
80 if(s{^(?'negate'!|)\s*/(?'pattern'.*?)(?<!\\)/(?'modifiers'[a-zA-Z]*)\s*(\+(?'offset'\d+))?}{})
82 if(exists $filt->{replacer})
84 die "$0: pattern matching can not follow a replace: $remainder\n";
86 $filt->{negate} = $+{negate} ? 1 : 0;
87 $filt->{pattern} = $+{pattern};
88 $filt->{modifiers} = $+{modifiers};
89 $filt->{offset} = $+{offset};
91 elsif(s/^\[\/(.+?)\]//)
93 if(exists $filt->{pattern})
95 push @{$filt->{tag_end}}, $1;
97 else
99 delete $filt->{match_tags}->{$1};
102 elsif(s/^\[(.+?)\]//)
104 if(exists $filt->{pattern})
106 push @{$filt->{tag_start}}, $1;
108 else
110 $filt->{match_tags}->{$1} = 1;
113 elsif(s{^(s/.*?(?<!\\)/.*?(?<!\\)/[a-zA-Z]*)}{})
115 my $replacer = $1;
116 eval 'my $test = ""; $test =~ '.$replacer.'; 1' or die "$0: error in s/// expression: $@\n";
117 $filt->{replacer} = $replacer;
119 else
121 die "$0: can not parse expression: $_\n";
124 $remainder = $_;
127 # if it's a "[blue]" type of line, ie. a lone tag specification, then it opens a section of rules, all of which must match to "blue" tag.
128 if(all {not exists $filt->{$_}} qw/pattern replacer tag_start tag_end/)
130 if(exists $filt->{match_tags})
132 $filt->{section_tags} = {};
133 $filt->{section_tags}->{$_} = $filt->{match_tags}->{$_} for keys %{$filt->{match_tags}};
137 debug Dumper $filt;
138 return $filt;
141 sub stream_tags_match
143 my $current_tags = shift;
144 my $mandatory_tags = shift;
145 return all { $_ ~~ [keys %$current_tags] } keys %$mandatory_tags;
148 sub apply_filter_rule
150 my $rule = shift;
151 my $line = shift;
152 my $context = shift;
153 my $result = shift;
154 my $applied = 0;
156 if(exists $rule->{replacer})
158 eval '$line =~ '.$rule->{replacer}.'; 1' or warn "$0: $@\n";
159 $result->{line} = $line;
160 $applied = 1;
162 if(exists $rule->{tag_start})
164 $context->{tags}->{$_} = 1 for @{$rule->{tag_start}};
165 $applied = 1;
167 if(exists $rule->{tag_end})
169 delete $context->{tags}->{$_} for @{$rule->{tag_end}};
170 $applied = 1;
172 return $applied;
175 sub evaluate_filters
177 my $line = shift;
178 my $context = shift;
179 my $result = {pass => 1, line => $line,};
181 for my $fnum (0 .. $#Filters)
183 my $filt = $Filters[$fnum];
185 debug Dumper { filter => $filt, context => $context, };
187 $context->{delayed_rule}->[$fnum]->{count_down} -= 1 if $context->{delayed_rule}->[$fnum]->{count_down} > 0;
188 if(%{$context->{delayed_rule}->[$fnum]} and $context->{delayed_rule}->[$fnum]->{count_down} <= 0)
190 debug Dumper { apply_delayed_rule => $context->{delayed_rule}->[$fnum]->{rule}, };
191 apply_filter_rule($context->{delayed_rule}->[$fnum]->{rule}, $line, $context, $result);
192 delete $context->{delayed_rule}->[$fnum];
195 next if not stream_tags_match($context->{tags}, $filt->{match_tags});
197 my %applicable_rule = map {($_=>$filt->{$_})} grep {$_ ~~ [qw/tag_start tag_end replacer/]} keys %$filt;
199 if(exists $filt->{pattern})
201 my $pattern_matched = $line =~ m/(?$filt->{modifiers})$filt->{pattern}/;
202 $pattern_matched = !$pattern_matched if $filt->{negate};
203 if($pattern_matched)
205 if($filt->{offset})
207 debug Dumper { delay_rule_application => \%applicable_rule, };
208 $context->{delayed_rule}->[$fnum]->{count_down} = $filt->{offset};
209 $context->{delayed_rule}->[$fnum]->{rule} = \%applicable_rule;
211 else
213 $result->{pass} = 1;
214 apply_filter_rule(\%applicable_rule, $line, $context, $result);
217 else
219 if(not %applicable_rule)
221 $result->{pass} = 0;
225 else
227 apply_filter_rule(\%applicable_rule, $line, $context, $result);
231 return $result;
234 sub process_line
236 my $line = shift;
237 my $stream = shift;
238 my $filt_result = evaluate_filters($line, $stream->{filter_context});
239 print {$stream->{'write_fh'}} $filt_result->{line} if $filt_result->{pass};
242 sub process_stream
244 my $stream = shift;
245 while(1)
247 my $nl = index $stream->{'buf'}, "\n";
248 last if $nl == -1;
249 $nl++;
251 my $line = substr $stream->{'buf'}, 0, $nl;
252 process_line $line, $stream;
254 $stream->{'buf'} = substr $stream->{'buf'}, $nl;
260 pipe($stdout_r, $stdout_w) or die "$0: pipe: $!\n";
261 pipe($stderr_r, $stderr_w) or die "$0: pipe: $!\n";
264 $CHILD_PID = fork // die "$0: fork: $!\n";
266 if($CHILD_PID == 0)
268 open STDOUT, '>&', $stdout_w or die "$0: replace stdout: $!\n";
269 open STDERR, '>&', $stderr_w or die "$0: replace stderr: $!\n";
270 select STDERR; $|++;
271 select STDOUT; $|++;
273 exec {$ARGV[0]} @ARGV;
274 my ($errno, $errstr) = (int $!, $!);
275 warn "$0: ${ARGV[0]}: $errstr\n";
276 exit 125+$errno;
280 my $child_status = undef;
281 close STDIN;
282 close $stdout_w;
283 close $stderr_w;
284 fcntl($stdout_r, F_SETFL, fcntl($stdout_r, F_GETFL, 0) | O_NONBLOCK);
285 fcntl($stderr_r, F_SETFL, fcntl($stderr_r, F_GETFL, 0) | O_NONBLOCK);
286 select STDERR; $|++;
287 select STDOUT; $|++;
290 $streams = {
291 'out' => {
292 'fh' => $stdout_r,
293 'write_fh' => \*STDOUT,
294 'fd' => fileno $stdout_r,
295 'buf' => '',
297 'err' => {
298 'fh' => $stderr_r,
299 'write_fh' => \*STDERR,
300 'fd' => fileno $stderr_r,
301 'buf' => '',
305 sub reset_stream_contexts
307 $streams->{'out'}->{'filter_context'} = {
308 'tags' => {
309 'STDOUT' => 1,
312 $streams->{'err'}->{'filter_context'} = {
313 'tags' => {
314 'STDERR' => 1,
319 reset_stream_contexts();
321 $SIG{'HUP'} = sub { $do_reload = 1; };
324 while(1)
326 $fds = '';
327 for my $stream_name (keys %$streams)
329 vec($fds, $streams->{$stream_name}->{'fd'}, 1) = 1 if defined $streams->{$stream_name}->{'fh'};
331 last if $fds eq '';
333 $! = 0;
334 select($fds, undef, undef, undef);
335 my $errno = int $!;
337 if($do_reload)
339 $do_reload = 0;
340 reset_stream_contexts();
341 process_args();
344 next if $errno;
346 for my $stream_name (keys %$streams)
348 if(vec($fds, $streams->{$stream_name}->{'fd'}, 1) == 1)
350 my $bytes = sysread $streams->{$stream_name}->{'fh'}, $streams->{$stream_name}->{'buf'}, 1024, length $streams->{$stream_name}->{'buf'};
351 if($bytes)
353 process_stream($streams->{$stream_name});
355 else
357 # this stream is closed.
358 undef $streams->{$stream_name}->{'fh'};
359 # process last unterminated line (if any)
360 process_line($streams->{$stream_name}->{'buf'}, $streams->{$stream_name}) if length $streams->{$stream_name}->{'buf'};
366 waitpid($CHILD_PID, 0);
367 $child_status = $?;
368 $exit_status = WEXITSTATUS($child_status);
369 $exit_status = 128 + WTERMSIG($child_status) if WIFSIGNALED($child_status);
370 exit $exit_status;
373 __END__
375 =pod
377 =head1 NAME
379 stdfilt - Run a command but filter its STDOUT and STDERR
381 =head1 SYNOPSIS
383 stdfilt [I<OPTIONS>] [--] I<COMMAND> [I<ARGS>]
385 =head1 DESCRIPTION
387 Run I<COMMAND> and match each of its output lines (both stdout and stderr separately) against
388 the filter rules given by command arguments (B<-f>) or in files (B<-F>).
389 All filter expressions are evaluated and the last matching rule wins.
390 So it's a good idea to add wider matching patterns first, then the more specific ones later.
392 =head1 OPTIONS
394 =over 4
396 =item -F, --filter-file I<FILE>
398 =item -f, --filter I<EXPR>
400 =back
402 =head1 FILTER FILE FORMAT
404 Empty and comments are ignored as well as leading whitespace.
405 Comment is everything after a hashmark (C<#>) preceded by whitespace or the while line if it starts with a hashmark.
407 Each line is a filter rule, of which syntax is:
409 [B<match_tags>] [B<pattern> [B<offset>]] [B<replacer>] [B<set_tags>]
411 =over 4
413 =item B<match_tags>
415 Tag names, each of them in square-bracket (eg. C<[blue] [red]>).
416 The rest of the rule will be evaluated only if the tags are on the current stream.
417 Tags can be added, removed by the B<set_tags> element.
419 If a rule only consists of B<match_tags> tags, it opens a section in the filter file (and in B<-f> arguments too).
420 In this section, all rules are interpreted as they had the given B<match_tags> of the section written in them.
421 For example this filter-set selects all ranges in the output (and stderr) stream bound by those regexp patterns inclusively,
422 and blocks everying in them except "errors":
424 /begin checking procedure/ [checking]
425 /checking finished/+1 [/checking]
426 [checking]
428 /error/i
429 [/checking]
431 The 2 streams, stdout and stderr are tagged by default by "STDOUT" and "STDERR" respectively:
432 So this filters out everying in the stdout except "errors":
434 [STDOUT]
436 /error/i
437 [/STDOUT]
439 =item B<pattern>
441 Regexp pattern (perlre(1)) to match to the streams' (stdout and stderr) lines.
442 In the form of C<< /B<PATTERN>/B<MODIFIERS> >>.
443 Optionally prefixed with an exclamation mark (C<!>) which negates the result.
445 Pass every line by C<//>.
446 Exclude every line by C<!//>.
448 If there is a B<pattern> in the rule, replacement or tagging will only take place
449 if the pattern matched (or not matched if it was negated).
451 If there is no B<pattern>, only B<match_tags> controls if the rest will be applied or not.
453 You may escape slash (C</>) in the B<PATTERN> normally as it's customary in Perl,
454 by backslash, but to keep the filter expression parsing simple,
455 an escaped backslash itself (double backslash) at the end of the regexp pattern,
456 ie. just before the closing slash,
457 won't be noticed.
458 So type it as C<\x5C> instead.
460 Further limitation, that only slash C</> can be used, others, eg. C<m{...}> not.
462 =item B<offset>
464 A B<pattern> may be followed by a plus sign and a number (C<< +B<N> >>)
465 to denote that the given action (string replacement, or tagging)
466 should take effect after the given number of lines.
468 This way you can exclude the triggering line from the tagging.
470 A B<pattern> with B<offset> but without B<replacer> or B<set_tags> is meaningless.
472 =item B<replacer>
474 A C<s///> string substitution Perl expression.
475 Optionally with modifiers.
476 This can be abused to execute any perl code (with the "e" modifier).
478 =item B<set_tags>
480 The syntax is the same as for B<match_tags>.
481 But is the square-bracketed tags are at the right of the B<pattern>,
482 then the tags are applied to the stream.
484 Remove tags by a leading slash, like C<[/blue]>.
486 B<set_tags> is useful with a B<pattern>.
488 Example filter:
490 /BEGIN/ [keyblock]
491 /END/ [/keyblock]
492 [keyblock] s/^/\t/
494 This prepends a TAB char to each lines in the output stream which are between the lines containing "BEGIN" and "END".
496 =back
498 =head1 SIGNALS
500 HUP - re-read filter files given at command line
502 =head1 EXAMPLES
504 Prefix each output (and stderr) lines with the I<COMMAND> process'es PID:
506 stdfilt -f 's/^/$CHILD_PID: /' some_command...
508 Prefix each line with STDOUT/STDERR:
510 stdfile -f '[STDOUT]' -f 's/^/STDOUT: /' -f '[/STDOUT]' -f '[STDERR]' -f 's/^/STDERR: /' -f '[/STDERR]' some_command...
512 =head1 SEE ALSO
514 grep(1), stdbuf(1), logwall(8), perlre(1)
516 =cut