sed: legge l'intero file nello spazio modello senza errori sull'input a riga singola


9

La lettura di un intero file nello spazio modello è utile per sostituire le nuove righe, ecc. e ci sono molti casi che consigliano quanto segue:

sed ':a;N;$!ba; [commands...]'

Tuttavia, non riesce se l'input contiene solo una riga.

Ad esempio, con due input di riga, ogni riga è soggetta al comando di sostituzione:

$ echo $'abc\ncat' | sed ':a;N;$!ba; s/a/xxx/g'
xxxbc
cxxxt

Ma, con input a riga singola, non viene eseguita alcuna sostituzione:

$ echo 'abc' | sed ':a;N;$!ba; s/a/xxx/g'
abc

Come si scrive un sedcomando per leggere tutti gli input in una volta e non avere questo problema?


Ho modificato la tua domanda in modo che contenga una domanda effettiva. Puoi aspettare altre risposte se vuoi, ma alla fine contrassegnare la risposta migliore come accettata (vedi il pulsante di pipe a sinistra della risposta, proprio sotto i pulsanti freccia su-giù).
Giovanni 1024,

@ John1024 Grazie, bello avere un esempio. Trovare questo genere di cose tende a ricordarmi che "tutto è sbagliato", ma sono contento che alcuni di noi non si arrendano. :}
dicktyr

2
C'è una terza opzione! Usa l' sed -zopzione GNU . Se il tuo file non ha null, leggerà fino alla fine del file! Trovato da questo: stackoverflow.com/a/30049447/582917
CMCDragonkai

Risposte:


13

Esistono diversi motivi per cui la lettura di un intero file nello spazio modello può andare storta. Il problema logico nella domanda che circonda l'ultima riga è comune. È correlato al sedciclo di linee - quando non ci sono più linee e sedincontra EOF che sta attraversando - termina l'elaborazione. E quindi se sei sull'ultima riga e ti ordini seddi prenderne un altro, si fermerà proprio lì e non farà più nulla.

Detto questo, se hai davvero bisogno di leggere un intero file nello spazio modello, allora probabilmente vale la pena considerare un altro strumento. Il fatto è che sedè l'omonimo stream editor - è progettato per lavorare una linea - o un blocco logico di dati - alla volta.

Esistono molti strumenti simili che sono meglio equipaggiati per gestire blocchi di file completi. ede ex, ad esempio, può fare gran parte di ciò che sedpuò fare e con una sintassi simile - e molto altro ancora - ma piuttosto che operare solo su un flusso di input mentre lo trasforma in output sed, mantiene anche i file di backup temporanei nel file system . Il loro lavoro è bufferizzato su disco secondo necessità, e non si interrompono bruscamente alla fine del file (e tendono a implodere molto meno spesso sotto sforzo del buffer) . Inoltre offrono molte utili funzioni che sednon hanno - del tipo che semplicemente non ha senso in un contesto di flusso - come segni di linea, annullamento, buffer con nome, join e altro.

sedIl principale punto di forza è la capacità di elaborare i dati non appena li legge, in modo rapido, efficiente e in streaming. Quando si assorbe un file, lo si butta via e si tende a incorrere in difficoltà di edge case come l'ultimo problema di linea che si cita, buffer overrun e prestazioni abissali - man mano che i dati analizzati aumentano in lunghezza il tempo di elaborazione di un motore regexp quando si elencano le corrispondenze aumenta esponenzialmente .

Per quanto riguarda l'ultimo punto, a proposito: mentre capisco il s/a/A/gcaso di esempio è molto probabilmente solo un esempio ingenuo e probabilmente non è lo script reale per cui vuoi raccogliere un input, potresti trovare utile valere la pena y///. Se ti ritrovi spesso a gsostituire lobalmente un singolo personaggio con un altro, allora ypotrebbe esserti molto utile. È una trasformazione al contrario di una sostituzione ed è molto più veloce in quanto non implica una regexp. Quest'ultimo punto può anche essere utile quando si tenta di conservare e ripetere //indirizzi vuoti perché non li influenza ma possono esserne interessati. In ogni caso, y/a/A/è un mezzo più semplice per ottenere lo stesso risultato e gli swap sono possibili come:y/aA/Aa/ che scambiavano tutte le lettere maiuscole / minuscole come su una linea l'una per l'altra.

Dovresti anche notare che il comportamento che descrivi in ​​realtà non è quello che dovrebbe accadere comunque.

