IFS : séparateurs & scripts BASH

closeCet article a été publié il y a 8 ans 5 mois 13 jours, il est donc possible qu’il ne soit plus à jour. Les informations proposées sont donc peut-être expirées.

Un truc qui m’a pris la tête il fût un temps et plus récemment, pour une question de syntaxe oubliée.
Voilà le topo : vous faites un rapide script BASH pour faire tel ou tel traitement sur des fichiers. Et là, chaque espace dans un nom de fichier vous met en l’air votre script car c’est vu comme « un saut de ligne », dirons-nous.
Il faut alors penser à jouer avec le « Internal Field Separator » – IFS – pour indiquer à BASH quel(s) caractère(s) considérer comme saut de ligne.
Par défaut, on trouve le saut de ligne « \n », mais aussi la tabulation (« \t ») et l’espace !!!

Simplification possible du problème : un extrêmiste pinguiste vous dira simplement que les espaces dans un nom de fichier, c’est mal. Ca a beau être mal, qui n’a jamais collé un espace dans un nom de fichier ? qui a le choix de ne pas le faire ? ou de tuer ses gentils utilisateurs le faisant ?

En images, ça donne ça :

user@srv:/tmp$ echo >> liste
champ1.1;champ1.2;champ1.3
champ2.1;champ avec espace;champ2.3
champ   avec_tabulation;champ3.2;champ pour finir
CTRL-D pour finir la saisie

user@srv:/tmp$ for i in `cat liste`
> do
> echo $i
> done
champ1.1;champ1.2;champ1.3
champ2.1;champ
avec
espace;champ2.3
champ
avec_tabulation;champ3.2;champ
pour
finir

Houuu le vilain résultat pour quelqu’un qui voulait voir afficher 3 lignes, pour par exemple faire un « awk -F ‘;’ ‘{print $1 ou $2 ou $3}’ pour récupérer tel ou tel champ d’un fichier CSV.
Notez que le « $i » ne changera rien par rapport au $i.

Solution : redéfinir l’IFS.
D’abord le on le sauvegarde car c’est pas simple de taper son contenu, la preuve, affichez cette variable, elle semble vide :

user@srv:/tmp$ echo $IFS

user@srv:/tmp$

La difficulté de syntaxe mentionnée au début de l’article est de faire gober le « \n » sans interprétation par le shell, voici comment faire dans le cas qui nous intéresse : (le reste est expliqué sur wikipedia)

user@srv: /tmp$ OLDIFS=$IFS
user@srv:/tmp$ IFS=$'\n'
user@srv:/tmp$ for i in `cat liste`; do echo "$i"; done
champ1.1;champ1.2;champ1.3
champ2.1;champ avec espace;champ2.3
champ   avec_tabulation;champ3.2;champ pour finir

A noter que votre variable IFS restera en l’état pendant toute votre session (ie, votre shell ouvert). Donc n’oubliez pas de rétablir IFS=$OLDIFS.
Si vous faites ceci dans un script, c’est alors propre à l’exécution du script.

Enfin, pour les bidouilleux, ça peut permettra de faire du découpage assez bizzarement :

user@srv:/tmp$ IFS=a
user@srv:/tmp$ for i in `cat liste`; do echo "$i"; done
ch
mp1.1;ch
mp1.2;ch
mp1.3
ch
mp2.1;ch
mp
vec esp
ce;ch
mp2.3
ch
mp
vec_t
bul
tion;ch
mp3.2;ch
mp pour finir

