Como executar um cano com segurança e sequencialmente?

3

No Linux, é possível executar um pipe:

cmd1 | cmd2

de tal forma que:

  1. cmd2 não começa a ser executado até cmd1 ter terminado completamente e

  2. Se cmd1 tiver um erro, cmd2 não será executado e o status de saída do canal será o status de saída de cmd1 .

Para dar um exemplo, como fazer esse pipe:

false | echo ok

não produz nada e retorna um status diferente de zero?

Solução com falha 1

set -o pipefail

O canal tem um status de saída diferente de zero, mas cmd2 ainda é executado, mesmo se cmd1 falhar.

Solução com falha 2

cmd1 && cmd2

Isso não é um pipe. Nenhum redirecionamento de E / S.

Solução falhada 3

mkfifo /tmp/fifo
cmd1 > /tmp/fifo && cmd2 < /tmp/fifo

Bloqueia.

Solução subótima

touch /tmp/file
cmd1 > /tmp/file && cmd2 < /tmp/file

Isso parece estar funcionando. Mas tem várias deficiências:

  1. Grava dados no disco em que a E / S é mais lenta. (Certamente você pode usar tmpfs , mas esse é um requisito adicional do sistema).

  2. Você precisa escolher o nome do arquivo temporário com cuidado. Caso contrário, pode substituir acidentalmente um arquivo existente. mktemp pode ajudar, mas um pipe sem nome salva você completamente a tarefa de nomeação.

  3. O sistema de arquivos no qual o arquivo temporário reside pode não ser grande o suficiente para armazenar os dados inteiros.

  4. O arquivo temporário não faz a limpeza automática.

por Cyker 23.08.2017 / 23:49

5 respostas

4

Não sabemos o tamanho da saída de cmd1 , mas os canais têm um tamanho de buffer limitado . Uma vez que essa quantidade de dados tenha sido gravada no pipe, qualquer gravação subsequente será bloqueada até que alguém leia o canal (tipo de sua solução com falha 3).

Você deve usar um mecanismo que garanta não bloquear. Para dados muito grandes, use um arquivo temporário. Senão, se você puder pagar por manter os dados na memória (essa era a ideia, afinal, com pipes), use isto:

result=$(cmd1) && cmd2 < <(printf '%s' "$result")
unset result

Aqui, o resultado de cmd1 é armazenado na variável result . Se cmd1 for bem-sucedida, cmd2 será executado e será alimentado com os dados em result . Finalmente, result não está configurado para liberar a memória associada.

Nota: antigamente, eu usava uma string aqui ( <<< "$result" ) para alimentar cmd2 com dados, mas Stéphane Chazelas observou que bash criaria um arquivo temporário, o que você não deseja.

Respostas a perguntas no comentário:

  • Sim, os comandos podem ser encadeados ad libitum :

    result=$(cmd1) \
    && result=$(cmd2 < <(printf '%s' "$result")) \
    && result=$(cmd3 < <(printf '%s' "$result")) \
    ...
    && cmdN < <(printf '%s' "$result")
    unset result
    
  • Não, a solução acima não é adequada para dados binários porque:

    1. Substituição de comando $(...) come todas as novas linhas finais.
    2. O comportamento não é especificado para caracteres NUL ( base64 ) no resultado de uma substituição de comando (por exemplo, o Bash os descartaria).
  • Sim, para contornar todos esses problemas com dados binários, você pode usar um codificador como uuencode (ou >(...) , ou um codificador caseiro que cuida apenas dos caracteres NUL e das novas linhas iniciais):

    result=$(cmd1 > >(base64)) && cmd2 < <(printf '%s' "$result" | base64 -d)
    unset result
    

    Aqui, tive que usar uma substituição de processo ( cmd1 ) para manter o valor de saída %code% intacto.

Dito isso, mais uma vez parece ser um incômodo apenas garantir que os dados não sejam gravados no disco. Um arquivo temporário intermediário é uma solução melhor. Veja a resposta de Stéphane que aborda a maioria de suas preocupações sobre isso.

    
por 24.08.2017 / 01:55
2

O ponto principal dos comandos de tubulação é executá-los simultaneamente com um lendo a saída do outro. Se você quiser executá-los sequencialmente, e se mantivermos a metáfora do encanamento, você precisará canalizar a saída do primeiro comando para um bucket (armazená-lo) e depois esvaziar o bucket no outro comando.

