4 use Getopt
::Long qw
/:config no_ignore_case bundling no_getopt_compat/;
5 use Fcntl qw
/F_GETFL F_SETFL O_NONBLOCK/;
8 use List
::MoreUtils qw
/all any none/;
9 no if ($] >= 5.018), 'warnings' => 'experimental::smartmatch';
18 warn @_ if $ENV{DEBUG
};
24 pop @Filters while @Filters;
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], $@
;
32 pod2usage
(-exitval
=>0, -verbose
=>99);
35 '<>' => sub { unshift @ARGV, @_[0]; die '!FINISH'; },
39 process_args
or exit 2;
43 pod2usage
(-exitval
=>2, -verbose
=>99);
50 open my $fh, '<', $filepath or die "$filepath: $!\n";
57 push @Filters, parse_filter_expr
($_, \
@Filters);
59 } or die "$0: filter expression error in $filepath in line $.:\n$@";
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
}};
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;
99 delete $filt->{match_tags
}->{$1};
102 elsif(s/^\[(.+?)\]//)
104 if(exists $filt->{pattern
})
106 push @
{$filt->{tag_start
}}, $1;
110 $filt->{match_tags
}->{$1} = 1;
113 elsif(s{^(s/.*?(?<!\\)/.*?(?<!\\)/[a-zA-Z]*)}{})
116 eval 'my $test = ""; $test =~ '.$replacer.'; 1' or die "$0: error in s/// expression: $@\n";
117 $filt->{replacer
} = $replacer;
121 die "$0: can not parse expression: $_\n";
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
}};
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
156 if(exists $rule->{replacer
})
158 eval '$line =~ '.$rule->{replacer
}.'; 1' or warn "$0: $@\n";
159 $result->{line
} = $line;
162 if(exists $rule->{tag_start
})
164 $context->{tags
}->{$_} = 1 for @
{$rule->{tag_start
}};
167 if(exists $rule->{tag_end
})
169 delete $context->{tags
}->{$_} for @
{$rule->{tag_end
}};
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
};
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;
214 apply_filter_rule
(\
%applicable_rule, $line, $context, $result);
219 if(not %applicable_rule)
227 apply_filter_rule
(\
%applicable_rule, $line, $context, $result);
238 my $filt_result = evaluate_filters
($line, $stream->{filter_context
});
239 print {$stream->{'write_fh'}} $filt_result->{line
} if $filt_result->{pass
};
247 my $nl = index $stream->{'buf'}, "\n";
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";
268 open STDOUT
, '>&', $stdout_w or die "$0: replace stdout: $!\n";
269 open STDERR
, '>&', $stderr_w or die "$0: replace stderr: $!\n";
273 exec {$ARGV[0]} @ARGV;
274 my ($errno, $errstr) = (int $!, $!);
275 warn "$0: ${ARGV[0]}: $errstr\n";
280 my $child_status = undef;
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
);
293 'write_fh' => \
*STDOUT
,
294 'fd' => fileno $stdout_r,
299 'write_fh' => \
*STDERR
,
300 'fd' => fileno $stderr_r,
305 sub reset_stream_contexts
307 $streams->{'out'}->{'filter_context'} = {
312 $streams->{'err'}->{'filter_context'} = {
319 reset_stream_contexts
();
321 $SIG{'HUP'} = sub { $do_reload = 1; };
327 for my $stream_name (keys %$streams)
329 vec($fds, $streams->{$stream_name}->{'fd'}, 1) = 1 if defined $streams->{$stream_name}->{'fh'};
334 select($fds, undef, undef, undef);
340 reset_stream_contexts
();
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'};
353 process_stream
($streams->{$stream_name});
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);
368 $exit_status = WEXITSTATUS
($child_status);
369 $exit_status = 128 + WTERMSIG
($child_status) if WIFSIGNALED
($child_status);
379 stdfilt - Run a command but filter its STDOUT and STDERR
383 stdfilt [I<OPTIONS>] [--] I<COMMAND> [I<ARGS>]
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.
396 =item -F, --filter-file I<FILE>
398 =item -f, --filter I<EXPR>
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>]
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]
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":
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,
458 So type it as C<\x5C> instead.
460 Further limitation, that only slash C</> can be used, others, eg. C<m{...}> not.
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.
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).
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>.
494 This prepends a TAB char to each lines in the output stream which are between the lines containing "BEGIN" and "END".
500 HUP - re-read filter files given at command line
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...
514 grep(1), stdbuf(1), logwall(8), perlre(1)