Anticipa l'ultima riga di stdin all'intera stdin


9

Considera questo script:

tmpfile=$(mktemp)

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

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

Funziona e produce:

line 3
line 1
line 2
line 3

Diciamo che la nostra fonte di input, anziché essere un vero file, era invece stdin:

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

Come si modifica il comando:

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

In modo che produca ancora lo stesso output, in questo diverso contesto?

NOTA: lo specifico Heredoc che sto allevando, così come l'uso di un Heredoc stesso, è puramente illustrativo. Qualsiasi risposta accettabile dovrebbe presumere che stia ricevendo dati arbitrari tramite stdin .


1
stdin è sempre un "file reale" (anche un file fifo / socket / etc; non tutti i file sono ricercabili). La risposta alla tua domanda è un banale "usa un file temporaneo" o un po 'di horror che caricherà l'intero file in memoria. "Come posso recuperare i vecchi dati da uno stream senza averli archiviati ovunque ?" non può avere una buona risposta.
mosvy

1
@mosvy Questa è una risposta perfettamente accettabile se desideri aggiungerla.
Giona

2
@mosvy Come ha detto Jonah, le risposte dovrebbero essere pubblicate nella casella di risposta. So che è difficile leggere qualsiasi sito Web al momento, ma per favore ignora il rosso che gocciola lentamente sulla tua visione e usa l'area di testo inferiore.
wizzwizz4,

Risposte:


7

Provare:

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

Esempio

Definisci una variabile con il nostro input:

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

Esegui il nostro comando:

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

In alternativa, ovviamente, potremmo usare un here-doc:

$ 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

Come funziona

  • x=x $0 ORS

    Questo accoda ogni riga di input alla variabile x.

    In awk, ORSè il separatore del record di output . Per impostazione predefinita, è un carattere di nuova riga.

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

    Dopo l'abbiamo letto in tutto il file, questo stampe l'ultima linea, $0seguito da contenuto del file intero, x.

Poiché questo legge l'intero input in memoria, non sarebbe appropriato per input di grandi dimensioni ( ad es. Gigabyte).


Grazie John Quindi non è possibile farlo in modo analogo al mio esempio di file con nome nell'OP? Immaginavo che lo stdin fosse duplicato in qualche modo ... in qualche modo tee, ma di uno stdin e di un file, avremmo convogliato lo stesso stdin in due diverse sostituzioni di processo. o qualcosa che sarebbe approssimativamente equivalente a quello?
Giona

5

Se stdin punta a un file ricercabile (come nel caso dei documenti di bash (ma non di tutte le altre shell) qui che sono implementati con i file temporanei), è possibile ottenere la coda e poi riprovare prima di leggere l'intero contenuto:

gli operatori di ricerca sono disponibili nelle shell zsho ksh93, o nei linguaggi di scripting come tcl / perl / python, ma non in bash. Ma puoi sempre chiamare quegli interpreti più avanzati da bashse devi usare bash.

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

O

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

Ora, ciò non funzionerà quando stdin punta a un file non ricercabile come una pipe o un socket. Quindi, l'unica opzione è leggere e archiviare (in memoria o in un file temporaneo ...) l'intero input.

Alcune soluzioni per l'archiviazione in memoria sono già state fornite.

Con un tempfile, con zsh, potresti farlo con:

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

Se su Linux, con bashoo zshqualsiasi shell che utilizza file temporanei per i documenti qui, è possibile utilizzare effettivamente il file temporaneo creato da un documento qui per archiviare l'output:

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

4
cat <<EOS | sed -ne '1{h;d;}' -e 'H;${G;p;}'
line 1
line 2
line 3
EOS

Il problema con la traduzione di questo in qualcosa che utilizza tailè che è tailnecessario leggere l'intero file per trovarne la fine. Per usarlo nella tua pipeline, devi farlo

  1. Fornire l'intero contenuto del documento a tail.
  2. Forniscilo di nuovo a cat.
  3. In questo ordine.

Il trucco non è duplicare il contenuto del documento (lo teefa) ma ottenere l'output di tailavvenire prima che venga emesso il resto del documento, senza usare un file temporaneo intermedio.

L'utilizzo sed(o awk, come fa John1024 ) elimina la doppia analisi dei dati e il problema di ordinazione memorizzando i dati in memoria.

La sedsoluzione che propongo è

  1. 1{h;d;}, archivia la prima riga nello spazio di attesa, così com'è, e passa alla riga successiva.
  2. H, aggiungere ogni altra riga allo spazio di attesa con una nuova riga incorporata.
  3. ${G;p;}, aggiungi lo spazio di conservazione all'ultima riga con una nuova riga incorporata e stampa i dati risultanti.

Questa è una traduzione letterale della soluzione di John1024 in sed, con l'avvertenza che lo standard POSIX garantisce solo che lo spazio di attesa sia almeno 8192 byte (8 KiB; ma raccomanda che questo buffer sia allocato dinamicamente ed espanso secondo necessità, che entrambi GNU sede BSD sedsta facendo).


Se ti permetti di usare una pipa denominata:

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

Questo utilizza teeper inviare i dati verso il basso mypipee, allo stesso tempo per cat. L' catutilità leggerà prima l'output da tail(che legge da mypipe, che teesta scrivendo), quindi aggiungerà la copia del documento proveniente direttamente da tee.

C'è però un grave difetto in questo, nel senso che se il documento è troppo grande (più grande della dimensione del buffer della pipe), sta teescrivendo mypipee catsi bloccherebbe in attesa che la pipe (senza nome) si svuoti. Non verrebbe svuotato fino a quando non verrà catletto. catnon avrebbe letto da esso fino a quando non tailfosse terminato. E tailnon sarebbe finito fino a quando non teefosse finito. Questa è una classica situazione di deadlock.

La variazione

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

ha lo stesso problema.


2
Quello sednon funziona se l'ingresso ha solo una linea (forse sed '1h;1!H;$!d;G'). Si noti inoltre che diverse sedimplementazioni hanno un limite basso per le dimensioni del modello e per lo spazio disponibile.
Stéphane Chazelas,

La soluzione pipe denominata è il tipo di cosa che stavo cercando. Il limite è un peccato. Ho capito la tua spiegazione, tranne per "E la coda non sarebbe finita fino a quando il tee non fosse terminato" - potresti approfondire il perché?
Giona

2

Esiste uno strumento denominato peein una raccolta di utilità della riga di comando generalmente impacchettate con il nome "moreutils" (o altrimenti recuperabili dal suo sito Web di origine ).

Se puoi averlo sul tuo sistema, l'equivalente per il tuo esempio sarebbe come:

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

L'ordinamento dei comandi eseguiti peeè importante perché vengono eseguiti nella sequenza fornita.


1

Provare:

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

Dal momento che il tutto sono dati letterali (un "documento qui-è"), e la differenza tra esso e l'output desiderato è banale, basta massaggiare quei dati letterali proprio per abbinare l'output.

Ora supponiamo che line 3provenga da qualche parte e sia memorizzato in una variabile chiamata lastline:

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

In un documento qui, possiamo generare testo sostituendo le variabili. Non solo, ma possiamo calcolare il testo usando la sostituzione dei comandi:

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

Possiamo interpolare più linee:

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

In generale, evita di elaborare il modello di documento qui; prova a generarlo usando il codice interpolato.


1
Onestamente non so dire se questo è uno scherzo o no. L' cat <<EOS...OP era solo un esempio di "catturare un file arbitrario" per rendere il post specifico e la domanda chiara. Non era davvero ovvio per te o pensavi solo che sarebbe stato intelligente interpretare la domanda alla lettera?
Giona

@Jonah La domanda dice chiaramente "[l] et dice che la nostra fonte di input, anziché essere un vero file, era invece stdin:". Niente di "file arbitrari"; riguarda qui i documenti. Un documento qui non è arbitrario. Non è un input per il tuo programma, ma un pezzo della sua sintassi che il programmatore sceglie.
Kaz

1
Penso che il contesto e le risposte esistenti abbiano chiarito che era così, anche solo perché la tua interpretazione era corretta, dovevi letteralmente presumere che né io né nessuno degli altri poster che hanno risposto si è reso conto che era possibile copiare e incollare un riga di codice. Tuttavia, modificherò la domanda per renderla esplicita.
Giona

1
Kaz, grazie per la risposta, ma nota anche con la tua modifica, ti manca l'intenzione della domanda. Si ricevono in ingresso multilinea arbitrario tramite un tubo . Non hai idea di cosa sarà. Il tuo compito è quello di produrre l'ultima riga di input, seguita dall'intero input.
Giona

1
Kaz, l'ingresso è lì solo come esempio. Molte persone, incluso me stesso, trovano utile avere un esempio di input reale e output atteso, piuttosto che solo la domanda astratta. Sei l'unico che è stato confuso da questo.
Giona

0

Se non ti interessa l'ordine. Quindi funzionerà cat lines | tee >(tail -1). Come altri hanno già detto. Devi leggere il file due volte o bufferizzare l'intero file, per farlo nell'ordine richiesto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.