Mas fazer isso com pipes significa ter dois processos para o primeiro comando (o comando e outro processo lendo sua saída da outra extremidade do tubo para armazenar no bucket) e dois para o segundo (um esvaziando o bucket em uma extremidade do tubo para o comando ler do outro lado).

Para o intervalo, você precisará da memória ou do sistema de arquivos. A memória não escala bem e você precisa dos canos. O sistema de arquivos faz muito mais sentido. É para isso que o /tmp é. Observe que os discos provavelmente nunca verão os dados, pois os dados podem não ser liberados até muito mais tarde (depois que você remover o arquivo temporário) e, mesmo que esteja, provavelmente ainda permanecerá na memória (em cache). E quando não é, é quando os dados seriam grandes demais para caber na memória.

Observe que os arquivos temporários são usados o tempo todo em shells. Na maioria dos shells, aqui os documentos e as strings aqui são implementadas com arquivos temporários.

Em:

cat << EOF
foo
EOF

A maioria dos shells cria um arquivo temporário, abre-o para escrita e para leitura, apaga-o, preenche-o com foo e executa cat com seu stdin duplicado a partir do fd aberto para leitura. O arquivo é excluído antes mesmo de ser preenchido (isso dá ao sistema uma pista de que, seja lá o que for que esteja escrito, não precisa sobreviver a uma perda de energia).

Você pode fazer o mesmo aqui com:

tmp=$(mktemp) && {
  rm -f -- "$tmp" &&
    cmd1 >&3 3>&- 4<&- &&
    cmd2 <&4 4<&- 3>&-
} 3> "$tmp" 4< "$tmp"

Depois, você não precisa se preocupar com a limpeza, pois o arquivo é excluído desde o início. Não há necessidade de processos extras para obter os dados dentro e fora dos depósitos, cmd1 e cmd2 fazem isso sozinhos.

Se você quisesse armazenar a saída na memória, usar um shell para isso não seria uma boa ideia. Os primeiros shells que não sejam zsh não podem armazenar dados arbitrários em suas variáveis. Você precisaria usar alguma forma de codificação. E então, para passar esses dados, você acabaria duplicando-o na memória várias vezes, se não estiver gravando no disco ao usar um aqui-doc ou uma string aqui.

Você pode usar perl como exemplo:

 perl -MPOSIX -e '
   sub status() {return WIFEXITED($?) ? WEXITSTATUS($?) : WTERMSIG($?) | 128}
   $/ = undef;
   open A, "-|", "cmd1" or die "open A: $!\n";
   $out = <A>;
   close A;
   $status = status;
   exit $status if $status != 0;

   open B, "|-", "cmd2" or die "open B: $!\n";
   print B $out;
   close B;
   exit status'
    
por 26.08.2017 / 16:00
1

Aqui está uma versão francamente horrível que combina diferentes ferramentas de moreutils :

chronic sh -c '! { echo 123 ; false ; }' | mispipe 'ifne -n false' 'ifne echo ok'

Ainda não é bem o que você quer: ele retorna 1 no caso de falha e zero caso contrário. No entanto, ele não inicia o segundo comando, a menos que o primeiro seja bem-sucedido, ele retorna um código com falha ou com êxito, de acordo com o primeiro comando funcionando ou não, e não usa arquivos.

A versão mais genérica é:

chronic sh -c '! '"$CMD1" | mispipe 'ifne -n false' "ifne $CMD2"

Isso reúne três das ferramentas do moreutils:

  • chronic executa um comando silenciosamente, a menos que falhe. Neste caso, estamos executando um shell para executar seu primeiro comando para que possamos inverter o resultado do sucesso / falha: ele executará o comando silenciosamente se falhar e imprimirá a saída no final se for bem sucedido.
  • mispipe canaliza dois comandos juntos, retornando o status de saída do primeiro. Isso é semelhante ao efeito de set -o pipefail . Os comandos são fornecidos como strings para que possam diferenciá-los.
  • ifne executa um programa se a entrada padrão não estiver vazia ou se estiver vazia com -n . Estamos usando duas vezes:

    • O primeiro é ifne -n false . Isso executa false e usa-o como o código de saída, se a entrada estiver vazia (significando que chronic o comeu, significando que cmd1 falhou).

      Quando a entrada não está vazia, ela não executa false , passa a entrada como cat e sai 0. A saída será canalizada para o próximo comando por mispipe .

    • O segundo é ifne cmd2 . Isso executa cmd2 iff a entrada não é vazia . Essa entrada é a saída de ifne -n false , que será não vazia exatamente quando a saída de chronic não estiver vazia, o que acontece quando o comando é bem-sucedido.

      Quando a entrada está vazia, cmd2 nunca é executado e ifne sai zero. mispipe descarta o valor de saída mesmo assim.

