Bash
將輸入傳遞給多個命令並比較它們的輸出
我正在嘗試將標準輸入傳遞給多個命令並比較它們的輸出。我目前的嘗試似乎很接近,但不太奏效——而且它依賴於我認為沒有必要的臨時文件。
我希望我的腳本執行的一個範例:
$ 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)
如果只存在這樣的語法。
由於公認的答案是 using
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
將反映命令和過濾器的退出狀態;這可以改變/簡化。