将输入传递给多个命令并比较它们的输出

将输入传递给多个命令并比较它们的输出

我正在尝试将标准输入传递给多个命令并比较它们的输出。我当前的尝试似乎很接近,但不太有效 - 而且它依赖于我认为没有必要的临时文件。

我希望我的脚本执行以下操作的示例:

$ 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: $!";
}

笔记:

  1. 类似的代码delete $$_[1] for @cl不仅会从数组中删除文件句柄,还会立即关闭它们,因为没有其他参考指向它们;这与(正确的)垃圾收集语言(如javascript.

  2. 的退出状态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-*

相关内容