16 réflexions au sujet de « IFS : séparateurs & scripts BASH »

  1. AP

    Les « extrémistes pinguistes » qui pronent des noms de fichier en ASCII non accentué et sans espaces sont des cons et je propose qu’on les dissolve tous sur le champ dans l’acide chlorhydrique. 🙂

    Les systèmes de fichiers modernes sont faits pour ça, bordel !

    Donc… Toujours prévoir, dans ses scripts, que les fichiers peuvent contenir des espaces. La solution est juste d’ajouter des guillemets :

    Avant :
    mv $fichier $nouveaufichier

    Après :
    mv « $fichier » « $nouveaufichier »

    Voilàààààà. Faciles et pas chères, les bonnes habitudes.

    De même, pour ligne un fichier une ligne à la fois :

    cat « $fichier »|while read ligne
    do
    echo « La ligne, à l’envers : »
    echo « $ligne »|rev
    done

    Répondre
  2. Thesa

    Sinon, pour éviter de changer l’IFS (dont je n’avais jamais entendu parler, est-ce un bashisme ou du shell de Bourne POSIX ?), un classique
    cat liste | while read ligne ; do echo $ligne ; done
    marche très bien 😉

    (enfin, très bien… le pipe implique un sous-shell, donc pas de conservation des variables modifiées à l’intérieur du while. Faut quand même le savoir 🙂 )

    Répondre
  3. kim

    c’est peu connu notamment parce que c’est très dangereux à manipuler… Genre tu modifies $IFS, tu oublies que tu le modifies, ou genre tu ajoutes une nouvelle fonction dans ton script, et paf, tout merde, et là, va retrouver pourquoi…

    -> pour résoudre ton probleme, tu as :
    car fichier | while read line; do
    echo $line
    done

    ça marche et ça évite de bidouiller des variables interne BASH qui peuvent avoir un impact sur toutes les boucles de ton shell.

    Répondre
  4. p4bl0

    Je suis étonné que personne ne propose le plus évident ^^
    while read file; do
    # utiliser $file
    done > list » va juste ajouter une ligne dans le fichier list (et le créer si il n’existe pas), je suppose que tu voulais écrire « cat ».
    Et aussi je pense que tu oublies un « IFS= » entre user@srv: /tmp$ « OLDIFS=$IFS » et « user@srv:/tmp$ for i in `cat liste`; do echo “$i”; done » non ? Sinon la variable n’a pas changée, à moins que OLDIFS ne soit une variable spéciale aussi qui enlève des séparateurs les caractères qu’elle contient ? Mais je ne crois pas.

    Répondre
  5. michauko Auteur de l’article

    Mea culpa
    Mon temps de modération a été plutôt long. Ca m’apprendra à poster juste avant de rentrer à la maison 🙂

    Désolé

    OUI pour la remarque concernant le while read ligne
    Ca m’avait embêté une fois, je ne sais plus pourquoi, peut-être 2 boucles imbriquées. Bref, j’avais trouvé le coup du IFS assez pratique.
    Risqué peut-être, mais c’est en connaissant les risques qu’on agit le mieux.

    OLDIFS = un nom bidon pour stocker l’ancienne valeur plutôt que $’ \t\n’ qui n’est pas très pratique

    Répondre
  6. Anthony

    Contrairement à ce qui est marqué, IFS est un « field separator » pas un « line ». Donc, effectivement, on peut le changer.

    Répondre
  7. p4bl0

    @kim mon commentaire n’était pas complet, un truc foireux. Bon mais ça devrait marcher cette fois ci :
    ==========================
    Je suis étonné que personne ne propose le plus évident ^^
    while read file; do
    # utiliser $file
    done > list » va juste ajouter une ligne dans le fichier list (et le créer si il n’existe pas), je suppose que tu voulais écrire « cat ».
    Et aussi je pense que tu oublies un « IFS= » entre « user@srv: /tmp$ OLDIFS=$IFS » et « user@srv:/tmp$ for i in `cat liste`; do echo « $i »; done » non ? Sinon la variable n’a pas changée, à moins que OLDIFS ne soit une variable spéciale aussi qui enlève des séparateurs les caractères qu’elle contient ? Mais je ne crois pas.
    ==========================

    Voilà ce que j’ajouterai depuis :
    ==========================
    depuis j’ai testé et effectivement OLDIFS n’est pas une variable spéciale et il faut bien changer IFS en faisant genre IFS= »\n » pour arriver au résultat que tu montre, mais changer IFS n’est pas très conseillé, et ce n’est pas portable car tout les OS ne représente pas les retour à la ligne de la même manière.
    ==========================

    🙂

    Répondre
  8. p4bl0

    Ben non ça ne marche toujours pas… Il manque 1 paragraphes et demie de mon commentaires à chaque fois… C’est pas terrible cette gestion des comments de WordPress…

    Je vais le faire en deux comments pour que ça passe…

    ==============================
    Je suis étonné que personne ne propose le plus évident ^^
    while read file; do
    # utiliser $file
    done < « $fichier »
    C’est plus simple d’utiliser une redirection, et c’est plus propre que de piper le cat dans le while puisque cette dernière technique puisque le pipe implique que la boucle soit lancer dans un processus fils, et n’a donc pas accès aux variables du reste du script en écriture, à moins de les rendre global, ce qui est encore moins propre.

    1/2

    Répondre
  9. p4bl0

    2/2

    Sinon, le « echo >> list » va juste ajouter une ligne dans le fichier list (et le créer si il n’existe pas), je suppose que tu voulais écrire « cat ».
    Et aussi je pense que tu oublies un « IFS= » entre « user@srv: /tmp$ OLDIFS=$IFS » et « user@srv:/tmp$ for i in `cat liste`; do echo « $i »; done » non ? Sinon la variable n’a pas changée, à moins que OLDIFS ne soit une variable spéciale aussi qui enlève des séparateurs les caractères qu’elle contient ? Mais je ne crois pas.
    =====================================

    Voilà ce que j’ajouterai depuis :
    =====================================
    depuis j’ai testé et effectivement OLDIFS n’est pas une variable spéciale et il faut bien changer IFS en faisant genre IFS= »\n » pour arriver au résultat que tu montre, mais changer IFS n’est pas très conseillé, et ce n’est pas portable car tout les OS ne représente pas les retour à la ligne de la même manière.
    =====================================

    Répondre
  10. kim

    bah disons que dès que c’est pas posix, si on peut éviter, c’est mieux 🙂
    pour avoir travaillé sur du zsh en environnement hétérogène, je peux vous dire qu’il faut vraiment être rigoureux et essayer d’éviter les basheries spécifiques et un peu « chelous », c’est pas portable, et ça ajoute des erreurs, on a beau se dire « bah c’est tordu alors du coup on fera plus attention », le mec qui repasse derrière toi, ben il verra pas le truc… C’est pas maintenable quoi 😉

    Répondre
  11. lefoudupc

    Moi j’ai un truc bizarre.
    Lorsque je met IFS=”\n”, il me coupe bien à chaque saut de ligne, mais il me coupe aussi tout les mot ou j’ai des « n ».
    J’ai essaigné sur autre machine, est la ça fonctionne bien.
    Bizarre, si qu’elle q’un à une idée je suis preneur

    Répondre
  12. ap0

    merci, merci, merci, merci !
    2h que je galËre ‡ grand coup de sed/find/ls pour faire ce que je veux.

    merci !

    Répondre
  13. M'sieur Toph'

    Je suis d’accord que le « while read line » est ce qu’il y a de plus beau en terme de dev.

    Malheureusement, j’ai du opter pour le for..in assez souvent car le comportement du while est assez instable dès lors qu’on fait du rsh dans la boucle. Du moins en ksh.

    Pour moi, en l’occurence, le while sortait après la première boucle (sans raison apparente) au lieu de traiter la globalité des lignes … En désespoir de cause, j’ai testé en remplaçant le while par un for…in et tout est rentré dans l’ordre.
    J’en ai parlé à quelques développeurs autour de moi, ils m’ont dit que ça leur était déjà arrivé et arrivent à la même conclusion : while read line, c’est bien … mais pas si tu fais du ssh ou du rsh (inclus scp et rcp et toutes les commandes du genre)

    Ton exemple ne marche pas sous ksh.
    Pour configurer un IFS comme séparateur de ligne, il faut faire : IFS=${IFS#??}
    (Me demandez pas pourquoi, j’ai trouvé ça sur un forum et ça a l’air de marcher).

    En espérant avoir contribué …

    Répondre
  14. Fravadona

    Pour les partisants du « il faut savoir gerer tout », vous devez savoir que sous Linux tous les caracteres sont autiroses sauf dans un nom de fichier. Par exemple, un petit saut de ligne dans un nom et hop, vous pouvez jeter vos scripts a la poubelle.

    Bon, il y a des maniere de gerer ca, mais generalement elles impliquent des find -print0 | xargs -0 et d’encoder les noms de fichier en quelque chose sans caractere special (je vote pour base64 : a-z A-Z 0-9 +/=)

    Vous etes prevenus 😉

    Répondre
  15. patrick L

    bonsoir

    j’ai un probleme sur un script de sauvegarde avec la commande tar –never mtime= »2016-09-15 10:15:22″

    je fais stat –printf « %y#%n » fichier
    et ensuite je mets la date dans une variable $ddate

    ensuite

    tar -zcvf archive.tar.gz –newer-mtime= »$ddate » et je me retrouve avec une ligne de commande sous la forme

    tar -zcvf archive.tar;gz ‘-newer-mtime= »2016-09-15’ ’10:15:22″‘

    comment je peux générer une commande tar pour envoyer au systeme sous la forme

    tar -zcvf archive.tar;gz -newer-mtime= »2016-09-15 10:15:22″‘

    Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *