Come posso utilizzare un file in un comando e reindirizzare l'output allo stesso file senza troncarlo?


96

Fondamentalmente voglio prendere come testo di input da un file, rimuovere una riga da quel file e inviare l'output allo stesso file. Qualcosa del genere se questo lo rende più chiaro.

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

tuttavia, quando lo faccio mi ritrovo con un file vuoto. qualche idea?


Risposte:


84

Non puoi farlo perché bash elabora prima i reindirizzamenti, quindi esegue il comando. Quindi quando grep guarda nome_file, è già vuoto. Tuttavia, puoi utilizzare un file temporaneo.

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

in questo modo, considera l'utilizzo mktempper creare il tmpfile ma nota che non è POSIX.


47
Il motivo per cui non puoi farlo: bash elabora prima i reindirizzamenti, quindi esegue il comando. Quindi quando grep guarda nome_file, è già vuoto.
glenn jackman

1
@glennjackman: per "reindirizzamento processi intendi che nel caso di> apre il file e lo cancella e nel caso di >> lo apre solo"?
Razvan

2
sì, ma è importante notare che in questa situazione il >reindirizzamento aprirà il file e lo troncerà prima dell'avvio della shell grep.
glenn jackman,

1
Vedi la mia risposta se non vuoi usare un file temporaneo, ma per favore non votare questo commento.
Zack Morris

Invece di questo, dovrebbe essere accettata la risposta usando il spongecomando .
vlz

95

Usa una spugna per questo tipo di attività. Fa parte di moreutils.

Prova questo comando:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

4
Grazie per la risposta. Come aggiunta forse utile, se stai usando homebrew su Mac, puoi usare brew install moreutils.
Anthony Panozzo

2
O sudo apt-get install moreutilssu sistemi basati su Debian.
Jonah

3
Dannazione! Grazie per avermi presentato moreutils =) alcuni bei programmi lì!
netigger

grazie mille, moreutils per il salvataggio! spugna come un boss!
aqquadro

3
Un avvertimento, "sponge" è distruttivo, quindi se hai un errore nel tuo comando, puoi cancellare il tuo file di input (come ho fatto la prima volta che ho provato sponge). Assicurati che il tuo comando funzioni e / o il file di input sia sotto il controllo della versione se stai tentando di iterare per far funzionare il comando.
user107172

18

Usa sed invece:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

1
iirc -iè solo un'estensione GNU, basta notare.
c00kiemon5ter

3
Su * BSD (e quindi anche OSX) puoi dire che -i ''l'estensione non è strettamente obbligatoria, ma l' -iopzione richiede qualche argomento.
tripla

13

prova questo semplice

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Questa volta il file non sarà vuoto :) e anche l'output verrà stampato sul terminale.


1
Mi piace questa soluzione! E se non vuoi che venga stampato nel terminale, puoi comunque reindirizzare l'output in /dev/nullo luoghi simili.
Frozn

4
Questo cancella anche il contenuto del file qui. È dovuto a una differenza GNU / BSD? Sono su macOS ...
ssc

7

Non è possibile utilizzare l'operatore di reindirizzamento ( >o >>) allo stesso file, perché ha una precedenza maggiore e creerà / troncerà il file prima ancora che il comando venga invocato. Per evitare questo, si dovrebbe utilizzare strumenti appropriati come tee, sponge, sed -io qualsiasi altro strumento che può scrivere i risultati al file (ad esempio sort file -o file).

Fondamentalmente reindirizzare l'input allo stesso file originale non ha senso e dovresti usare editor sul posto appropriati per questo, ad esempio editor Ex (parte di Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

dove:

  • '+cmd'/ -c- esegue qualsiasi comando Ex / Vim
  • g/pattern/d- rimuove le linee che corrispondono a un modello usando global ( help :g)
  • -s- modalità silenziosa ( man ex)
  • -c wq- eseguire :writee :quitcomandi

Puoi usare sedper ottenere lo stesso risultato (come già mostrato in altre risposte), tuttavia in-place ( -i) è un'estensione di FreeBSD non standard (può funzionare in modo diverso tra Unix / Linux) e fondamentalmente è un s tream ed itor , non un editor di file . Vedi: La modalità Ex ha qualche utilità pratica?


6

Un'alternativa di riga: imposta il contenuto del file come variabile:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

4

Poiché questa domanda è il miglior risultato nei motori di ricerca, ecco un one-liner basato su https://serverfault.com/a/547331 che utilizza una subshell invece di sponge(che spesso non fa parte di un'installazione vanilla come OS X) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

Il caso generale è:

echo "$(cat file_name)" > file_name

Modifica, la soluzione sopra ha alcuni avvertimenti:

  • printf '%s' <string>dovrebbe essere usato al posto di in echo <string>modo che i file contenenti -nnon causino comportamenti indesiderati.
  • La sostituzione del comando elimina le nuove righe finali ( questo è un bug / caratteristica di shell come bash ) quindi dovremmo aggiungere un carattere postfisso come xl'output e rimuoverlo all'esterno tramite l' espansione dei parametri di una variabile temporanea come ${v%x}.
  • L'uso di una variabile temporanea $vcalpesta il valore di qualsiasi variabile esistente $vnell'ambiente shell corrente, quindi dovremmo annidare l'intera espressione tra parentesi per preservare il valore precedente.
  • Un altro bug / caratteristica di shell come bash è che la sostituzione del comando rimuove i caratteri non stampabili come nulldall'output. L'ho verificato chiamando dd if=/dev/zero bs=1 count=1 >> file_namee visualizzandolo in esadecimale con cat file_name | xxd -p. Ma echo $(cat file_name) | xxd -pè spogliato. Quindi questa risposta non dovrebbe essere utilizzata su file binari o qualsiasi cosa che utilizzi caratteri non stampabili, come ha sottolineato Lynch .

La soluzione generale (anche se leggermente più lenta, più dispendiosa in termini di memoria e ancora eliminando i caratteri non stampabili) è:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

Prova da https://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Dovrebbe stampare:

hello
world

Mentre chiamando cat file_uniquely_named.txt > file_uniquely_named.txtnella shell corrente:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Stampa una stringa vuota.

Non l'ho testato su file di grandi dimensioni (probabilmente oltre 2 o 4 GB).

Ho preso in prestito questa risposta da Hart Simha e kos .


2
Ovviamente non funzionerà con file di grandi dimensioni. Questa non può essere una buona soluzione o funzionare sempre. Quello che sta succedendo è che bash esegue prima il comando e poi carica lo stdout di cate lo mette come primo argomento a echo. Ovviamente le variabili non stampabili non verranno restituite correttamente e danneggeranno i dati. Non provare a reindirizzare un file a se stesso, semplicemente non può essere buono.
Lynch

1

C'è anche ed(in alternativa a sed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

1

Puoi farlo usando la sostituzione del processo .

È un po 'un trucco anche se bash apre tutti i tubi in modo asincrono e dobbiamo aggirare questo sleepproblema usando così YMMV.

Nel tuo esempio:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) crea un file temporaneo che riceve l'output da grep
  • sleep 1 ritardi di un secondo per dare a grep il tempo di analizzare il file di input
  • infine cat > file_namescrive l'output

1

Puoi usare slurp con POSIX Awk:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

Esempio


1
Forse dovrebbe essere sottolineato che "slurp" significa "leggere l'intero file in memoria". Se hai un file di input di grandi dimensioni, forse vuoi evitarlo.
tripla

1

Questo è assolutamente possibile, devi solo assicurarti che nel momento in cui scrivi l'output, lo stai scrivendo su un file diverso. Questo può essere fatto rimuovendo il file dopo aver aperto un descrittore di file su di esso, ma prima di scriverlo:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

Oppure riga per riga, per capirlo meglio:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

È ancora una cosa rischiosa da fare, perché se COMMAND non funziona correttamente, perderai il contenuto del file. Ciò può essere mitigato ripristinando il file se COMMAND restituisce un codice di uscita diverso da zero:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

Possiamo anche definire una funzione di shell per renderlo più facile da usare:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

Esempio :

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Inoltre, nota che questo manterrà una copia completa del file originale (fino alla chiusura del terzo descrittore di file). Se stai usando Linux e il file su cui stai elaborando è troppo grande per stare due volte sul disco, puoi controllare questo script che reindirizzerà il file al comando specificato blocco per blocco mentre annulli l'allocazione del già elaborato blocchi. Come sempre, leggi le avvertenze nella pagina di utilizzo.


0

Prova questo

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

Una breve spiegazione o anche commenti possono essere utili.
Rich

Penso che funzioni perché l'estrapolazione delle stringhe viene eseguita prima dell'operatore di reindirizzamento, ma non lo so esattamente
Виктор Пупкин

0

Quanto segue farà la stessa cosa che spongefa, senza richiedere moreutils:

    shuf --output=file --random-source=/dev/zero 

La --random-source=/dev/zeroparte si sforza di shuffare le sue cose senza fare alcun mescolamento, quindi bufferizzerà il tuo input senza alterarlo.

Tuttavia, è vero che l'utilizzo di un file temporaneo è il migliore, per motivi di prestazioni. Quindi, ecco una funzione che ho scritto che lo farà per te in modo generalizzato:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

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.