web-dev-qa-db-fra.com

Script shell lu manquant dernière ligne

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

Mon équipe travaille sur un script qui parcourt les lignes d'un fichier et en vérifie le contenu. Nous avions un bogue dans lequel, lorsqu’il était exécuté via le processus automatisé qui séquençait différents scripts, la dernière ligne n’était pas visible.

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

cat "$DATAFILE" | while read line 

Nous pourrions exécuter le script à partir de la ligne de commande et il verrait chaque ligne du fichier, y compris la dernière, très bien. Cependant, lorsqu'il est exécuté par le processus automatisé (qui exécute le script qui génère le fichier DATAFILE juste avant le script en question), la dernière ligne n'est jamais vue.

Nous avons mis à jour le code pour utiliser les éléments suivants pour parcourir les lignes et le problème a été résolu:

for line in `cat "$DATAFILE"` 

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

Ma question est en deux parties ... Pourquoi la dernière ligne ne serait-elle pas vue par le code d'origine et pourquoi cela changerait-il une différence?

Je pensais seulement que je pouvais trouver pourquoi la dernière ligne ne serait pas vue:

  • Le processus précédent, qui écrivait le fichier, s’appuyait sur le processus pour terminer le descripteur de fichier.
  • Le script qui posait problème commençait et ouvrait le fichier suffisamment tôt pour que, bien que le processus précédent soit "terminé", il ne soit pas suffisamment "fermé/nettoyé" pour que le système ferme automatiquement le descripteur de fichier.

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

Toute compréhension des questions, en particulier de la première, serait très appréciée.

47
RHSeeger

La norme C stipule que les fichiers texte doivent se terminer par une nouvelle ligne ou que les données qui suivent la dernière ligne peuvent ne pas être lues correctement.

ISO/IEC 9899: 2011 §7.21.2 Flux

Un flux de texte est une séquence ordonnée de caractères composée de lignes, chaque ligne composé de zéro ou plusieurs caractères plus un caractère de fin de ligne. Si le la dernière ligne nécessite un caractère de fin de ligne nouvelle défini par l'implémentation. Personnages vous devrez peut-être ajouter, modifier ou supprimer des entrées et des sorties pour vous conformer aux différences.. conventions pour représenter le texte dans l'environnement hôte. Ainsi, il n’est pas nécessaire qu’il y ait un one-to - une correspondance entre les caractères d'un flux et ceux de l'externe représentation. Les données lues à partir d'un flux de texte seront nécessairement égales aux données qui étaient précédemment écrites dans ce flux uniquement si: les données consistent uniquement en impression caractères et les caractères de contrôle onglet horizontal et nouvelle ligne; aucun caractère de nouvelle ligne n'est immédiatement précédé par des espaces; et le dernier caractère est un caractère de nouvelle ligne . Indique si les espaces sont écrits immédiatement avant un caractère de nouvelle ligne apparaît lorsque lu est défini par l'implémentation.

Je n'aurais pas de nouvelle ligne manquante à la fin du fichier pour causer des problèmes dans bash (ou tout shell Unix), mais cela semble être le problème 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 n’est pas non plus limité à bash - Korn Shell (ksh) et zsh se comportent également de la sorte. Je vis, j'apprends; merci d'avoir soulevé la question.

Comme illustré dans le code ci-dessus, la commande cat lit l'intégralité du fichier. La technique 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?

La spécification POSIX read command indique:

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

Par défaut, sauf si l'option -r est spécifiée, la <barre oblique inverse> doit agir comme un caractère d'échappement. Une <barre oblique inverse> non échappée doit conserver la valeur littérale du caractère suivant, à l'exception d'une <nouvelle ligne>. Si un <newline> suit la <barre oblique inverse>, l'utilitaire de lecture l'interprète comme une continuation de ligne. La <barre oblique inverse> et le <newline> doivent être supprimés avant de fractionner l'entrée en champs. Tous les autres caractères <barres obliques inverses> non échappés doivent être supprimés après fractionnement de l'entrée en champs.

Si l'entrée standard est un terminal et que le shell appelant est interactif, read doit demander une ligne de continuation lorsqu'il lit une ligne d'entrée se terminant par une <barre oblique inverse> <nouvelle ligne>, sauf si l'option -r est spécifiée.

La <nouvelle ligne> finale (le cas échéant) doit être supprimée de l'entrée et les résultats doivent être divisés en champs comme dans le shell pour les résultats du développement des paramètres (voir Fractionnement de champ); [...]

Notez que '(le cas échéant)' (italiques ajoutés entre guillemets)! Il me semble que s'il n'y a pas de nouvelle ligne, il devrait quand même lire le résultat. D'autre part, il est également dit:

STDIN

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

et vous revenez ensuite au débat sur le point de savoir si un fichier qui ne se termine pas par une nouvelle ligne est un fichier texte ou non.

Cependant, la justification sur la même page documente:

Bien que l'entrée standard doive être un fichier texte et se termine donc toujours par un <newline> (sauf s'il s'agit d'un fichier vide), le traitement des lignes de continuation lorsque l'option -r n'est pas utilisée peut avoir pour résultat que l'entrée ne se termine pas. avec une <nouvelle ligne>. Cela se produit si la dernière ligne du fichier d'entrée se termine par une <barre oblique inverse> <nouvelle ligne>. C’est pour cette raison que "le cas échéant" est utilisé dans "La <nouvelle ligne> finale (le cas échéant) doit être supprimée de l’entrée" dans la description. Ce n'est pas un assouplissement de l'exigence selon laquelle l'entrée standard doit être un fichier texte.

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

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

