我正在尝试将标准输入传递给多个命令并比较它们的输出。我当前的尝试似乎很接近,但不太有效 - 而且它依赖于我认为没有必要的临时文件。
我希望我的脚本执行以下操作的示例:
$ echo '
> Line 1
> Line B
> Line iii' | ./myscript.sh 'sed s/B/b/g' 'sed s/iii/III/' 'cat'
1:Line B 2:Line b
1:Line iii 3:Line III
到目前为止我有这个:
i=0
SOURCES=()
TARGETS=()
for c in "$@"; do
SOURCES+=(">($c > tmp-$i)")
TARGETS+=("tmp-$i")
i=$((i+1))
done
eval tee ${SOURCES[@]} >/dev/null <&0
comm ${TARGETS[@]}
问题是:
- 似乎存在竞争条件。在执行结束时, comm tmp-0 tmp-1 具有所需的输出(或多或少),但从脚本执行时,输出似乎是不确定的。
- 这仅限于 2 个输入,但我至少需要 3 个(最好是任何数字)
- 这会创建临时文件,我必须跟踪并随后删除这些文件,理想的解决方案仅使用重定向
限制条件是:
- 输入可能还没有结束。特别是,输入可能类似于 /dev/zero 或 /dev/urandom,因此仅仅将输入复制到文件是行不通的。
- 命令中可能有空格并且本身相当复杂
- 我想要逐行、按顺序进行比较。
知道我该如何实施这个吗?我基本上想要类似的东西,echo $input | tee >(A >?) >(B >?) >(C >?) ?(compare-all-files)
如果只有这样的语法存在的话。
答案1
由于接受的答案是使用perl
,因此您也可以在 中完成整个操作perl
,无需其他非标准工具和非标准 shell 功能,并且不会在内存中加载不可预测的长数据块,或其他此类可怕的错误功能。
ytee
当以这种方式使用时,该答案末尾的脚本:
ytee command filter1 filter2 filter3 ...
会像
command <(filter1) <(filter2) <(filter3) ...
其标准输入通过管道并行传输到filter1
, filter2
, filter3
, ... ,就好像它与
tee >(filter1) >(filter2) >(filter3) ...
例子:
echo 'Line 1
Line B
Line iii' | ytee 'paste' 'sed s/B/b/g | nl' 'sed s/iii/III/ | nl'
1 Line 1 1 Line 1
2 Line b 2 Line B
3 Line iii 3 Line III
ytee:
#! /usr/bin/perl
# usage: ytee [-r irs] { command | - } [filter ..]
use strict;
if($ARGV[0] =~ /^-r(.+)?/){ shift; $/ = eval($1 // shift); die $@ if $@ }
elsif(! -t STDIN){ $/ = \0x8000 }
my $cmd = shift;
my @cl;
for(@ARGV){
use IPC::Open2;
my $pid = open2 my $from, my $to, $_;
push @cl, [$from, $to, $pid];
}
defined(my $pid = fork) or die "fork: $!";
if($pid){
delete $$_[0] for @cl;
$SIG{PIPE} = 'IGNORE';
my ($s, $n);
while(<STDIN>){
for my $c (@cl){
next unless exists $$c[1];
syswrite($$c[1], $_) ? $n++ : delete $$c[1]
}
last unless $n;
}
delete $$_[1] for @cl;
while((my $p = wait) > 0){ $s += !!$? << ($p != $pid) }
exit $s;
}
delete $$_[1] for @cl;
if($cmd eq '-'){
my $n; do {
$n = 0; for my $c (@cl){
next unless exists $$c[0];
if(my $d = readline $$c[0]){ print $d; $n++ }
else{ delete $$c[0] }
}
} while $n;
}else{
exec join ' ', $cmd, map {
use Fcntl;
fcntl $$_[0], F_SETFD, fcntl($$_[0], F_GETFD, 0) & ~FD_CLOEXEC;
'/dev/fd/'.fileno $$_[0]
} @cl;
die "exec $cmd: $!";
}
笔记:
类似的代码
delete $$_[1] for @cl
不仅会从数组中删除文件句柄,还会立即关闭它们,因为没有其他参考指向它们;这与(正确的)垃圾收集语言(如javascript
.的退出状态
ytee
将反映命令的退出状态和过滤器;这可以改变/简化。
答案2
这更简单:
#!bash
if [[ -t 0 ]]; then
echo "Error: you must pipe data into this script"
exit 1
fi
input=$(cat)
commands=$( "$@" )
outputs=()
for cmd in "${commands[@]}"; do
echo "calling: $cmd"
outputs+=( "$( $cmd <<<"$input" )" )
done
# now, do stuff with "${outputs[0]}", "${outputs[1]}", etc
这是未经测试的。该outputs+=...
线特别脆弱:参见http://mywiki.wooledge.org/BashFAQ/050
答案3
如果行长于 RAM 大小,此操作将会失败。
#!/bin/bash
commands=('sed s/8/b/g' 'sed s/7/III/' cat)
parallel 'rm -f fifo-{#};mkfifo fifo-{#}' ::: "${commands[@]}"
cat input |
parallel -j0 --tee --pipe 'eval {} > fifo-{#}' ::: "${commands[@]}" &
perl -e 'for(@ARGV){ open($in{$_},"<",$_) }
do{
@in = map { $f=$in{$_}; scalar <$f> } @ARGV;
print grep { $in[0] ne $_ } @in;
} while (not grep { eof($in{$_}) } @ARGV)' fifo-*