Da GNU info sednella sezione BUG COMUNICATI SEGNALATI :

  • N comando sull'ultima riga

    • La maggior parte delle versioni di sedexit senza stampare nulla quando il Ncomando viene emesso sull'ultima riga di un file. GNU sedstampa lo spazio del motivo prima di uscire a meno che, ovviamente, non -nsia stato specificato l'interruttore di comando. Questa scelta è di progettazione.

    • Ad esempio, il comportamento di sed N foo bardipenderebbe dal fatto che foo abbia un numero pari o dispari di righe. Oppure, quando si scrive una sceneggiatura per leggere le prossime righe che seguono una corrispondenza del modello, le implementazioni tradizionali di sedti costringerebbero a scrivere qualcosa di simile /foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }anziché solo /foo/{ N;N;N;N;N;N;N;N;N; }.

    • In ogni caso, la soluzione più semplice è utilizzare $d;Nnegli script che si basano sul comportamento tradizionale o impostare la POSIXLY_CORRECTvariabile su un valore non vuoto.

La POSIXLY_CORRECTvariabile d'ambiente è menzionata perché POSIX specifica che se sedincontra EOF durante il tentativo Ndovrebbe uscire senza output, ma la versione GNU si rompe intenzionalmente con lo standard in questo caso. Si noti inoltre che, anche se il comportamento è giustificato in precedenza, il presupposto è che il caso di errore è quello della modifica dello stream, non l'assorbimento di un intero file in memoria.

Lo standard definisce Nquindi il comportamento:

  • N

    • Aggiungi la riga successiva di input, meno la sua \newline finale, allo spazio del pattern, usando una \newline incorporata per separare il materiale aggiunto dal materiale originale. Si noti che il numero di riga corrente cambia.

    • Se non è disponibile alcuna riga di input successiva, il Nverbo di comando si dirama verso la fine dello script e si chiude senza iniziare un nuovo ciclo o copiare lo spazio del pattern nell'output standard.

In quella nota, ci sono alcuni altri GNU-ismi dimostrati nella domanda - in particolare l'uso delle parentesi di :etichetta, branch e {contesto di funzione }. Come regola generale, qualsiasi sedcomando che accetta un parametro arbitrario è delimitato da una \newline nello script. Quindi i comandi ...

:arbitrary_label_name; ...
b to_arbitrary_label_name; ...
//{ do arbitrary list of commands } ...

... è molto probabile che si comportino in modo irregolare a seconda seddell'implementazione che li legge. Portabilmente dovrebbero essere scritti:

...;:arbitrary_label_name
...;b to_arbitrary_label_name
//{ do arbitrary list of commands
}

Lo stesso vale per r, w, t, a, i, e c (e forse un paio di più che sto dimenticando in questo momento) . In quasi tutti i casi potrebbero anche essere scritti:

sed -e :arbitrary_label_name -e b\ to_arbitary_label_name -e \
    "//{ do arbitrary list of commands" -e \}

... dove la nuova -edichiarazione \nxecution sostituisce il delimitatore ewline. Quindi, laddove il infotesto GNU suggerisce che un'implementazione tradizionale sedti costringerebbe a fare :

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }

... dovrebbe piuttosto essere ...

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N
}

... ovviamente, neanche questo è vero. Scrivere la sceneggiatura in quel modo è un po 'sciocco. Esistono mezzi molto più semplici per fare lo stesso, come:

printf %s\\n foo . . . . . . |
sed -ne 'H;/foo/h;x;//s/\n/&/3p;tnd
         //!g;x;$!d;:nd' -e 'l;$a\' \
     -e 'this is the last line' 

... che stampa:

foo
.
.
.
foo\n.\n.\n.$
.$
this is the last line

... perché il tcomando est - come la maggior parte dei sedcomandi - dipende dal ciclo di linea per aggiornare il suo registro di ritorno e qui il ciclo di linea è autorizzato a fare la maggior parte del lavoro. Questo è un altro compromesso che si fa quando si assorbe un file: il ciclo di linea non si aggiorna mai più e così tanti test si comporteranno in modo anomalo.

Il comando sopra non rischia di superare l'input perché fa solo alcuni semplici test per verificare ciò che legge mentre lo legge. Con il Hvecchio tutte le linee vengono aggiunte allo spazio di attesa, ma se una linea corrisponde /foo/, sovrascrive il hvecchio spazio. I buffer vengono successivamente xmodificati e s///viene tentato un ubstitution condizionale se il contenuto del buffer corrisponde //all'ultimo modello indirizzato. In altre parole, //s/\n/&/3ptenta di sostituire la terza riga nuova nello spazio di attesa con se stesso e di stampare i risultati se lo spazio di attesa corrisponde attualmente /foo/. Se che tressi successo i rami script al not dlabel elete - che fa un lOOK e avvolge lo script.

