Passe a entrada para múltiplos comandos e compare suas saídas

3

Estou tentando passar a entrada padrão para vários comandos e comparar suas saídas. Minha tentativa atual parece próxima, mas não funciona - além disso, ela depende de arquivos temporários que eu sinto que não seriam necessários.

Um exemplo do que eu gostaria que meu script fizesse:

$ 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

Até agora eu tenho isso:

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[@]}

Os problemas são:

  • Parece haver uma condição de corrida. No final da execução, comm tmp-0 tmp-1 tem a saída desejada (mais ou menos), mas quando executada a partir do script, a saída parece não determinística.
  • Isso é limitado a apenas 2 entradas, mas eu preciso de pelo menos 3 (idealmente qualquer número)
  • Isso cria arquivos temporários que eu teria que acompanhar e excluir depois, uma solução ideal só usaria o redirecionamento

As restrições são:

  • A entrada pode não estar terminando. Em particular, a entrada poderia ser algo como / dev / zero ou / dev / urandom, portanto, apenas copiar a entrada para um arquivo não funcionará.
  • Os comandos podem ter espaços e ser bastante complicados
  • Quero uma comparação linha a linha e em ordem.

Alguma idéia de como eu poderia implementar isso? Eu basicamente quero algo como echo $input | tee >(A >?) >(B >?) >(C >?) ?(compare-all-files) se apenas existisse tal sintaxe.

    
por LambdaBeta 03.10.2018 / 21:41

3 respostas

2

Como a resposta aceita é usar perl , você também pode fazer a coisa toda em perl , sem outras ferramentas não-padrão e recursos de shell não padrão, e sem carregar blocos de dados imprevisivelmente longos no memória, ou outros tais misfeatures horríveis.

O script ytee do final desta resposta, quando usado desta maneira:

ytee command filter1 filter2 filter3 ...

funcionará como

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

com sua entrada padrão canalizada para filter1 , filter2 , filter3 , ... em paralelo, como se estivesse com

tee >(filter1) >(filter2) >(filter3) ...

Exemplo:

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

Esta também é uma resposta para as duas perguntas muito semelhantes: aqui e aqui .

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){ $/ = 
ytee command filter1 filter2 filter3 ...
x8000 } 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: $!"; }

notas:

  1. o código como delete $$_[1] for @cl removerá não apenas as alças de arquivo da matriz, mas também as fechará imediatamente , porque não há outra referência apontando para elas; isso é diferente de (corretamente) idiomas coletados como lixo javascript .

  2. o status de saída de ytee refletirá os status de saída dos filtros de comando e ; isso pode ser alterado / simplificado.

por 10.10.2018 / 11:48
2

Isso é mais simples:

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

Isso não foi testado. A linha outputs+=... é particularmente frágil: consulte o link

    
por 03.10.2018 / 22:57
1

Isso falhará se as linhas forem maiores que o tamanho da 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-*
    
por 03.10.2018 / 22:47