3.395 Fichier texte

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

Cela ne stipule pas que "se termine par une <nouvelle ligne>" directement, mais se reporte à la norme C.Une solution au problème 'no terminal newline'.


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 lâchent le dernier segment de l'entrée (on ne peut pas l'appeler une ligne), mais il peut y avoir une justification suffisante dans POSIX pour le faire. Et il est clairement préférable de s’assurer que vos fichiers texte sont réellement des fichiers texte se terminant par une nouvelle ligne.


I'm still surprised to find that the shells drop the last segment (it can't be called a line because it doesn't end with a newline) of the input, but there might be sufficient justification in POSIX to do so. And clearly it is best to ensure that your text files really are text files ending with a newline.

62
Jonathan Leffler

Selon la spécification POSIX pour la commande de lecture , il devrait renvoyer un statut différent de zéro si "La fin du fichier a été détectée ou une erreur est survenue". Étant donné que EOF est détecté lors de la lecture de la dernière "ligne", il définit $line, puis renvoie un statut d'erreur, qui empêche la boucle de s'exécuter sur cette dernière "ligne". La solution est simple: faire exécuter la boucle si la commande de lecture réussit OR si quelque chose a été lu dans $line.

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

Ajout de quelques informations supplémentaires:

  1. Il n'est pas nécessaire 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 une boucle While pour lire des lignes:

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

si vous répondez aux exigences ci-dessus, une boucle while appropriée se présentera comme suit:

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

Et pour que cela fonctionne avec des fichiers sans nouvelle ligne à la fin (republier ma solution de ici ):

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)
11
Jahid

Je soupçonne que ne pas avoir newline dans la dernière ligne de votre fichier pourrait être à l'origine de ce problème Pour tester, pouvez-vous légèrement modifier votre script et lire DATAFILE comme ceci:

while read line
do
    echo $line # do processing here
done < "$DATAFILE"

Et voyez si cela fait une différence.

1
anubhava

Utilisez sed pour faire correspondre la dernière ligne d'un fichier, auquel il ajoutera une nouvelle ligne s'il n'en existe pas et le fait remplacer inline le fichier:

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

Le code provient de stackexchange link

Remarque: J'ai ajouté des guillemets simples vides à -i '' car, du moins sous OS X, -i utilisait -e comme extension de fichier pour le fichier de sauvegarde. J'aurais volontiers commenté le post original mais il me manquait 50 points. Peut-être que cela me rapportera quelques points dans ce fil, merci.

1
Joel Bruner

J'avais un problème similaire . Je faisais un chat dans un fichier, le passant à une sorte puis le résultat à un 'en lecture var1 var2 var3' . C'est-à-dire: cat $ FILE | sort -k3 | pendant la lecture Compte IP Namedo Le travail sous "do" était une instruction if qui identifiait des données changeantes dans le champ $ Name et était basé sur un changement ou non le changement a fait la somme de $ Count ou l’impression de la ligne résumée dans le rapport . J'ai également rencontré le problème suivant: je ne pouvais pas obtenir la dernière ligne à imprimer dans le rapport . J'y suis allé avec le simple moyen de rediriger le chat./sort dans un nouveau fichier, en faisant écho à une nouvelle ligne dans ce nouveau fichier et ALORS a exécuté mon "nom de nom IP en lecture" sur le nouveau fichier avec des résultats positifs . c.-à-d.: cat $ FILE | sort -k3> NEWFILE echo "\ n" >> NEWFILEcat NEWFILE | lors de la lecture du compte IP Namedo Parfois, le plus simple est l’inélégant.

0
Gulesbaron

Pour contourner le problème, avant de lire le fichier texte, vous pouvez ajouter une nouvelle ligne au fichier. 

echo "\n" >> $file_path

Cela garantira que toutes les lignes précédemment contenues dans le fichier seront lues.

0
ArunGJ

J'ai testé cela en ligne de commande

# create dummy file. last line doesn't end with newline
printf "%i\n%i\nNo-newline-here" >testing

Testez avec votre première forme (de la tuyauterie à la boucle while)

cat testing | while read line; do echo $line; done

Cela manque la dernière ligne, ce qui est logique car read reçoit uniquement les entrées qui se terminent par une nouvelle ligne.


Testez avec votre deuxième formulaire (substitution de commande)

for line in `cat testbed1` ; do echo $line; done

Cela obtient aussi la dernière ligne


read n'entre que si elle est terminée par une nouvelle ligne, c'est pourquoi vous manquez la dernière ligne.

D'autre part, dans la seconde forme

`cat testing` 

se développe à la forme de 

line1\nline2\n...lineM 

qui est séparé par le shell en plusieurs champs en utilisant IFS, de sorte que vous obtenez 

line1 line2 line3 ... lineM 

C'est pourquoi vous obtenez toujours la dernière ligne.

p/s: Ce que je ne comprends pas, c'est comment vous obtenez le premier formulaire qui fonctionne ...

0
doubleDown