Nel caso in cui sia /foo/una terza riga che una terza riga non possano essere accoppiate insieme nello spazio di attesa, allora //!gsovrascriverà il buffer se /foo/non è abbinato, o, se è abbinato, sovrascriverà il buffer se una \newline non è abbinata (sostituendo quindi /foo/con stesso) . Questo piccolo test sottile impedisce al buffer di riempirsi inutilmente per lunghi periodi di no /foo/e garantisce che il processo rimanga scattante perché l'input non si accumula. In seguito in caso di no /foo/o //s/\n/&/3pfail i buffer vengono nuovamente scambiati e ogni riga tranne l'ultima è lì cancellata.

Quest'ultima - l'ultima riga $!d- è una semplice dimostrazione di come è sedpossibile creare uno script top-down per gestire facilmente più casi. Quando il tuo metodo generale è quello di eliminare i casi indesiderati a partire dal più generale e lavorando verso il più specifico, i casi limite possono essere gestiti più facilmente perché sono semplicemente autorizzati a cadere fino alla fine dello script con gli altri dati desiderati e quando avvolge tutto ciò che rimane con solo i dati desiderati. Dover recuperare tali casi limite da un circuito chiuso può essere molto più difficile da fare, però.

E quindi ecco l'ultima cosa che devo dire: se devi davvero inserire un intero file, allora puoi sopportare di fare un po 'meno lavoro facendo affidamento sul ciclo di linea per farlo per te. Tipicamente Nuseresti ext e next per lookahead - perché avanzano prima del ciclo di linea. Piuttosto che implementare in modo ridondante un ciclo chiuso all'interno di un ciclo - poiché il sedciclo di linea è comunque solo un semplice ciclo di lettura - se il tuo scopo è solo quello di raccogliere input indiscriminatamente, è probabilmente più facile fare:

sed 'H;1h;$!d;x;...'

... che raccoglierà l'intero file o fallirà.


una nota a margine Ne il comportamento dell'ultima riga ...

mentre non ho gli strumenti a mia disposizione per testare, considera che Ndurante la lettura e la modifica sul posto si comporta diversamente se il file modificato è il file di script per la lettura successiva.


1
Porre Hprima l'incondizionato è adorabile.
jillill

@mikeserv Grazie per il tuo contributo. Riesco a vedere potenziali vantaggi nel mantenere il ciclo di linea, ma come funziona di meno?
dicktyr

@dicktyr bene, la sintassi prende alcune scorciatoie :a;$!{N;ba}come menziono sopra - è più facile usare il modulo standard a lungo termine quando si tenta di eseguire regexps su sistemi non familiari. Ma non era proprio quello che intendevo dire: implementi un circuito chiuso - non puoi facilmente entrare nel mezzo di ciò quando vuoi come potresti invece ramificando - potando i dati indesiderati - e lasciando che il ciclo avvenga. È come una cosa dall'alto in basso: tutto ciò che sedfa è un risultato diretto di ciò che ha appena fatto. Forse lo vedi in modo diverso, ma se lo provi potresti scoprire che lo script diventa più facile.
Mikeserv,

11

Non riesce perché il Ncomando precede la corrispondenza del modello $!(non l'ultima riga) e sed si chiude prima di eseguire qualsiasi lavoro:

N

Aggiungi una nuova riga allo spazio del motivo, quindi aggiungi la riga successiva di input allo spazio del motivo. Se non vi sono più input, sed esce senza elaborare altri comandi .

Questo può essere facilmente risolto per funzionare anche con input a riga singola (e in effetti per essere più chiaro in ogni caso) semplicemente raggruppando i comandi Ne bdopo il modello:

sed ':a;$!{N;ba}; [commands...]'

Funziona come segue:

  1. :a creare un'etichetta denominata "a"
  2. $! se non l'ultima riga, allora
  3. Naggiungi la riga successiva allo spazio del pattern (o esci se non ci sono righe successive) e baramo (vai a) etichetta 'a'

Sfortunatamente, non è portatile (poiché si basa sulle estensioni GNU), ma la seguente alternativa (suggerita da @mikeserv) è portatile:

sed 'H;1h;$!d;x; [commands...]'

Ho pubblicato questo qui perché non ho trovato le informazioni altrove e volevo renderle disponibili in modo che altri possano evitare problemi con la diffusione :a;N;$!ba;.
dicktyr

Grazie per la pubblicazione! Ricorda che anche accettare la tua risposta va bene. Devi solo aspettare un po 'prima che il sistema ti permetta di farlo.
terdon
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.