Comment puis-je utiliser un fichier dans une commande et rediriger la sortie vers le même fichier sans le tronquer?


98

Fondamentalement, je veux prendre comme texte d'entrée d'un fichier, supprimer une ligne de ce fichier et renvoyer la sortie dans le même fichier. Quelque chose dans ce sens si cela rend les choses plus claires.

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

cependant, lorsque je fais cela, je me retrouve avec un fichier vierge. Des pensées?


Réponses:


84

Vous ne pouvez pas faire cela car bash traite d'abord les redirections, puis exécute la commande. Ainsi, au moment où grep regarde nom_fichier, il est déjà vide. Vous pouvez cependant utiliser un fichier temporaire.

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

comme ça, pensez à utiliser mktemppour créer le tmpfile mais notez que ce n'est pas POSIX.


47
La raison pour laquelle vous ne pouvez pas faire cela: bash traite d'abord les redirections, puis exécute la commande. Ainsi, au moment où grep regarde nom_fichier, il est déjà vide.
glenn jackman le

1
@glennjackman: par "traite la redirection, vous voulez dire que dans le cas de> il ouvre le fichier et l'efface et dans le cas de >> il ne l'ouvre que"?
Razvan du

2
oui, mais à noter dans cette situation, la >redirection ouvrira le fichier et le tronquera avant le lancement du shell grep.
glenn jackman du

1
Voir ma réponse si vous ne souhaitez pas utiliser de fichier temporaire, mais veuillez ne pas voter pour ce commentaire.
Zack Morris

Au lieu de cela, la réponse utilisant la spongecommande doit être acceptée.
vlz le

96

Utilisez une éponge pour ce genre de tâches. Sa partie de moreutils.

Essayez cette commande:

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

4
Merci d'avoir répondu. Comme ajout éventuellement utile, si vous utilisez homebrew sur Mac, vous pouvez utiliser brew install moreutils.
Anthony Panozzo

2
Ou sudo apt-get install moreutilssur les systèmes basés sur Debian.
Jonah

3
Zut! Merci de m'avoir présenté moreutils =) quelques programmes sympas là-bas!
netigger

merci beaucoup, moreutils pour le sauvetage! éponge comme un patron!
aqquadro

3
Attention, "sponge" est destructeur, donc si vous avez une erreur dans votre commande, vous pouvez effacer votre fichier d'entrée (comme je l'ai fait la première fois que j'ai essayé sponge). Assurez-vous que votre commande fonctionne et / ou que le fichier d'entrée est sous contrôle de version si vous essayez d'itérer pour que la commande fonctionne.
user107172

18

Utilisez plutôt sed:

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

1
iirc -iest uniquement une extension GNU, notez simplement.
c00kiemon5ter

3
Sur * BSD (et donc aussi OSX), vous pouvez dire -i ''que l'extension n'est pas strictement obligatoire, mais l' -ioption nécessite un argument.
tripleee

14

essayez ce simple

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

Votre fichier ne sera pas vierge cette fois :) et votre sortie est également imprimée sur votre terminal.


1
J'aime cette solution! Et si vous ne voulez pas qu'il soit imprimé dans le terminal, vous pouvez toujours rediriger la sortie vers /dev/nullou vers des endroits similaires.
Frozn

4
Cela efface également le contenu du fichier. Est-ce dû à une différence GNU / BSD? Je suis sur macOS ...
ssc

7

Vous ne pouvez pas utiliser l'opérateur de redirection ( >ou >>) vers le même fichier, car il a une priorité plus élevée et il créera / tronquera le fichier avant même que la commande ne soit appelée. Pour éviter cela, vous devez utiliser des outils appropriés tels que tee, sponge, sed -iou tout autre outil qui peut écrire des résultats dans le fichier (par exemple sort file -o file).

Fondamentalement, rediriger l'entrée vers le même fichier d'origine n'a pas de sens et vous devez utiliser des éditeurs sur place appropriés pour cela, par exemple l'éditeur Ex (qui fait partie de Vim):

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

où:

  • '+cmd'/ -c- exécuter n'importe quelle commande Ex / Vim
  • g/pattern/d- Retirer les lignes correspondant à un motif à l' aide global ( help :g)
  • -s- mode silencieux ( man ex)
  • -c wq- exécuter :writeet :quitcommandes

