Domanda

Stavo cercando di rimuovere tutte le righe di un file tranne l'ultima riga ma il seguente comando non ha funzionato, sebbene file.txt non sia vuoto.

$cat file.txt |tail -1 > file.txt

$cat file.txt

Perché è così?

È stato utile?

Soluzione

Il reindirizzamento da un file attraverso una pipeline allo stesso file non è sicuro; se file.txt viene sovrascritto dalla shell quando si configura l'ultimo stadio della pipeline prima che tail inizi a leggere il primo stadio, si finisce con un output vuoto.

Procedi invece come segue:

tail -1 file.txt >file.txt.new && mv file.txt.new file.txt

... beh, in realtà, non farlo nel codice di produzione; in particolare se ci si trova in un ambiente sensibile alla sicurezza e si esegue come root, è più appropriato quanto segue:

tempfile="$(mktemp file.txt.XXXXXX)"
chown --reference=file.txt -- "$tempfile"
chmod --reference=file.txt -- "$tempfile"
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt

Un altro approccio (evitando i file temporanei, a meno che <<< non li crei implicitamente sulla tua piattaforma) è il seguente:

lastline="$(tail -1 file.txt)"; cat >file.txt <<<"$lastline"

(L'implementazione di cui sopra è specifica per bash, ma funziona nei casi in cui non è presente l'eco, ad esempio quando l'ultima riga contiene " - versione " ;, ad esempio).

Infine, si può usare la spugna da moreutils :

tail -1 file.txt | sponge file.txt

Altri suggerimenti

Puoi usare sed per cancellare tutte le righe tranne l'ultima da un file:

sed -i '$!d' file
  • -i dice a sed di sostituire il file in atto; in caso contrario, il risultato verrebbe scritto su STDOUT.
  • $ è l'indirizzo che corrisponde all'ultima riga del file.
  • d è il comando di eliminazione. In questo caso, viene negato da ! , quindi tutte le righe non corrispondenti all'indirizzo verranno eliminate.

Prima che 'cat' venga eseguito, Bash ha già aperto 'file.txt' per la scrittura, cancellandone il contenuto.

In generale, non scrivere sui file che stai leggendo nella stessa istruzione. Questo può essere risolto scrivendo in un altro file, come sopra:

$cat file.txt | tail -1 >anotherfile.txt
$mv anotherfile.txt file.txt
o usando un'utilità come sponge da moreutils :
$cat file.txt | tail -1 | sponge file.txt
Questo funziona perché sponge attende che il flusso di input sia terminato prima di aprire il file di output.

Quando invii la tua stringa di comando a bash, effettua le seguenti operazioni:

  1. Crea una pipe I / O.
  2. Inizia " / usr / bin / tail -1 " ;, leggendo dalla pipe e scrivendo in file.txt.
  3. Inizia " / usr / bin / cat file.txt " ;, scrivendo nella pipe.

Quando 'cat' inizia a leggere, 'file.txt' è già stato troncato da 'tail'.

Fa tutto parte del design di Unix e dell'ambiente shell e risale alla shell Bourne originale. È una caratteristica, non un bug.

tmp = $ (tail -1 file.txt); echo $ tmp > file.txt;

Funziona bene in una shell Linux:

replace_with_filter() {
  local filename="$1"; shift
  local dd_output byte_count filter_status dd_status
  dd_output=$("$@" <"$filename" | dd conv=notrunc of="$filename" 2>&1; echo "${PIPESTATUS[@]}")
  { read; read; read -r byte_count _; read filter_status dd_status; } <<<"$dd_output"
  (( filter_status > 0 )) && return "$filter_status"
  (( dd_status > 0 )) && return "$dd_status"
  dd bs=1 seek="$byte_count" if=/dev/null of="$filename"
}

replace_with_filter file.txt tail -1

dd '" notrunc " L'opzione viene utilizzata per riscrivere i contenuti filtrati, al loro posto, mentre <=> è nuovamente necessario (con un numero di byte) per effettivamente troncare il file. Se la nuova dimensione del file è maggiore o uguale alla dimensione del file precedente, la seconda <=> chiamata non è necessaria.

I vantaggi di questo rispetto a un metodo di copia file sono: 1) non è necessario spazio su disco aggiuntivo, 2) prestazioni più veloci su file di grandi dimensioni e 3) shell pura (diversa da dd).

Come dice Lewis Baumstark, non gli piace che tu stia scrivendo con lo stesso nome file.

Questo perché la shell apre " file.txt " e lo tronca per eseguire il reindirizzamento prima di " cat file.txt " è eseguito. Quindi, devi

tail -1 file.txt > file2.txt; mv file2.txt file.txt
echo "$(tail -1 file.txt)" > file.txt

Solo per questo caso è possibile utilizzare

cat < file.txt | (rm file.txt; tail -1 > file.txt)
Questo aprirà & Quot; file.txt & Quot; appena prima della connessione " cat " con subshell in " (...) " ;. " rm file.txt " rimuoverà il riferimento dal disco prima che subshell lo aprirà per scrivere per " tail " ;, ma i contenuti saranno ancora disponibili tramite il descrittore aperto che viene passato a " cat " fino a quando non chiuderà lo stdin. Quindi è meglio essere sicuri che questo comando finisca o il contenuto di & Quot; file.txt & Quot; andrà perso

Sembra che non ti piaccia il fatto che lo stai scrivendo nello stesso nome file. Se fai quanto segue funziona:

$cat file.txt | tail -1 > anotherfile.txt

tail -1 > file.txt sovrascriverà il tuo file, facendo sì che cat legga un file vuoto perché la riscrittura avverrà prima di eseguire qualsiasi comando nella tua pipeline.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top