Comprendre l'IFS

71

Les quelques discussions suivantes sur ce site et StackOverflow ont été utiles pour comprendre comment cela IFSfonctionne:

Mais j'ai encore quelques petites questions. J'ai décidé de leur poser la même question car je pense que cela pourrait aider de meilleurs lecteurs:

Q1. IFSest typiquement discuté dans le contexte de "division de champ". La division de champ est -elle identique à la division de mot ?

Q2: La spécification POSIX dit :

Si la valeur de IFS est nulle, aucune division de champ ne doit être effectuée.

La définition est IFS=-elle identique à la valeur IFSnull? Est-ce que c'est ce que l'on entend par le mettre à un empty stringaussi?

Q3: Dans la spécification POSIX, j'ai lu ce qui suit:

Si IFS n'est pas défini, le shell doit se comporter comme si la valeur de IFS était <space>, <tab> and <newline>

Dites que je veux restaurer la valeur par défaut de IFS. Comment je fais ça? (plus spécifiquement, comment puis-je me référer à <tab>et <newline>?)

Q4: Enfin, comment ce code pourrait-il:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

se comporter si nous nous changeons la première ligne

while read -r line # Use the default IFS value

ou pour:

while IFS=' ' read -r line
Amelio Vazquez-Reina
la source

Réponses:

28
  1. Oui, ils sont pareils.
  2. Oui.
  3. En bash, et des coquilles similaires, vous pourriez faire quelque chose comme IFS=$' \t\n'. Sinon, vous pourriez insérer les codes de contrôle littéraux en utilisant [space] CTRL+V [tab] CTRL+V [enter]. Toutefois, si vous envisagez de le faire, il est préférable d’utiliser une autre variable pour stocker temporairement l’ancienne IFSvaleur, puis de la restaurer ultérieurement (ou de la remplacer temporairement pour une commande en utilisant la var=foo commandsyntaxe).
    • Le premier extrait de code place l'intégralité de la ligne lue, mot pour mot, dans la $linemesure où il n'y a pas de séparateur de champ pour le fractionnement des mots. Gardez toutefois à l'esprit que, comme de nombreux shells utilisent des chaînes de caractères pour stocker des chaînes, la première instance d'une NUL peut toujours entraîner la fin prématurée de son apparence.
    • Le deuxième extrait de code ne peut pas mettre une copie exacte de l'entrée dans $line. Par exemple, s'il existe plusieurs séparateurs de champs consécutifs, ils seront transformés en une seule instance du premier élément. Ceci est souvent reconnu comme une perte d'espaces blancs environnants.
    • Le troisième fragment de code fera la même chose que le deuxième, à la différence qu'il ne se divisera que sur un espace (pas l'espace habituel, l'onglet ou la nouvelle ligne).
Chris Down
la source
3
La réponse à Q2 est fausse: un vide IFSet un non défini IFSsont très différents. La réponse à la question 4 est en partie fausse: les séparateurs intérieurs ne sont pas touchés ici, ils sont les premiers et les derniers.
Gilles 'SO, arrête d'être méchant'
3
@Gilles: En Q2, aucune des trois dénominations données ne fait référence à un non défini IFS, toutes signifient IFS=.
Stéphane Gimenez
@ Gilles En Q2, je n'ai jamais dit qu'ils étaient identiques. Et les séparateurs internes sont touchés, comme indiqué ici: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Euh, quoi? Il devrait y avoir plusieurs délimiteurs d'espace, le moteur SO continue de les supprimer).
Chris Down
2
@ StéphaneGimenez, Chris: Oh, d'accord, désolé pour la Q2, j'ai mal interprété la question. Pour Q4, nous parlons de read; la dernière variable saisit tout ce qui reste sauf le dernier séparateur et laisse les séparateurs internes à l'intérieur.
Gilles 'SO- arrête d'être méchant'
1
Gilles a partiellement raison en ce qui concerne les espaces non supprimés en lecture. Lisez ma réponse pour plus de détails.
22

Q1: oui «Fractionnement de champs» et «fractionnement de mots» sont deux termes qui désignent le même concept.

