複数のコマンドに入力を渡し、出力を比較します。

複数のコマンドに入力を渡し、出力を比較します。

複数のコマンドに標準入力を渡し、出力を比較しようとしています。私の現在の試みは似ているようですが、それほど効率的ではありません。そしてそれは必要ではないと思う一時ファイルに依存します。

スクリプトで実行したい操作の例:

$ 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

許容される答えはperlperl

yteeこの方法では、この回答の最後にあるスクリプトは次のようになります。

ytee command filter1 filter2 filter3 ...

同じだろう

command <(filter1) <(filter2) <(filter3) ...

標準入力は、まるで、、...と並列filter1filter2パイプされます。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

これは非常によく似た2つの質問に対する答えでもあります。ここそしてここ

イティ:

#! /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-*

関連情報