web-dev-qa-db-fra.com

Pourquoi «alors que IFS = lecture» est utilisé si souvent, au lieu de «IFS =; pendant la lecture .. »?

Il semble que la pratique normale mettrait le réglage d'IFS en dehors de la boucle while afin de ne pas répéter le réglage pour chaque itération ... Est-ce juste un style habituel "singe voir, singe faire", comme il l'a été pour ce singe jusqu'à J'ai lu homme l, ou est-ce que je manque un piège subtil (ou flagrant) ici?

85
Peter.O

Le piège est que

IFS=; while read..

définit IFS pour l'ensemble de l'environnement Shell en dehors de la boucle, tandis que

while IFS= read

le redéfinit uniquement pour l'invocation read (sauf dans le Bourne Shell). Vous pouvez vérifier que faire une boucle comme

while IFS= read xxx; ... done

puis après une telle boucle, echo "blabalbla $IFS ooooooo" impressions

blabalbla
 ooooooo

alors qu'après

IFS=; read xxx; ... done

le IFS reste redéfini: maintenant echo "blabalbla $IFS ooooooo" impressions

blabalbla  ooooooo

Donc, si vous utilisez le deuxième formulaire, vous devez vous rappeler de réinitialiser: IFS=$' \t\n'.


La deuxième partie de cette question a été fusionnée ici , j'ai donc supprimé la réponse connexe d'ici.

86
rozcietrzewiacz

Regardons un exemple, avec du texte d'entrée soigneusement conçu:

text=' hello  world\
foo\bar'

C'est deux lignes, la première commençant par un espace et se terminant par une barre oblique inverse. Tout d'abord, regardons ce qui se passe sans aucune précaution autour de read (mais en utilisant printf '%s\n' "$text" pour imprimer soigneusement $text sans risque d'expansion). (Au dessous de, $ ‌ est l'invite du shell.)

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

read a mangé les barres obliques inverses: la barre oblique inversée-newline fait ignorer la nouvelle ligne, et la barre oblique inversée-n'importe quoi ignore cette première barre oblique inverse. Pour éviter que les barres obliques inverses soient traitées spécialement, nous utilisons read -r.

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

C'est mieux, nous avons deux lignes comme prévu. Les deux lignes contiennent presque le contenu souhaité: le double espace entre hello et world a été conservé, car il se trouve dans la variable line. En revanche, l'espace initial a été consommé. C'est parce que read lit autant de mots que vous passez de variables, sauf que la dernière variable contient le reste de la ligne - mais elle commence toujours par le premier mot, c'est-à-dire que les espaces initiaux sont ignorés.

Ainsi, afin de lire chaque ligne littéralement, nous devons nous assurer qu'aucun Word splitting ne se passe. Pour ce faire, définissez la variable IFS sur une valeur vide.

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

Notez comment nous définissons IFS spécifiquement pour la durée du read intégré . Le IFS= read -r line définit la variable d'environnement IFS (sur une valeur vide) spécifiquement pour l'exécution de read. Il s'agit d'une instance de la syntaxe générale commande simple : une séquence (éventuellement vide) d'affectations de variables suivie d'un nom de commande et de ses arguments (vous pouvez également lancer des redirections à tout moment). Puisque read est une fonction intégrée, la variable ne se retrouve jamais réellement dans l'environnement d'un processus externe; néanmoins la valeur de $IFS est ce que nous y affectons tant que read s'exécute¹. Notez que read n'est pas un spécial intégré , donc l'affectation ne dure que pour sa durée.

Nous prenons donc soin de ne pas modifier la valeur de IFS pour les autres instructions qui peuvent en dépendre. Ce code fonctionnera peu importe ce que le code environnant a initialement défini IFS, et il ne causera aucun problème si le code à l'intérieur de la boucle s'appuie sur IFS.

Contrairement à cet extrait de code, qui recherche les fichiers dans un chemin séparé par deux-points. La liste des noms de fichiers est lue dans un fichier, un nom de fichier par ligne.

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

Si la boucle était while IFS=; read -r name; do …, puis for dir in $PATH ne se séparerait pas $PATH en composants séparés par deux points. Si le code était IFS=; while read …, il serait encore plus évident que IFS n'est pas défini sur : dans le corps de la boucle.

Bien sûr, il serait possible de restaurer la valeur de IFS après avoir exécuté read. Mais cela nécessiterait de connaître la valeur précédente, ce qui représente un effort supplémentaire. IFS= read est le moyen le plus simple (et, commodément, également le plus court).

¹ Et, si read est interrompu par un signal piégé, peut-être pendant l'exécution du trap - cela n'est pas spécifié par POSIX et dépend du Shell dans la pratique.

Outre les (déjà clarifiées) IFS différences de portée entre les while IFS='' read, IFS=''; while read et while IFS=''; read idiomes (par commande vs script/à l'échelle du shell IFS portée des variables), la leçon à retenir est que vous perdez le début et espaces de fin d'une ligne d'entrée si la variable IFS est définie sur (contient un) espace.

Cela peut avoir des conséquences assez graves si les chemins d'accès aux fichiers sont traités.

Par conséquent, la définition de la variable IFS sur la chaîne vide est tout sauf une mauvaise idée car elle garantit que les espaces de début et de fin d'une ligne ne sont pas supprimés.

Voir aussi: Bash, lecture ligne par ligne du fichier, avec IFS

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
3
jon

Inspiré par La réponse de Yuzem

Si vous voulez définir IFS sur un caractère réel, cela a fonctionné pour moi

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
1
Steven Penny