Linux: come utilizzare un file come input e output contemporaneamente?


55

Ho appena eseguito quanto segue in bash:

uniq .bash_history > .bash_history

e il mio file di cronologia è finito completamente vuoto.

Immagino di aver bisogno di un modo per leggere l'intero file prima di scriverlo. Come è fatto?

PS: ovviamente ho pensato di usare un file temporaneo, ma sto cercando una soluzione più elegante.


È perché i file vengono aperti da destra a sinistra. Vedere anche stackoverflow.com/questions/146435/...
WheresAlice

Devi scrivere l'output in un nuovo file nella stessa directory e rinominarlo in cima al vecchio file. Qualsiasi altro approccio rischierà di perdere i dati se vengono interrotti a metà. Alcuni strumenti potrebbero nasconderti questo passaggio.
Kasperd,

In alternativa, bashnon inserirai duplicati consecutivi nella sua cronologia se imposti HISTCONTROL per includere ignororedup; vedi la manpage.
dave_thompson_085,

Risposte:


49

Consiglio di utilizzare spongeda moreutils . Dalla manpage:

DESCRIPTION
  sponge  reads  standard  input  and writes it out to the specified file. Unlike
  a shell redirect, sponge soaks up all its input before opening the output file.
  This allows for constructing pipelines that read from and write to the same 
  file.

Per applicare questo al tuo problema, prova:

uniq .bash_history | sponge .bash_history

6
È come un gatto, ma con capacità di succhiare: D
MilliaLover,

77

Volevo solo contribuire con un'altra risposta che è semplice e non usa la spugna (perché spesso non è inclusa in ambienti leggeri).

echo "$(uniq .bash_history)" > .bash_history

dovrebbe avere il risultato desiderato. La subshell viene eseguita prima che .bash_history venga aperto per la scrittura. Come spiegato nella risposta di Phil P, quando .bash_history viene letto nel comando originale, è già stato troncato dall'operatore '>'.


15
Di solito non sono un fan delle risposte che portano a un'antica domanda che ha già una risposta valida e accettata, ma è elegante, ben scritta e fa un forte argomento per la sua necessità (ambienti leggeri); per me, aggiunge davvero qualcosa all'insieme di risposte esistente. Benvenuti a San Francisco, Hart (sei stato qui un mese, ma penso che questo sia il tuo primo post sostanziale). Spero di leggere altre risposte come questa!
MadHatter,

4
Questa è la soluzione migliore Ho dovuto usare una subshell $()invece dei backtick a causa di alcuni problemi di fuga.
CMCDragonkai,

3
Mi chiedo se questa soluzione si ridimensiona in file di grandi dimensioni, diciamo ... 20 o 50 GB.
Amit Naidu,

1
Questa dovrebbe davvero essere la risposta accettata.
maxywb,

1
Ho usato questa risposta per fare echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txtoggi. Cercava da tempo una soluzione elegante. Mille grazie, @Hart Simha!
shredalert,

12

Il problema è che la shell sta configurando la pipeline di comandi prima di eseguire i comandi. Non si tratta di "input e output", è che il contenuto del file è già sparito prima ancora che uniq venga eseguito. Va qualcosa come:

  1. La shell apre il >file di output per la scrittura, troncandolo
  2. La shell è configurata per utilizzare il descrittore di file 1 (per stdout) per quell'output
  3. La shell esegue uniq, forse qualcosa come execlp ("uniq", "uniq", ".bash_history", NULL)
  4. uniq viene eseguito, apre .bash_history e non trova nulla lì

Esistono varie soluzioni, tra cui la modifica sul posto e l'utilizzo temporaneo dei file di cui altri citano, ma la chiave è capire il problema, cosa sta realmente andando storto e perché.


9

Un altro trucco per farlo, senza usare sponge, è il seguente comando:

{ rm .bash_history && uniq > .bash_history; } < .bash_history

Questo è uno dei trucchi descritti nell'eccellente articolo "Modifica sul posto" dei file su backreference.org.

Fondamentalmente apre il file per la lettura, quindi "lo rimuove". In realtà non viene rimosso: c'è un descrittore di file aperto che lo punta e, finché rimane aperto, il file è ancora in giro. Quindi crea un nuovo file con lo stesso nome e scrive le righe univoche su di esso.

Svantaggio di questa soluzione: se uniqfallisce per qualche motivo, la tua cronologia sparirà.



3

Questo sedscript rimuove i duplicati adiacenti. Con l' -iopzione, esegue la modifica sul posto. Viene dal sed infofile:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history

sed utilizza ancora il file temporaneo, ha aggiunto una risposta con straceillustrazione (non che contenga davvero) :-)
Kyle Brandt

3
@Kyle: vero, ma "lontano dagli occhi, lontano dal cuore". Personalmente, userei il file temporaneo esplicito poiché qualcosa di simile process input > tmp && mv tmp inputè molto più semplice e più leggibile rispetto all'utilizzo seddell'inganno semplicemente per evitare un file temporaneo e non sovrascriverà il mio originale se fallisce (non so se sed -ifallisce con grazia - Vorrei penso che sarebbe comunque). Inoltre, ci sono molte cose che puoi fare con il metodo di output-to-temp-file che non può essere eseguito sul posto senza qualcosa di ancora più coinvolto di questo sedscript. So che sai tutto questo, ma potrebbe essere utile per alcuni spettatori.
Dennis Williamson,

3

Come interessante notizia, sed utilizza anche un file temporaneo (questo lo fa proprio per te):

$ strace sed -i 's/foo/bar/g' foo    
open("foo", O_RDONLY|O_LARGEFILE)       = 3
...
open("./sedPmPv9z", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = 4
...
read(3, "foo\n"..., 4096)               = 4
write(4, "bar\n"..., 4)                 = 4
read(3, ""..., 4096)                    = 0
close(3)                                = 0
close(4)                                = 0
rename("./sedPmPv9z", "foo")            = 0
close(1)                                = 0
close(2)                                = 0

Descrizione:
Il file temporaneo ./sedPmPv9zdiventa fd 4 e i foofile diventano fd 3. Le operazioni di lettura sono su fd 3 e le scritture su fd 4 (il file temp). Il file foo viene quindi sovrascritto con il file temporaneo nella chiamata di ridenominazione.



0

Un file temporaneo è praticamente tutto, a meno che il comando in questione non supporti la modifica in atto ( uniqnon - alcuni seds do ( sed -i)).


0

Puoi usare Vim in modalità Ex:

ex -sc '%!uniq' -cx .bash_history
  1. % seleziona tutte le righe

  2. ! esegui comando

  3. x salva e chiudi


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.