複数のコマンドに標準入力を渡し、出力を比較しようとしています。私の現在の試みは似ているようですが、それほど効率的ではありません。そしてそれは必要ではないと思う一時ファイルに依存します。
スクリプトで実行したい操作の例:
$ 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
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
これは非常によく似た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: $!";
}
メモ:
同様のコードは
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-*