Q2: oui Si IFSnon défini (c'est-à-dire après unset IFS), il est équivalent à IFSêtre défini sur $' \t\n'(un espace, un onglet et une nouvelle ligne). Si IFSest défini sur une valeur vide (c'est ce que “null” signifie ici) (c'est-à-dire après IFS=ou IFS=''ou IFS=""), aucune division de champ n'est effectuée (et $*, qui utilise normalement le premier caractère de $IFS, utilise un caractère d'espacement).

Q3: Si vous souhaitez avoir le IFScomportement par défaut , vous pouvez utiliser unset IFS. Si vous souhaitez définir IFSexplicitement cette valeur par défaut, vous pouvez mettre les caractères littéraux espace, tabulation et nouvelle ligne entre guillemets simples. Dans ksh93, bash ou zsh, vous pouvez utiliser IFS=$' \t\n'. De manière portable, si vous voulez éviter d'avoir un caractère de tabulation littéral dans votre fichier source, vous pouvez utiliser

IFS=" $(echo t | tr t \\t)
"

Q4: avec IFSune valeur vide, read -r linedéfinit linela ligne entière à l'exception de la nouvelle ligne de fin. Avec IFS=" ", les espaces au début et à la fin de la ligne sont supprimés. Avec la valeur par défaut de IFS, les tabulations et les espaces sont supprimés.

Gilles, arrête de faire le mal
la source
2
Q2 est en partie faux. Si IFS est vide, "$ *" est joint sans séparateurs. (car $@, il existe certaines variations entre les shells dans des contextes non listés, par exemple IFS=; var=$@). Il est à noter que lorsque IFS est vide, aucun fractionnement de mot n’est effectué mais $ var est toujours étendu à aucun argument au lieu d’un argument vide lorsque $ var est vide, et le globbing s’applique toujours. Vous devez donc toujours citer des variables (même désactiver globbing)
Stéphane Chazelas Le
13

Q1. Fractionnement de champs.

La division de champ est-elle identique à la division de mot?

Oui, les deux pointent vers la même idée.

Q2: Quand IFS est-il nul ?

La définition est IFS=''-elle identique à null, identique à une chaîne vide?

Oui, les trois signifient la même chose: aucune division de champ / mot ne doit être effectuée. En outre, cela affecte les champs d'impression (comme avec echo "$*") tous les champs seront concaténés ensemble sans espace.

Q3: (partie a) IFS non défini.

Dans la spécification POSIX, j'ai lu ce qui suit :

Si IFS n'est pas défini, le shell se comportera comme si la valeur de IFS était <espace> <tab> <nouvelle ligne> .

Ce qui est exactement équivalent à:

Avec un unset IFS, le shell doit se comporter comme si IFS était la valeur par défaut.

Cela signifie que la "division de champ" sera exactement la même chose avec une valeur IFS par défaut, ou non définie.
Cela ne signifie PAS que l'IFS fonctionnera de la même manière dans toutes les conditions. Étant plus spécifique, l'exécution OldIFS=$IFSdéfinira la variable OldIFSsur null , pas sur la valeur par défaut. Et essayer de remettre IFS en arrière, comme ceci, IFS=OldIFSmettra IFS à null, ne le laissera pas non défini comme auparavant. Fais attention !!.

Q3: (partie b) Restaurez IFS.

Comment pourrais-je restaurer la valeur d'IFS par défaut. Dites que je veux restaurer la valeur par défaut de IFS. Comment je fais ça? (plus spécifiquement, comment me référer à <tab> et à <nouvelle ligne> ?)

Pour zsh, ksh et bash (autant que je sache), IFS pourrait être défini sur la valeur par défaut comme suit:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Fait, vous n'avez besoin de lire rien d'autre.

Mais si vous devez redéfinir IFS pour sh, cela peut devenir complexe.

Jetons un coup d'oeil du plus facile au complet sans aucun inconvénient (sauf la complexité).

1.- Désactiver IFS.

Nous pourrions juste unset IFS(Lire Q3 partie a, ci-dessus.).

2.- Permuter les caractères.

En guise de solution de contournement, permuter les valeurs de tabulation et de nouvelle ligne simplifie la définition de la valeur de IFS et fonctionne de manière équivalente.

Définissez IFS sur <espace> <nouvelle ligne> <tab> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- Un simple? Solution:

Si certains scripts enfants ont besoin d'IFS correctement défini, vous pouvez toujours écrire manuellement:

IFS = '   
'

Où la séquence tapée manuellement était:, IFS='spacetabnewline'séquence qui a effectivement été tapée correctement ci-dessus (si vous devez confirmer, éditez cette réponse). Mais un copier / coller de votre navigateur va casser parce que le navigateur va serrer / cacher les espaces. Il est difficile de partager le code comme indiqué ci-dessus.

4.- Solution complète.

Écrire du code qui peut être copié en toute sécurité implique généralement des échappées imprimables sans ambiguïté.

Nous avons besoin de code qui "produit" la valeur attendue. Mais, même si conceptuellement correct, ce code NE définira PAS de fin \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

Cela se produit parce que, dans la plupart des shells, toutes les nouvelles lignes $(...)ou les `...`substitutions de commandes sont supprimées lors de l’agrandissement.

Nous devons utiliser un truc pour sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Une autre solution consiste à définir IFS en tant que valeur d’environnement de bash (par exemple), puis d’appeler sh (les versions qui acceptent que IFS soit défini via l’environnement), comme suit:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

En bref, sh fait de la réinitialisation d’IFS une situation par défaut plutôt étrange.

Q4: En code actuel:

Enfin, comment ce code pourrait-il:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

se comporter si nous nous changeons la première ligne

while read -r line # Use the default IFS value

ou pour:

while IFS=' ' read -r line

Premièrement: je ne sais pas si le echo $line(avec le var NON cité) est là sur le porpouse, ou pas. Il introduit un deuxième niveau de "division de champ" que la lecture n'a pas. Je vais donc répondre aux deux. :)

Avec ce code (pour que vous puissiez confirmer). Vous aurez besoin du xxd utile :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Je reçois:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

La première valeur est juste la valeur correcte de IFS='spacetabnewline'

La ligne suivante $acontient toutes les valeurs hexadécimales de la variable et une nouvelle ligne '0a' à la fin, telle qu'elle sera donnée à chaque commande de lecture.

La ligne suivante, pour laquelle IFS est null, n'effectue aucune «division de champ», mais la nouvelle ligne est supprimée (comme prévu).

Les trois lignes suivantes, IFS contenant un espace, supprimez les espaces initiaux et définissez la ligne var sur le solde restant.

Les quatre dernières lignes montrent ce que fera une variable non citée. Les valeurs seront réparties sur les espaces (plusieurs) et seront imprimées comme suit:bar,baz,qux,


la source
4

unset IFS efface IFS, même si IFS est par la suite présumé être "\ t \ n":

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Testé sur les versions 4.2.45 et 3.2.25 de Bash avec le même comportement.

derekm
la source
La question et la documentation liée ne parlent pas unsetde IFS, comme expliqué dans les commentaires de la réponse acceptée ici.
ILMostro_7