Problème avec la redirection de la sortie Bash
j'essayais de supprimer toutes les lignes d'un fichier sauf la dernière ligne, mais la commande suivante n'a pas fonctionné, bien que file.txt n'est pas vide.
$cat file.txt |tail -1 > file.txt
$cat file.txt
Pourquoi est-ce ainsi?
11 réponses
rediriger à partir d'un fichier via un pipeline vers le même fichier n'est pas sûr; si file.txt
est écrasé par le shell lors de la mise en place de la dernière étape du pipeline avant tail
commence la lecture de la première étape, vous finissez avec une sortie vide.
procédez de la manière suivante à la place:
tail -1 file.txt >file.txt.new && mv file.txt.new file.txt
...Eh bien, en fait, ne faites pas cela dans le code de production; en particulier si vous êtes dans un environnement sensible à la sécurité et que vous exécutez en tant que root, ce qui suit est plus approprié:
tempfile="$(mktemp file.txt.XXXXXX)"
chown --reference=file.txt -- "$tempfile"
chmod --reference=file.txt -- "$tempfile"
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt
une autre approche (éviter les fichiers temporaires, sauf si <<<
crée implicitement sur votre plate-forme) est la suivante:
lastline="$(tail -1 file.txt)"; cat >file.txt <<<"$lastline"
(l'implémentation ci-dessus est spécifique à bash, mais fonctionne dans les cas où echo ne le fait pas -- comme lorsque la dernière ligne contient "--version", par exemple).
enfin, on peut utiliser une éponge de moreutils:
tail -1 file.txt | sponge file.txt
Vous pouvez utiliser sed pour supprimer toutes les lignes sauf la dernière d'un fichier:
sed -i '$!d' file
- - i indique à sed de remplacer le fichier en place; sinon, le résultat s'écrirait à STDOUT.
- $ est l'adresse qui correspond à la dernière ligne du fichier.
- d est la commande supprimer. Dans ce cas, il est niée par !, donc toutes les lignes l'adresse correspondante sera supprimée.
avant que' cat 'ne soit exécuté, Bash a déjà ouvert le fichier'.txt " pour l'écriture, l'effacement de son contenu.
en général, n'écrivez pas aux fichiers que vous lisez dans le même énoncé. Cela peut être fait en écrivant dans un fichier différent, comme ci-dessus:
$cat file.txt | tail -1 >anotherfile.txt $mv anotherfile.txt file.txtou en utilisant un utilitaire comme une éponge à partir de moreutils:
$cat file.txt | tail -1 | sponge file.txtCela fonctionne parce que sponge attend jusqu'à ce que son flux d'entrée se termine avant d'ouvrir son fichier de sortie.
Lorsque vous soumettez votre chaîne de commande de bash, il procède de la façon suivante:
- crée une pipe d'entrée / sortie.
- commence"/usr/bin / tail -1", lit à partir du tuyau, et écrit au fichier.txt.
- Commence à "/usr/bin/cat fichier.txt", écrit à la pipe.
au moment où 'cat' commence à lire, ' file.txt' a déjà été tronqué par 'tail'.
tout cela fait partie de la conception D'Unix et de l'environnement shell, et remonte tout le temps à l'origine du Bourne shell. C'est une fonctionnalité, pas un bug.
cela fonctionne bien dans un shell Linux:
replace_with_filter() {
local filename=""; 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
est utilisée pour réécrire le contenu filtré, en place, alors que dd
est nécessaire à nouveau (avec un nombre d'octets) pour réellement tronquer le fichier. Si la nouvelle taille de fichier est supérieure ou égale à l'ancienne, la seconde dd
l'invocation n'est pas nécessaire.
les avantages de cette méthode par rapport à une méthode de copie de fichier sont: 1) pas d'espace disque supplémentaire nécessaire, 2) des performances plus rapides sur les gros fichiers, et 3) Coque pure (autre que dd).
comme le dit Lewis Baumstark, il n'aime pas que vous écriviez sur le même nom de fichier.
C'est parce que la coquille s'ouvre "fichier.txt" et tronque à faire la redirection avant "fichier cat.txt" est exécutée. Ainsi, vous devez vous
tail -1 file.txt > file2.txt; mv file2.txt file.txt
Juste pour ce cas, il est possible d'utiliser
cat < file.txt | (rm file.txt; tail -1 > file.txt)Qui va s'ouvrir "le fichier.txt" juste avant la connexion "chat" avec shell interne est exécuté"...)". "fichier rm.txt "supprimera la référence du disque avant que subshell l'ouvre pour écrire pour "queue", mais le contenu sera toujours disponible par le descripteur ouvert qui est passé à" cat " jusqu'à ce qu'il ferme stdin. Donc, vous avez intérêt à être sûr que cette commande terminera ou le contenu de "file.txt" seront perdues
il semble ne pas aimer le fait que vous l'écriviez à nouveau au même nom de fichier. Si vous cela fonctionne:
$cat file.txt | tail -1 > anotherfile.txt
tail -1 > file.txt
va écraser votre fichier, provoquant cat à lire un fichier vide parce que la ré-écriture se produira avant les commandes de votre pipeline sont exécutées.