Como contar o número de linhas no meio de um cano

Como contar o número de linhas no meio de um cano

Quero contar o número de linhas em um cano e depois continuar o cano dependendo do resultado.

tentei

x=$(printf 'faa\nbor\nbaz\n' \
  | tee /dev/stderr | wc -l) 2>&1 \
  | if [[ $x -ge 2 ]]; then
      grep a
    else
      grep b
    fi

Mas não filtra nada (nem para "a" nem para "b"). Isso foi bastante inesperado, pois pelo menos estes funcionam conforme o esperado:

printf 'faa\nbor\nbaz\n' | if true; then grep a; else grep b; fi
printf 'faa\nbor\nbaz\n' | if false; then grep a; else grep b; fi

Parece que não consigo redirecionar o stderr de dentro da substituição do comando, pois isso também não funciona (no bash). Ele imprime todas as três linhas:

x=$(printf 'faa\nbor\nbaz\n' | tee /dev/stderr | wc -l) 2>&1 | grep a

No zsh ele imprime apenas duas linhas.

Mas em ambos os shells a variável x não é definida após o pipeline e nem mesmo durante a segunda metade do pipeline.

O que posso fazer para contar as linhas de um pipeline e depois agir dependendo desse número? Eu gostaria de evitar arquivos temporários.

Responder1

Este comentárioé verdade:

Cada parte de um pipeline é iniciada independentemente das outras partes do mesmo pipeline. Isso significa que $xnão poderá estar disponível no meio do pipeline se estiver definido em um dos outros estágios.

Isso não significa que você não possa fazer nada. Um pipeline pode ser considerado o principal canal de dados, mas os processos podem se comunicar usando canais secundários: arquivos, chamados fifos ou qualquer outro (embora às vezes você precise ter cuidado extra e não deixá-los bloquear).

Você deseja contar o número de linhas e processar condicionalmente todo o fluxo de dados posteriormente. Isso significa que você precisa chegar ao final do fluxo e só então passar por todo o fluxo. Então você precisa salvar todo o fluxo de alguma forma. Um arquivo temporário parece uma abordagem sensata. Você deve dividir seu tubo em pelo menos duas partes. A primeira parte deverá salvar os dados em um arquivo; então as linhas devem ser contadas (essa tarefa pode pertencer à primeira parte, eu acho); então a última parte deve obter o número, ler o arquivo para receber os dados desde o início e agir de acordo.


Se você realmente deseja evitar arquivos temporários, alguma parte do seu pipeline deve agir de alguma forma sponge. Para evitar canais laterais, o número de linhas deve ser passado como a primeira linha da saída e a parte restante do pipeline deve compreender este protocolo.

Considere este comando:

sed '$ {=; H; g; p;}; H; d'

Acumula linhas em um espaço de espera. Se houver pelo menos uma linha, após receber a última linha, sedimprime o número de linhas seguido por uma linha vazia e a entrada real.

A linha vazia é desnecessária, mas aparece "naturalmente" neste código simples. Em vez de tentar evitá-lo sed, eu simplesmente lidaria com isso mais tarde no pipe (por exemplo, com sed '2 d').

Exemplo de uso:

#!/bin/sh

sed '$ {=; H; g; p;}; H; d' | sed '2 d' | {
   if ! IFS= read -r nlines; then
      echo "0 lines. Nothing to do." >&2
   else
      echo "$nlines lines. Processing accordingly." >&2
      if [ "$nlines" -ge 2 ]; then
         grep a
      else
         grep b
      fi
   fi
}

Notas:

  • IFS= read -ré um exagero porque a primeira linha está bem definida e contém um único número (ou não existe).
  • Eu usei /bin/sh. O código também será executado no Bash.
  • Você não pode assumir sedque é capaz de armazenar qualquer quantidade arbitrária de dados.Especificação POSIXdiz:

    O padrão e os espaços de espera devem ser capazes de conter pelo menos 8.192 bytes.

    Portanto, pode ser que o limite seja de apenas 8.192 bytes. Por outro lado, posso imaginar um arquivo temporário contendo facilmente 1 TB de dados. Talvez não evite arquivos temporários a todo custo.


O título diz "conte o número de linhas", mas seu exemplo tenta decidir se o número é 2 ou mais (N ou mais em geral). Esses problemas não são equivalentes. Após a 2ª (Nª) linha de entrada você sabe a resposta para o último problema, linhas pares aparecerão indefinidamente. O código acima não pode lidar com entradas indefinidas. Vamos consertar isso até certo ponto.

sed '
7~1 {p; d}
6 {H; g; i \
6+
p; d}
$ {=; H; g; p}
6! {H; d}
'

Este comando se comporta como a solução anterior, exceto que quando chega à 6ª linha ele assume (imprime) que o número de linhas é 6+. Em seguida, as linhas já vistas são impressas e as linhas seguintes (se houver) são impressas assim que aparecem ( catcomportamento semelhante).

Exemplo de uso:

#!/bin/sh

threshold=6

sed "
$((threshold+1))~1 {p; d}
$threshold {H; g; i \
$threshold+
p; d}
$ {=; H; g; p}
${threshold}! {H; d}
" | sed '2 d' | {
   if ! IFS= read -r nlines; then
      echo "0 lines. Nothing to do." >&2
   else
      echo "$nlines lines. Processing accordingly." >&2
      if [ "$nlines" = "$threshold+" ]; then
         grep a
      else
         grep b
      fi
   fi
}

Notas:

  • Corrigido "até certo ponto" porque a limitação de sed(qualquer que seja a limitação no seu caso) ainda se aplica. Mas agora sedprocessa no máximo $thresholdum número de linhas; se $thresholdfor baixo o suficiente, então deve estar OK.
  • O código de exemplo testa apenas, $threshold+mas o protocolo permite distinguir entre 0, 1, 2,…, limite menos um e limite ou mais linhas.

Não sou muito habilidoso nisso sed. Se meu sedcódigo puder ser simplificado, deixe uma dica em um comentário.

Responder2

Com base na discussão e no código sed de Kamil, encontrei esta solução awk:

awk -v th="$threshold" '
  function print_lines() { for (i in lines) print lines[i] }
  NR < th { lines[NR] = $0 }
  NR > th { print }
  NR == th { print th; print_lines(); print }
  END { if (NR < th) { print NR; print_lines(); } }' \
| if read nlines; then
    if [ "$nlines" -eq "$threshold" ]; then
      grep a
    else
      grep b
    fi
  fi

informação relacionada