Anexar a última linha de stdin a stdin inteiro

9

Considere este script:

tmpfile=$(mktemp)

cat <<EOS > "$tmpfile"
line 1
line 2
line 3
EOS

cat <(tail -1 "$tmpfile") "$tmpfile"

Isso funciona e gera:

line 3
line 1
line 2
line 3

Digamos que nossa fonte de entrada, em vez de ser um arquivo real, fosse stdin:

cat <<EOS | # what goes here now?
line 1
line 2
line 3
EOS

Como podemos modificar o comando:

cat <(tail -1 "$tmpfile") "$tmpfile"

Para que ainda produza a mesma saída, neste contexto diferente?

NOTA: O Heredoc específico que estou criando, bem como o uso de um Heredoc em si, são meramente ilustrativos. Qualquer resposta aceitável deve assumir que está recebendo dados arbitrários via stdin .

Jonah
fonte
1
stdin é sempre um "arquivo real" (um fifo / socket / etc também é um arquivo; nem todos os arquivos são procuráveis). A resposta para sua pergunta é trivial "use um arquivo temporário" ou algum horror que carregará o arquivo inteiro na memória. "Como posso recuperar dados antigos de um fluxo sem ter armazenado em qualquer lugar ?" não pode ter uma boa resposta.
mosvy
1
@mosvy Essa é uma resposta perfeitamente aceitável se você quiser adicioná-la.
Jonah
2
@mosvy Como Jonah disse, as respostas devem ser postadas na caixa de respostas. Sei que é complicado ler qualquer site no momento, mas ignore o vermelho que está pingando lentamente sobre sua visão e use a área de texto inferior.
wizzwizz4

Respostas:

7

Tentar:

awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'

Exemplo

Defina uma variável com a nossa entrada:

$ input="line 1
> line 2
> line 3"

Execute nosso comando:

$ echo "$input" | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 3
line 1
line 2
line 3

Como alternativa, é claro, poderíamos usar um documento aqui:

$ cat <<EOS | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 1
line 2
line 3
EOS
line 3
line 1
line 2
line 3

Como funciona

  • x=x $0 ORS

    Isso acrescenta cada linha de entrada à variável x.

    No awk, ORSé o separador de registros de saída . Por padrão, é um caractere de nova linha.

  • END{printf "%s", $0 ORS x}

    Depois que lemos o arquivo inteiro, isso imprime a última linha $0, seguida pelo conteúdo do arquivo inteiro x,.

Como isso lê toda a entrada na memória, não seria apropriado para entradas grandes ( por exemplo, gigabytes).

John1024
fonte
Obrigado John. Portanto, não é possível fazer isso de maneira análoga ao meu exemplo de arquivo nomeado no OP? Eu estava imaginando o stdin sendo duplicado de alguma forma ... da mesma forma teeque, mas de um stdin e de um arquivo, estaríamos canalizando o mesmo stdin em duas substituições de processo diferentes. ou qualquer coisa que seria aproximadamente equivalente a isso?
Jonah
5

Se o stdin apontar para um arquivo que pode ser procurado (como no caso dos documentos do bash (mas não de todos os outros shell) aqui implementados com arquivos temporários), você pode obter a cauda e procurar novamente antes de ler o conteúdo completo:

procurar operadores estão disponíveis nos zshou ksh93conchas, ou linguagens de script como tcl / perl / python, mas não em bash. Mas você sempre pode ligar para intérpretes mais avançados bashse precisar usar bash.

ksh93 -c 'tail -n1; cat <#((0))' <<...

Ou

zsh -c 'zmodload zsh/system; tail -n1; sysseek 0; cat' <<...

Agora, isso não funcionará quando o stdin apontar para arquivos não procuráveis, como um cano ou soquete. Então, a única opção é ler e armazenar (na memória ou em um arquivo temporário ...) toda a entrada.

Algumas soluções para armazenar na memória já foram fornecidas.

Com um arquivo temporário, com zsh, você poderia fazê-lo com:

seq 10 | zsh -c '{ cat =(sed \$w/dev/fd/3); } 3>&1'

Se no Linux, com bashou zshou qualquer shell que use arquivos temporários para documentos aqui, você poderá realmente usar o arquivo temporário criado por um documento aqui para armazenar a saída:

seq 10 | {
  chmod u+w /dev/fd/3 # only needed in bash5+
  cat > /dev/fd/3
  tail -n1 /dev/fd/3
  cat <&3
} 3<<EOF
EOF
Stéphane Chazelas
fonte
4
cat <<EOS | sed -ne '1{h;d;}' -e 'H;${G;p;}'
line 1
line 2
line 3
EOS

O problema de traduzir isso para algo que usa tailé que você tailprecisa ler o arquivo inteiro para encontrar o final dele. Para usar isso em seu pipeline, você precisa

  1. Forneça o conteúdo completo do documento para tail.
  2. Forneça novamente para cat.
  3. Naquela ordem.

A parte complicada não é duplicar o conteúdo do documento ( teeisso é possível), mas fazer com que a saída tailocorra antes da saída do restante do documento, sem usar um arquivo temporário intermediário.

O uso sed(ou awk, como John1024 faz ) se livra da análise dupla dos dados e do problema de pedido armazenando os dados na memória.

