Reindirizzamento IO e comando head


9

.hgignoreOggi stavo cercando di modificare rapidamente un file dalla shell bash di Cygwin e ho aggiunto una riga che era un errore. Non sono sicuro se questo fosse il modo migliore per farlo, ma ho pensato rapidamente di utilizzare head -1 .hgignoreper rimuovere la linea offensiva (in precedenza avevo solo una linea nel file). Abbastanza sicuro, quando eseguito dà la prima riga come unica uscita.

Ma quando ho provato a reindirizzare l'output e riscrivere il file utilizzando head -1 .hgignore > .hgignore, il file era vuoto. Perché succede? Se provo invece ad aggiungere head -1 .hgignore >> .hgignore, si aggiunge correttamente ma questo non è ovviamente il risultato desiderato. Perché in questo caso un reindirizzamento troncato non funziona?


Risposte:


10

Quando la shell ottiene una riga di comando come: command > file.outla shell stessa apre (e forse crea) il file denominato file.out. La shell imposta il descrittore di file 0 sul descrittore di file che ha ottenuto da open. Ecco come funziona il reindirizzamento I / O: ogni processo conosce i descrittori di file 0, 1 e 2.

La parte difficile di questo è come aprire file.out. Il più delle volte, vuoi che sia file.outaperto per la scrittura con offset 0 (cioè troncato) e questo è ciò che la shell ha fatto per te. Ha troncato .hgignore, lo ha aperto per la scrittura, ha duplicato il filedescriptor su 0, quindi eseguito head. Bloccaggio istantaneo dei file.

Nella shell bash, fai un set noclobberper cambiare questo comportamento.


Ah, capisco. Pensavo che la shell stesse troncando il file prima di eseguire il comando, ma non sapevo perché. Grazie per la spiegazione!
voithos,

10

Penso che Bruce risponda a ciò che sta succedendo qui con la pipeline di shell.

Una delle mie piccole utility preferite è il spongecomando di moreutils . Risolve esattamente questo problema "assorbendo" tutti gli input disponibili prima di aprire il file di output di destinazione e scrivere i dati. Ti consente di scrivere pipeline esattamente come ti aspettavi di:

$ head -1 .hgignore | sponge .hgignore

La soluzione del povero consiste nel reindirizzare l'output in un file temporaneo, quindi dopo aver eseguito la pipeline (ad esempio il comando successivo eseguito) è di spostare il file temporaneo nella posizione del file originale.

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}

Guardando questo qualche anno dopo, mi venne in mente un pensiero: non potevamo semplicemente farlo head -1 .hgignore | tee .hgignore? teeè in coreutils, e come perk / effetto collaterale, questo scrive anche a STDOUT
voithos

@voithos A mia conoscenza teesi apre e tronca il file si sta scrivendo a quando viene creata un'istanza proprio come tutto il resto in modo che non risolve il problema principale qui della condizione di competizione sulla lettura il contenuto del file prima di troncare con la scrittura.
Caleb,

Si mettono un punto che non ero a conoscenza, in realtà - e cioè che in filodiffusione comandi vengono avviati immediatamente, invece di sequenza. È preciso? Tuttavia, l'ho provato e tee sembra fare la cosa desiderata. Ho una versione 8.13sulla mia macchina.
voithos,

1
@voithos Sì i comandi in una pipeline e tutti i canali di input / output coinvolti vengono avviati in ordine inverso, quindi la pipeline è pronta a ricevere i dati quando il primo inizia a fornirli. Ho il sospetto che il tuo test sia difettoso perché probabilmente hai usato una quantità troppo piccola di dati e tutto è stato memorizzato nella cache in un buffer di lettura prima di averne bisogno. Il teeprogramma troncherà i tuoi file, non è configurato per il doppio buffer.
Caleb,

3

In

head -n 1 file > file

fileviene troncato prima di headessere avviato, ma se lo scrivi:

head -n 1 file 1<> file

non fileè come viene aperto in modalità lettura-scrittura. Tuttavia, quando headtermina la scrittura, non tronca il file, quindi la riga sopra sarebbe una no-op ( headriscriverebbe semplicemente la prima riga su se stessa e lascerebbe intatte le altre).

Tuttavia, dopo che headè tornato e mentre fdè ancora aperto, è possibile chiamare un altro comando che esegue il truncate.

Per esempio:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

Ciò che conta qui è che truncatesopra, headsposta il cursore per fd 1 all'interno del file subito dopo la prima riga. Riscrive la prima riga per la quale non ne avevamo bisogno, ma non è dannoso.

Con una testa POSIX, potremmo effettivamente scappare senza riscrivere quella prima riga:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

Qui, stiamo usando il fatto che headsposta la posizione del cursore nel suo stdin. Mentre in headgenere legge i suoi input da grossi blocchi per migliorare le prestazioni, POSIX richiederebbe (dove possibile) di seektornare indietro subito dopo la prima riga se fosse andata oltre. Si noti tuttavia che non tutte le implementazioni lo fanno.

In alternativa, puoi usare il readcomando della shell invece in questo caso:

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file

1
Stephane, conosci un comando standard o coreutils che può troncare in modo STDINsimile a quello che hai realizzato usando perlsopra
iruvar

2
@ 1_CR, no. ddpuò troncare a qualsiasi offset assoluto arbitrario nel file però. Quindi puoi determinare l'offset di byte della seconda riga e troncare da lì condd bs=1 seek="$offset" of=file
Stéphane Chazelas,

1

La soluzione del vero uomo è

ed .hgignore
$d
wq

o come one-liner

printf '%s\n' '$d' 'wq' | ed .hgignore

O con GNU sed:

sed -i '$d' .hgignore

(No, sto scherzando. Userei un editor interattivo. vi .hgignore GddZZ)


Mi chiedevo, c'è qualche vantaggio nell'usare :wqoltre ZZ?
voithos,

Inoltre, :xquesto è ciò che fanno automaticamente le mie dita
glenn jackman,

ed ZQè uguale a:q!
glenn jackman il

ZZ e: x scrivono solo se c'è qualcosa da scrivere ...: w sincronizza sempre il file su disco indipendentemente dal fatto che ne abbia bisogno. Uso: xa perché utilizzo le schede.
xenoterracide,

1

Puoi usare Vim in modalità Ex:

ex -sc '2,d|x' .hgignore
  1. 2, seleziona le righe 2 fino alla fine

  2. d Elimina

  3. x salva e chiudi


0

Per la modifica dei file sul posto puoi anche usare il trucco di gestione dei file aperti come mostrato da Jürgen Hötzel nell'output di reindirizzamento da sed 's / c / d /' myFile a myFile .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed

2
E subito dopo che il rm .hgignoretuo potere si guasta, togliendo ore di duro lavoro. Ok, non importa .hgignore, ma perché dovresti fare qualcosa di così complicato? Quindi il mio downvote: tecnicamente corretto ma una pessima idea.
Gilles 'SO- smetti di essere malvagio' il

@Gilles, forse non è una buona idea, ma questo è ad esempio ciò che perl -i(per l'editing sul posto) fa, e non sarei sorpreso se anche alcune implementazioni sed -ilo facessero (sebbene l'ultima versione di GNU sedsembra non farlo).
Stéphane Chazelas,
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.