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?

10
demandé sur Mark Biek 2008-09-23 23:29:26

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
19
répondu Charles Duffy 2014-07-16 16:00:38

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.
5
répondu Chris 2008-09-24 14:56:43

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.txt
ou en utilisant un utilitaire comme une éponge à partir de moreutils:
$cat file.txt | tail -1 | sponge file.txt
Cela fonctionne parce que sponge attend jusqu'à ce que son flux d'entrée se termine avant d'ouvrir son fichier de sortie.
3
répondu ephemient 2008-09-23 19:39:29

Lorsque vous soumettez votre chaîne de commande de bash, il procède de la façon suivante:

  1. crée une pipe d'entrée / sortie.
  2. commence"/usr/bin / tail -1", lit à partir du tuyau, et écrit au fichier.txt.
  3. 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.

2
répondu Craig Trader 2008-09-23 19:43:30

tmp=$(queue -1 fichier.txt); echo $tmp > fichier.txt;

2
répondu Ken 2008-09-23 19:44:42

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).

2
répondu m104 2013-05-14 13:13:52

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
1
répondu wnoise 2008-09-23 19:34:23
echo "$(tail -1 file.txt)" > file.txt
1
répondu ghostdog74 2010-03-25 05:24:30

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

1
répondu ony 2013-04-22 09:50:30

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
0
répondu Lewis Baumstark 2008-09-23 19:31:41

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.

0
répondu dsm 2008-09-23 19:34:37