Existem (pelo menos) duas falhas remanescentes nessa abordagem:

  1. Como mencionado, ele perde o código de saída real de cmd1 , reduzindo-o para booleano verdadeiro / falso. Se o código de saída tiver significado, isso é perdido. Seria possível salvar o código em um arquivo no comando sh e recarregá-lo mais tarde ( ifne -n sh -c 'read code <FILENAME ; rm -f FILENAME; exit $code' ou algo assim) se necessário.
  2. Se cmd1 puder ser bem-sucedido sem saída, tudo desmorona de qualquer maneira.

Além disso, é claro, existem vários comandos bastante raros reunidos, citados cuidadosamente, com um significado não óbvio.

    
por 26.08.2017 / 06:37
1

Em primeiro lugar, seu exemplo false | echo ok é sem sentido, pois false não produziria nada para sua saída padrão e echo não leria a partir de sua entrada padrão. A "solução" para isso é false && echo ok .

cmd1 && cmd2

Isso executará cmd1 e não iniciará cmd2 até que cmd1 tenha concluído a execução.

Em um pipeline, como

cmd1 | cmd2

os dois comandos são sempre iniciados simultaneamente (isso é o que você percebe na sua "Solução com Falha 1"). O que os sincroniza é cmd2 lendo a saída de cmd1 . Um pipeline é uma maneira de passar a saída de um programa para a entrada de outro programa, concorrentemente executado.

Para simular que cmd1 está exibindo algo que cmd2 lê, mas para se livrar da simultaneidade, você teria que armazenar a saída de cmd1 em um arquivo temporário que cmd2 lê:

cmd1 >outfile && cmd2 <outfile

O arquivo temporário pode ser tratado assim:

trap 'rm -f "$tmpfile"' EXIT
tmpfile=$(mktemp)

cmd1 >"$tmpfile" && cmd2 <"$tmpfile"

Isso configura um trap que será acionado ao sair do shell. A armadilha removerá o arquivo temporário.

Se você tiver $TMPDIR em um sistema de arquivos de memória, você não incorrerá em nenhuma penalidade de I / O para gravar no disco.

Se você está preocupado com o tamanho do arquivo, então você será forçado a armazená-lo no disco, não importa o que (um tubo não seria capaz de conter o conteúdo, isso é o que você percebe na sua "Solução falhada" 3 ").

Olhando para a solução de xhienne para o Bash:

result=$(cmd1) && cmd2 <<< "$result"
unset result

Isso funciona se o resultado for texto que não termine em linhas vazias, mas falhará se contiver bytes nulos (esses serão descartados por bash ).

Para atenuar isso, poderíamos codificar com base64 o resultado:

set -o pipefail # ksh/zsh/bash
result=$( cmd1 | base64 ) && base64 -d <<<"$result" | cmd2
unset result

Esta é uma idéia terrível em termos de memória e uso da CPU, especialmente se o resultado for grande (a codificação base64 de $result será um terço maior que o binário). Você está muito melhor escrevendo o resultado binário para o disco e lendo de lá.

Note também que bash implementa <<< usando um arquivo temporário em qualquer caso.

    
por 26.08.2017 / 09:07
-1

run a pipe cmd1 | cmd2 n such a way that:

cmd2 doesn't start running until cmd1 has completely finished

Isso é impossível em geral. Leia o canal (7) que lembra que os canais têm capacidade limitada (tipicamente 4Kbytes ou 64Kbytes) e eles usam alguma memória kernel para seu buffer.

Assim, a saída de cmd1 vai para o pipe. Quando ficar cheio, qualquer escrever (2) feito por cmd1 to STDOUT_FILENO iria bloquear (a menos que cmd1 fosse especialmente codificado para manipular I / O sem bloqueio para stdout, e isso é muito incomum) até que cmd2 tenha read (2) do final desse outro pipe. Se cmd2 não começou, isso nunca aconteceria.

Recomendo vivamente a leitura de um livro como Programação Avançada em Linux que explica isso em detalhes (e um livro inteiro é necessário para explicar tudo isso.

    
por 26.08.2017 / 07:50

Tags