100 votes

Shell script lu manque la dernière ligne

J'ai un problème ... étrange avec un shell bash script sur lequel j'espérais avoir un aperçu.

Mon équipe travaille sur un script qui itère à travers les lignes d'un fichier et vérifie le contenu de chacune d'elles. Nous avions un bug où, lorsqu'il était exécuté via le processus automatisé qui séquence différents script ensemble, la dernière ligne n'était pas vue.

Le code utilisé pour itérer sur les lignes du fichier (nom stocké dans le fichier DATAFILE était

cat "$DATAFILE" | while read line 

Nous pourrions exécuter le script depuis la ligne de commande et il verrait toutes les lignes du fichier, y compris la dernière, sans problème. Cependant, lorsqu'il est exécuté par le processus automatisé (qui exécute le script qui génère le DATAFILE juste avant le script en question), la dernière ligne n'est jamais vue.

Nous avons mis à jour le code pour utiliser ce qui suit afin d'itérer sur les lignes, et le problème a été résolu :

for line in `cat "$DATAFILE"` 

Note : DATAFILE n'a jamais de nouvelle ligne écrite à la fin du fichier.

Ma question est en deux parties... Pourquoi la dernière ligne ne serait-elle pas vue par le code original, et pourquoi ce changement ferait-il une différence ?

La seule idée que j'ai trouvée pour expliquer pourquoi la dernière ligne n'a pas été vue était.. :

  • Le processus précédent, qui écrit le fichier, comptait sur le processus de fin pour fermer le descripteur de fichier.
  • Le problème script démarrait et ouvrait le fichier antérieur assez rapidement pour que, bien que le processus précédent se soit "terminé", il ne se soit pas "arrêté/nettoyé" suffisamment pour que le système ferme automatiquement le descripteur de fichier pour lui.

Ceci étant dit, il semble que, si vous avez 2 commandes dans un shell script, la première devrait être complètement arrêtée au moment où le script exécute la seconde.

Tout éclairage sur ces questions, en particulier la première, serait très apprécié.

131voto

Jonathan Leffler Points 299946

La norme C stipule que les fichiers texte doivent se terminer par un saut de ligne, sinon les données situées après le dernier saut de ligne risquent de ne pas être lues correctement.

ISO/IEC 9899:2011 §7.21.2 Streams (flux)

Un flux de texte est une séquence ordonnée de caractères composée en lignes, chaque ligne est composée de zéro ou plusieurs caractères plus un caractère de fin de ligne. Que la dernière ligne nécessite un caractère de fin de ligne est définie par l'implémentation. Les caractères peuvent être ajoutés, modifiés ou supprimés en entrée et en sortie afin de se conformer à différentes conventions de représentation du texte dans l'environnement hôte. Ainsi, il n'est pas nécessaire qu'il y ait une correspondance un à entre les caractères d'un flux et ceux de la représentation externe. représentation externe. Les données lues à partir d'un flux de texte seront nécessairement comparées aux données qui ont été précédemment écrites dans ce flux. qui ont été précédemment écrites dans ce flux uniquement si : les données ne sont constituées que de caractères d'impression et des caractères de contrôle (tabulation horizontale et tabulation verticale). caractères d'impression et les caractères de contrôle tabulation horizontale et nouvelle ligne ; aucun caractère de nouvelle ligne n'est immédiatement précédé de caractères d'espacement ; et le dernier caractère est un caractère de nouvelle ligne. Si les caractères d'espacement qui sont écrits immédiatement avant un caractère de nouvelle ligne apparaissent lors de la lecture, cela dépend de l'implémentation.

Je ne m'attendais pas à ce qu'un saut de ligne manquant à la fin d'un fichier puisse causer des problèmes dans le système de gestion de l'information. bash (ou tout autre shell Unix), mais cela semble être le problème de manière reproductible ( $ est l'invite dans cette sortie) :

$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done      # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done   # UUOC Award pending
abc
def
ghi
xxx
$

Il ne se limite pas non plus à bash - Le shell Korn ( ksh ) y zsh se comportent aussi comme ça. Je vis, j'apprends ; merci d'avoir soulevé la question.

Comme le montre le code ci-dessus, l'élément cat lit le fichier entier. Le site for line in `cat $DATAFILE` collecte toutes les sorties et remplace les séquences arbitraires d'espaces blancs par un seul blanc (je conclus que chaque ligne du fichier ne contient aucun blanc).

Testé sur Mac OS X 10.7.5.


Que dit POSIX ?

Le système POSIX read La spécification de la commande dit :

L'utilitaire read doit lire une seule ligne à partir de l'entrée standard.

Par défaut, à moins que l'option -r est spécifiée, <backslash> doit agir comme un caractère d'échappement. Un <backslash> non encapsulé conserve la valeur littérale du caractère suivant, à l'exception d'un <newline>. Si une <nouvelle ligne> suit le <backslash>, l'utilitaire de lecture doit l'interpréter comme une continuation de ligne. Les caractères <backslash> et <newline> doivent être supprimés avant de diviser l'entrée en champs. Tous les autres caractères <backslash> non encodés doivent être supprimés après la division de l'entrée en champs.

Si l'entrée standard est un périphérique terminal et que le shell invoquant est interactif, read demandera une ligne de continuation lorsqu'il lira une ligne d'entrée se terminant par un <backslash> <newline>, à moins que l'option -r est spécifiée.

La terminaison <nouvelle ligne> (le cas échéant) sont supprimés de l'entrée et les résultats sont fractionnés en champs comme dans le shell pour les résultats de l'expansion des paramètres (voir Fractionnement des champs) ; [...]

Notez que "(s'il y en a)" (accentuation ajoutée dans la citation) ! Il me semble que s'il n'y a pas de nouvelle ligne, le résultat devrait quand même être lu. D'un autre côté, il est également dit :

STDIN

L'entrée standard doit être un fichier texte.

et on en revient au débat sur la question de savoir si un fichier qui ne se termine pas par une nouvelle ligne est un fichier texte ou non.

Cependant, le raisonnement sur la même page documente :

Bien que l'entrée standard doive être un fichier texte, et donc se terminera toujours par une <nouvelle ligne> (sauf s'il s'agit d'un fichier vide), le traitement des lignes de continuation lorsque la commande -r n'est pas utilisée peut avoir pour conséquence que l'entrée ne se termine pas par une <nouvelle ligne>. Cela se produit si la dernière ligne du fichier d'entrée se termine par une <backslash> <newline>. C'est pour cette raison que "s'il y en a" est utilisé dans "La <nouvelle> de fin (s'il y en a) doit être supprimée de l'entrée" dans la description. Il ne s'agit pas d'un assouplissement de l'exigence selon laquelle l'entrée standard doit être un fichier texte.

Ce raisonnement doit signifier que le fichier texte est censé se terminer par une nouvelle ligne.

La définition POSIX d'un fichier texte est la suivante :

3.395 Fichier texte

Un fichier qui contient des caractères organisés en zéro ou plusieurs lignes. Les lignes ne contiennent pas de caractères NUL et aucune ne peut dépasser {LINE_MAX} octets de longueur, y compris le caractère <nouvelle ligne>. Bien que la norme POSIX.1-2008 ne fasse pas de distinction entre les fichiers texte et les fichiers binaires (voir la norme ISO C), de nombreux utilitaires ne produisent une sortie prévisible ou significative que lorsqu'ils opèrent sur des fichiers texte. Les utilitaires standard qui ont de telles restrictions spécifient toujours "fichiers texte" dans leurs sections STDIN ou INPUT FILES.

Cela ne stipule pas directement "se termine par une <nouvelle ligne>", mais s'en remet à la norme C et dit "Un fichier qui contient des caractères organisés en zéro ou plus". lignes "et quand nous regardons la définition POSIX d'une "Ligne", elle dit :

3.206 Ligne

Une séquence de zéro ou plus caractères non <newline> plus un caractère caractère de terminaison <nouvelle ligne>.

Ainsi, selon la définition POSIX, un fichier doit se terminer par une nouvelle ligne de fin, car il est composé de lignes et chaque ligne doit se terminer par une nouvelle ligne de fin.


Une solution au problème de l'absence de nouvelle ligne terminale

Note Gordon Davisson 's réponse . Un simple test montre que son observation est exacte :

$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$

Par conséquent, sa technique de :

while read line || [ -n "$line" ]; do echo $line; done < y

ou :

cat y | while read line || [ -n "$line" ]; do echo $line; done

fonctionnera pour les fichiers sans nouvelle ligne à la fin (au moins sur ma machine).


Je suis toujours surpris de constater que les shells laissent tomber le dernier segment (on ne peut pas l'appeler une ligne car il ne se termine pas par une nouvelle ligne) de l'entrée, mais il pourrait y avoir une justification suffisante dans POSIX pour le faire. Et il est clair qu'il est préférable de s'assurer que vos fichiers texte sont vraiment des fichiers texte se terminant par une nouvelle ligne.

94voto

Gordon Davisson Points 22534

Selon le Spécification POSIX pour la commande de lecture il doit renvoyer un état non nul si "La fin du fichier a été détectée ou si une erreur s'est produite". Puisque la fin de fichier est détectée lors de la lecture de la dernière "ligne", elle définit l'état suivant $line et renvoie ensuite un état d'erreur, et cet état d'erreur empêche la boucle de s'exécuter sur cette dernière "ligne". La solution est simple : faites en sorte que la boucle s'exécute si la commande de lecture réussit OU si quelque chose a été lu dans le fichier $line .

while read line || [ -n "$line" ]; do

34voto

Jahid Points 12756

J'ajoute quelques informations supplémentaires :

  1. Il n'y a pas besoin d'utiliser cat avec la boucle while. while ...;do something;done<file est suffisant.
  2. Ne lisez pas les lignes avec for .

Lorsque vous utilisez la boucle while pour lire des lignes :

  1. Définissez le IFS correctement (vous risquez de perdre l'indentation sinon).
  2. Vous devriez presque toujours utiliser l'option -r avec read.

En respectant les exigences ci-dessus, une boucle while correcte ressemblera à ceci :

while IFS= read -r line; do
  ...
done <file

Et pour le faire fonctionner avec des fichiers sans nouvelle ligne à la fin (je reprends ma solution depuis aquí ):

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done <file

Ou en utilisant grep avec la boucle while :

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)

2voto

ArunGJ Points 1444

Comme solution de rechange, avant de lire le fichier texte, on peut ajouter une nouvelle ligne au fichier.

echo -e "\n" >> $file_path

Nous devons passer l'argument -e à echo pour permettre l'interprétation des séquences d'échappement. https://superuser.com/questions/313938/shell-script-echo-new-line-to-file

1voto

Joel Bruner Points 466

Utilisez sed pour faire correspondre la dernière ligne d'un fichier, qu'il ajoutera ensuite une nouvelle ligne s'il n'y en a pas et qu'il fera un remplacement en ligne du fichier :

sed -i '' -e '$a\' file

Le code provient de ce stackexchange lien

Note : J'ai ajouté des guillemets simples vides à -i '' car, au moins dans OS X, -i utilisait -e comme extension de fichier pour le fichier de sauvegarde. J'aurais volontiers commenté le message original mais il me manquait 50 points. Peut-être que cela m'en fera gagner quelques-uns dans ce fil, merci.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X