A sedsolução que proponho é

  1. 1{h;d;}, armazene a primeira linha no espaço em espera, como está, e pule para a próxima linha.
  2. H, acrescente uma à outra linha ao espaço de espera com uma nova linha incorporada.
  3. ${G;p;}, acrescente o espaço de espera à última linha com uma nova linha incorporada e imprima os dados resultantes.

Esta é uma tradução literal da solução de John1024 sed, com a ressalva de que o padrão POSIX apenas garante que o espaço de espera seja de no mínimo 8192 bytes (8 KiB; mas recomenda que esse buffer seja alocado e expandido dinamicamente conforme necessário, que ambos GNU sede BSD sedestá fazendo).


Se você se permitir usar um pipe nomeado:

mkfifo mypipe
cat <<EOS | tee mypipe | cat <( tail -n 1 mypipe ) -
line 1
line 2
line 3
EOS
rm -f mypipe

Isso usa teepara enviar os dados para baixo mypipee ao mesmo tempo para cat. O catutilitário primeiro lerá a saída de tail(que lê de mypipe, para o qual teeestá gravando) e, em seguida, anexará a cópia do documento proveniente diretamente tee.

Porém, há uma falha séria nisso: se o documento for muito grande (maior que o tamanho do buffer do tubo), ele estará teegravando mypipee catbloqueando enquanto aguarda que o tubo (sem nome) seja esvaziado. Não seria esvaziado até ser catlido. catnão leria até tailterminar. E tailnão iria terminar até teeterminar. Essa é uma situação clássica de conflito.

A variação

tee >( tail -n 1 >mypipe ) | cat mypipe -

tem o mesmo problema.

Kusalananda
fonte
2
A sedum não funciona se a entrada tem apenas uma linha (talvez sed '1h;1!H;$!d;G'). Observe também que várias sedimplementações têm um limite baixo no tamanho de seu padrão e mantêm espaço.
Stéphane Chazelas 30/03/19
A solução de pipe nomeado é o tipo de coisa que eu estava procurando. A limitação é uma vergonha. Eu entendi sua explicação, exceto “E a cauda não terminaria até que o tee terminasse” - você poderia explicar por que esse é o caso?
Jonah
2

Existe uma ferramenta nomeada peeem uma coleção de utilitários de linha de comando geralmente empacotados com o nome "moreutils" (ou recuperável no site da Web ).

Se você pode tê-lo em seu sistema, o equivalente ao seu exemplo seria:

cat <<EOS | pee 'tail -1' cat 
line 1
line 2
line 3
EOS

A ordem dos comandos executados peeé importante porque eles são executados na sequência fornecida.

LL3
fonte
1

Tentar:

cat <<EOS # | what goes here now? Nothing!
line 3
line 1
line 2
line 3
EOS

Como a coisa toda são dados literais (um "aqui está o documento") e a diferença entre eles e a saída desejada é trivial, basta massagear esses dados literais ali mesmo para corresponder à saída.

Agora, suponha que line 3venha de algum lugar e seja armazenado em uma variável chamada lastline:

cat <<EOS # | what goes here now? Nothing!
$lastline
line 1
line 2
$lastline
EOS

Em um documento aqui, podemos gerar texto substituindo variáveis. Não apenas isso, mas podemos calcular o texto usando a substituição de comandos:

cat <<EOS
this is template text
here we have a hex conversion: $(printf "%x" 42)
EOS

Podemos interpolar várias linhas:

cat <<EOS
multi line
preamble
$(for x in 3 1 2 3; do echo line $x ; done)
epilog
EOS

Em geral, evite o processamento de texto no modelo de documento aqui; tente gerá-lo usando código interpolado.

Kaz
fonte
1
Sinceramente, não sei dizer se é uma piada ou não. O cat <<EOS...no OP foi apenas um exemplo para "criar um arquivo arbitrário", para tornar o post específico e a pergunta clara. Isso realmente não era óbvio para você, ou você apenas pensou que seria inteligente interpretar a pergunta literalmente?
Jonah
@Jonah A pergunta diz claramente "[l] et dizem que nossa fonte de entrada, em vez de ser um arquivo real, era stdin:". Nada sobre "arquivos arbitrários"; é sobre aqui documentos. Um documento aqui não é arbitrário. Não é uma entrada para o seu programa, mas uma parte de sua sintaxe que o programador escolhe.
Kaz
1
Acho que o contexto e as respostas existentes deixaram claro que era esse o caso, mesmo que, para que sua interpretação fosse correta, você literalmente tivesse que assumir que nem eu nem nenhum dos outros pôsteres que responderam percebemos que era possível copiar e colar um linha de código. No entanto, editarei a pergunta para torná-la explícita.
Jonah
1
Kaz, obrigado pela resposta, mas observe que mesmo com sua edição, você está perdendo a intenção da pergunta. Você está recebendo uma entrada multilinha arbitrária por meio de um canal . Você não tem idéia do que será. Sua tarefa é gerar a última linha de entrada, seguida por toda a entrada.
Jonah
1
Kaz, a entrada está lá apenas como exemplo. A maioria das pessoas, inclusive eu, acha útil ter um exemplo de entrada real e saída esperada, em vez de apenas a questão abstrata. Você é o único que ficou confuso com isso.
Jonah
0

Se você não se importa com o pedido. Então isso vai funcionar cat lines | tee >(tail -1). Como outros já disseram. Você precisa ler o arquivo duas vezes ou armazenar em buffer o arquivo inteiro na ordem solicitada.

ctrl-alt-delor
fonte