Vous pouvez utiliser sedpour obtenir le même (comme cela a déjà montré dans d' autres réponses), mais en place ( -i) est non-standard extension de FreeBSD (peut fonctionner différemment entre Unix / Linux) et , fondamentalement , il est un s tream ed Itor, pas un éditeur de fichiers . Voir: Le mode Ex a-t-il une utilisation pratique?


6

Une alternative de ligne - définissez le contenu du fichier comme variable:

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

4

Étant donné que cette question est le meilleur résultat dans les moteurs de recherche, voici une ligne unique basée sur https://serverfault.com/a/547331 qui utilise un sous-shell au lieu de sponge(qui ne fait souvent pas partie d'une installation vanille comme OS X) :

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

Le cas général est:

echo "$(cat file_name)" > file_name

Edit, la solution ci-dessus a quelques mises en garde:

  • printf '%s' <string>doit être utilisé à la place de echo <string>pour que les fichiers contenant -nne provoquent pas de comportement indésirable.
  • La substitution de commande supprime les retours à la ligne ( c'est un bogue / fonctionnalité des shells comme bash ), nous devrions donc ajouter un caractère postfix comme xà la sortie et le supprimer à l'extérieur via l' expansion des paramètres d'une variable temporaire comme ${v%x}.
  • L'utilisation d'une variable temporaire $vstomps la valeur de toute variable existante $vdans l'environnement shell actuel, nous devons donc imbriquer l'expression entière entre parenthèses pour conserver la valeur précédente.
  • Un autre bogue / caractéristique des shells comme bash est que la substitution de commande supprime les caractères non imprimables comme nullde la sortie. J'ai vérifié cela en appelant dd if=/dev/zero bs=1 count=1 >> file_nameet en le visualisant en hexadécimal avec cat file_name | xxd -p. Mais echo $(cat file_name) | xxd -pest dépouillé. Donc, cette réponse ne doit pas être utilisée sur des fichiers binaires ou quoi que ce soit utilisant des caractères non imprimables, comme l'a souligné Lynch .

La solution générale (albiet légèrement plus lente, plus gourmande en mémoire et en supprimant toujours les caractères non imprimables) est:

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

Test de 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

Devrait imprimer:

hello
world

Alors que l'appel cat file_uniquely_named.txt > file_uniquely_named.txtdans le shell actuel:

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

Imprime une chaîne vide.

Je n'ai pas testé cela sur de gros fichiers (probablement plus de 2 ou 4 Go).

J'ai emprunté cette réponse à Hart Simha et kos .


2
Bien sûr, cela ne fonctionnera pas avec des fichiers volumineux. Cela ne peut pas être une bonne solution ou fonctionner tout le temps. Ce qui se passe, c'est que bash exécute d'abord la commande, puis charge le stdout de catet le place comme premier argument dans echo. Bien sûr, les variables non imprimables ne sortiront pas correctement et corrompront les données. N'essayez pas de rediriger un fichier vers lui-même, cela ne peut tout simplement pas être bon.
Lynch

1

Il y a aussi ed(comme alternative à 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

Vous pouvez le faire en utilisant la substitution de processus .

C'est un peu un hack car bash ouvre tous les tuyaux de manière asynchrone et nous devons contourner cela en utilisant sleepdonc YMMV.

Dans votre exemple:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) crée un fichier temporaire qui reçoit la sortie de grep
  • sleep 1 retards pendant une seconde pour donner à grep le temps d'analyser le fichier d'entrée
  • cat > file_nameécrit enfin la sortie

1

Vous pouvez utiliser slurp avec POSIX Awk:

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

Exemple


1
Il faut peut-être souligner que "slurp" signifie "lire le fichier entier en mémoire". Si vous avez un gros fichier d'entrée, vous voudrez peut-être éviter cela.
tripleee

1

C'est très possible, il vous suffit de vous assurer qu'au moment où vous écrivez la sortie, vous l'écrivez dans un fichier différent. Cela peut être fait en supprimant le fichier après avoir ouvert un descripteur de fichier, mais avant d'y écrire:

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

Ou ligne par ligne, pour mieux le comprendre:

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

C'est toujours une chose risquée à faire, car si COMMAND ne fonctionne pas correctement, vous perdrez le contenu du fichier. Cela peut être atténué en restaurant le fichier si COMMAND renvoie un code de sortie différent de zéro:

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

Nous pouvons également définir une fonction shell pour faciliter son utilisation:

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

Exemple :

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

Notez également que cela conservera une copie complète du fichier d'origine (jusqu'à ce que le troisième descripteur de fichier soit fermé). Si vous utilisez Linux et que le fichier que vous traitez est trop volumineux pour tenir deux fois sur le disque, vous pouvez consulter ce script qui dirigera le fichier vers la commande spécifiée bloc par bloc tout en annulant le déjà traité blocs. Comme toujours, lisez les avertissements dans la page d'utilisation.


0

Essaye ça

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

cat testfile
AAA
BBB
CCC

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

Une brève explication ou même des commentaires peuvent être utiles.
Riche

Je pense que cela fonctionne parce que l'extrapolation de chaîne s'exécute avant l'opérateur de redirection, mais je ne sais pas exactement
Виктор Пупкин

0

Ce qui suit accomplira la même chose que sponge, sans exiger moreutils:

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

La --random-source=/dev/zeropartie astuces shufpour faire son travail sans faire de mélange du tout, donc elle mettra en mémoire tampon votre entrée sans la modifier.

Cependant, il est vrai que l'utilisation d'un fichier temporaire est préférable, pour des raisons de performances. Alors, voici une fonction que j'ai écrite qui le fera pour vous de manière généralisée:

# 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"
}

-2

J'utilise généralement le programme tee pour faire ceci:

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

Il crée et supprime un fichier temporaire par lui-même.


Désolé, tee n'est pas garanti de fonctionner. Voir askubuntu.com/a/752451/335781 .
studgeek
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.