Est-ce une approche sensée de «sauvegarder» la variable $ IFS?


19

J'hésite toujours beaucoup à jouer $IFSparce que ça fait trembler un monde.

Mais souvent, cela rend le chargement des chaînes dans un tableau bash agréable et concis, et pour les scripts bash, la concision est difficile à trouver.

Je pense donc que cela pourrait être mieux que rien si j'essaie de «sauvegarder» le contenu de départ d' $IFSune autre variable, puis de le restaurer immédiatement après avoir fini d'utiliser $IFSquelque chose.

Est-ce pratique? Ou est-il essentiellement inutile et je devrais simplement IFSrevenir directement à ce qu'il doit être pour ses utilisations ultérieures?


Pourquoi ne serait-ce pas pratique?
Bratchley

Parce que désarmer IFS ferait l'affaire.
llua

1
Pour ceux qui disent que la désactivation de l'IFS fonctionnera correctement, gardez à l'esprit que c'est situationnel: stackoverflow.com/questions/39545837/… . D'après mon expérience, il est préférable de définir IFS manuellement par défaut pour votre interpréteur de shell, à savoir $' \t\n'si vous utilisez bash. unset $IFSne le restaure pas toujours à ce que vous attendez d'être la valeur par défaut.
Darrel Holt

Réponses:


9

Vous pouvez enregistrer et affecter à IFS selon vos besoins. Il n'y a rien de mal à le faire. Il n'est pas rare d'enregistrer sa valeur pour la restauration après une modification temporaire et rapide, comme votre exemple d'affectation de tableau.

Comme @llua le mentionne dans son commentaire à votre question, la simple suppression de IFS restaurera le comportement par défaut, ce qui équivaut à attribuer un espace-tab-newline.

Cela vaut la peine de considérer comment il peut être plus problématique de ne pas explicitement définir / désinstaller IFS que de le faire.

Depuis l'édition POSIX 2013, 2.5.3 Variables du shell :

Les implémentations peuvent ignorer la valeur d'IFS dans l'environnement, ou l'absence d'IFS de l'environnement, au moment où le shell est appelé, auquel cas le shell doit définir IFS sur <space> <tab> <newline> lors de son appel. .

Un shell invoqué compatible POSIX peut ou non hériter IFS de son environnement. De cela suit:

  • Un script portable ne peut pas hériter de manière fiable d'IFS via l'environnement.
  • Un script qui a l'intention d'utiliser uniquement le comportement de fractionnement par défaut (ou de jointure, dans le cas de "$*"), mais qui peut s'exécuter sous un shell qui initialise IFS à partir de l'environnement, doit explicitement définir / désactiver IFS pour se défendre contre les intrusions environnementales.

NB Il est important de comprendre que pour cette discussion le mot "invoqué" a une signification particulière. Un shell n'est invoqué que lorsqu'il est explicitement appelé en utilisant son nom (y compris un #!/path/to/shellshebang). Un sous-shell - tel que pourrait être créé par $(...)ou cmd1 || cmd2 &- n'est pas un shell invoqué, et son IFS (ainsi que la plupart de son environnement d'exécution) est identique à celui de son parent. Un shell invoqué définit la valeur de $son pid, tandis que les sous-shell en héritent.


Ce n'est pas simplement une disquisition pédante; il existe une réelle divergence dans ce domaine. Voici un bref script qui teste le scénario à l'aide de plusieurs shells différents. Il exporte un IFS modifié (défini sur :) vers un shell appelé qui imprime ensuite son IFS par défaut.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS n'est généralement pas marqué pour l'exportation, mais s'il l'était, notez comment bash, ksh93 et ​​mksh ignorent leur environnement IFS=:, tandis que dash et busybox l'honorent.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Quelques informations de version:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Même si bash, ksh93 et ​​mksh n'initialisent pas IFS à partir de l'environnement, ils réexportent leur IFS modifié.

Si, pour quelque raison que ce soit, vous devez transmettre IFS de manière portable via l'environnement, vous ne pouvez pas le faire en utilisant IFS lui-même; vous devrez affecter la valeur à une variable différente et marquer cette variable pour l'exportation. Les enfants devront ensuite attribuer explicitement cette valeur à leur IFS.


Je vois, donc si je peux paraphraser, il est sans doute plus portable de spécifier explicitement la IFSvaleur dans la plupart des situations où elle doit être utilisée, et donc il n'est souvent pas terriblement productif d'essayer même de "préserver" sa valeur d'origine.
Steven Lu

1
Le problème primordial est que si votre script utilise IFS, il doit explicitement définir / désactiver IFS pour garantir que sa valeur correspond à ce que vous voulez qu'il soit. En règle générale, le comportement de votre script dépend d'IFS s'il existe des extensions de paramètres non cotées, des substitutions de commandes non cotées, des extensions arithmétiques non cotées, reads ou des références entre guillemets $*. Cette liste est juste au dessus de ma tête, donc elle peut ne pas être complète (surtout si l'on considère les extensions POSIX des shells modernes).
Barefoot IO

10

En règle générale, il est recommandé de rétablir les conditions par défaut.

Cependant, dans ce cas, pas tellement.

Pourquoi?:

En outre, le stockage de la valeur IFS a un problème.
Si l'IFS d'origine n'a pas été défini, le code IFS="$OldIFS"définira IFS sur "", et non pas le désactiver.

Pour conserver réellement la valeur de IFS (même si non définie), utilisez ceci:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.

IFS ne peut pas vraiment être désarmé. Si vous le désactivez, le shell le rétablit à sa valeur par défaut. Vous n'avez donc pas vraiment besoin de vérifier cela lors de l'enregistrement.
filbranden

Sachez que dans bash, unset IFSne parvient pas à désactiver IFS s'il a été déclaré local dans un contexte parent (contexte de fonction) et non dans le contexte actuel.
Stéphane Chazelas

5

Vous avez raison d'hésiter à secouer un monde. N'ayez crainte, il est possible d'écrire du code de travail propre sans jamais modifier le global réel IFS, ou faire une danse de sauvegarde / restauration lourde et sujette aux erreurs.

Vous pouvez:

  • définissez IFS pour une seule invocation:

    IFS=value command_or_function

    ou

  • définir IFS dans un sous-shell:

    (IFS=value; statement)
    $(IFS=value; statement)

Exemples

  • Pour obtenir une chaîne séparée par des virgules à partir d'un tableau:

    str="$(IFS=, ; echo "${array[*]-}")"

    Remarque: Le -est uniquement destiné à protéger un tableau vide contre set -uen fournissant une valeur par défaut lorsqu'il n'est pas défini (cette valeur étant la chaîne vide dans ce cas) .

    La IFSmodification n'est applicable qu'à l'intérieur du sous-shell engendré par la $() substitution de commande . C'est parce que les sous-coquilles ont des copies des variables du shell appelant et peuvent donc lire leurs valeurs, mais toutes les modifications effectuées par la sous-coquille n'affectent que la copie de la sous-coquille et non la variable du parent.

    Vous pourriez également penser: pourquoi ne pas sauter le sous-shell et faire juste ceci:

    IFS=, str="${array[*]-}"  # Don't do this!

    Il n'y a pas d'invocation de commande ici, et cette ligne est plutôt interprétée comme deux affectations de variables ultérieures indépendantes, comme si elle l'était:

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Enfin, expliquons pourquoi cette variante ne fonctionnera pas:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    La echocommande sera en effet appelée avec sa IFSvariable définie sur ,, mais echone s'en soucie ni ne l'utilise IFS. La magie de l'extension "${array[*]}"à une chaîne est effectuée par le (sous-) shell lui-même avant echomême d'être invoquée.

  • Pour lire un fichier entier (qui ne contient pas d' NULLoctets) dans une seule variable nommée VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Remarque: IFS=est identique à IFS=""et IFS='', qui définissent tous IFS sur la chaîne vide, ce qui est très différent de unset IFS: si IFSn'est pas défini, le comportement de toutes les fonctionnalités bash qui utilisent en interne IFSest exactement le même que s'il IFSavait la valeur par défaut de $' \t\n'.

    La définition IFSde la chaîne vide garantit que les espaces de début et de fin sont préservés.

    Le -d ''ou -d ""indique à read d'arrêter uniquement son appel en cours sur un NULLoctet, au lieu du retour à la ligne habituel.

  • Pour diviser le $PATHlong de ses :délimiteurs:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Cet exemple est purement illustratif. Dans le cas général où vous divisez le long d'un délimiteur, il est possible que les champs individuels contiennent (une version d'échappement de) ce délimiteur. Pensez à essayer de lire une ligne d'un .csvfichier dont les colonnes peuvent elles-mêmes contenir des virgules (échappées ou citées d'une manière ou d'une autre). L'extrait ci-dessus ne fonctionnera pas comme prévu dans de tels cas.

    Cela dit, il est peu probable que vous rencontriez de tels :chemins contenant $PATH. Bien que les chemins d'accès UNIX / Linux soient autorisés à contenir un :, il semble que bash ne serait pas en mesure de gérer de tels chemins de toute façon si vous essayez de les ajouter à votre $PATHet d'y stocker des fichiers exécutables, car il n'y a pas de code pour analyser les deux-points échappés / cités. : code source de bash 4.4 .

    Enfin, notez que l'extrait ajoute une nouvelle ligne de fin au dernier élément du tableau résultant (comme l'a appelé @ StéphaneChazelas dans les commentaires maintenant supprimés), et que si l'entrée est la chaîne vide, la sortie sera un élément unique tableau, où l'élément sera constitué d'un saut de ligne ( $'\n').

Motivation

L' old_IFS="${IFS}"; command; IFS="${old_IFS}"approche de base qui touche le global IFSfonctionnera comme prévu pour le plus simple des scripts. Cependant, dès que vous ajoutez une complexité, elle peut facilement se séparer et provoquer des problèmes subtils:

  • Si commandest une fonction bash qui modifie également le global IFS(soit directement soit, à l'abri des regards, à l'intérieur d'une autre fonction qu'elle appelle), et ce faisant, utilise par erreur la même old_IFSvariable globale pour effectuer la sauvegarde / restauration, vous obtenez un bogue.
  • Comme indiqué dans ce commentaire de @Gilles , si l'état d'origine de IFSn'était pas défini, la sauvegarde et la restauration naïves ne fonctionneront pas, et entraîneront même des échecs purs et simples si l' option shell couramment (mal) utilisée set -u(aka set -o nounset) est en vigueur.
  • Il est possible pour certains codes shell de s'exécuter de manière asynchrone avec le flux d'exécution principal, comme avec les gestionnaires de signaux (voir help trap). Si ce code modifie également le global IFSou suppose qu'il a une valeur particulière, vous pouvez obtenir des bogues subtils.

Vous pouvez concevoir une séquence de sauvegarde / restauration plus robuste (comme celle proposée dans cette autre réponse pour éviter certains ou tous ces problèmes. Cependant, vous devrez répéter ce morceau de code passe-partout bruyant partout où vous avez temporairement besoin d'une personnalisation IFS. réduit la lisibilité et la maintenabilité du code.

Considérations supplémentaires pour les scripts de type bibliothèque

IFSest particulièrement une préoccupation pour les auteurs de bibliothèques de fonctions shell qui doivent s'assurer que leur code fonctionne de manière robuste quel que soit l'état global ( IFS, les options de shell, ...) imposé par leurs appelants, et aussi sans perturber cet état (les invocateurs peuvent s'appuyer sur dessus pour rester toujours statique).

Lorsque vous écrivez du code de bibliothèque, vous ne pouvez pas compter sur IFSune valeur particulière (pas même celle par défaut) ou même être défini du tout. Au lieu de cela, vous devez définir explicitement IFSpour tout extrait dont le comportement dépend IFS.

Si IFSest explicitement défini sur la valeur nécessaire (même si celle-ci se trouve être celle par défaut) dans chaque ligne de code où la valeur importe en utilisant l'un des deux mécanismes décrits dans cette réponse qui convient pour localiser l'effet, le code est à la fois indépendante de l’état mondial et évite de l’encombrer complètement. Cette approche a l'avantage supplémentaire de la rendre très explicite pour une personne lisant le script qui IFScompte précisément pour cette commande / expansion à un coût textuel minimum (par rapport à même la sauvegarde / restauration la plus basique).

De quel code est affecté de IFStoute façon?

Heureusement, il n'y a pas beaucoup de scénarios où cela IFScompte (en supposant que vous citez toujours vos extensions ):

  • "$*"et "${array[*]}"extensions
  • invocations de la readcible intégrée ciblant plusieurs variables ( read VAR1 VAR2 VAR3) ou une variable de tableau ( read -a ARRAY_VAR_NAME)
  • invocations de readciblage d'une variable unique en ce qui concerne les espaces blancs de début / fin ou les espaces non blancs apparaissant dans IFS.
  • séparation de mots (comme pour les extensions non citées, que vous voudrez peut-être éviter comme la peste )
  • d'autres scénarios moins courants (Voir: IFS @ Wiki de Greg )

Je ne peux pas dire que je comprends le Pour diviser $ PATH le long de ses: délimiteurs en supposant qu'aucun des composants ne contient une phrase : eux-mêmes . Comment les composants pourraient-ils contenir :quand :est le délimiteur?
Stéphane Chazelas

@ StéphaneChazelas Eh bien, :c'est un caractère valide à utiliser dans un nom de fichier sur la plupart des systèmes de fichiers UNIX / Linux, il est donc tout à fait possible d'avoir un répertoire avec un nom contenant :. Peut-être que certains shells ont une disposition pour s'échapper :dans PATH en utilisant quelque chose comme \:, et vous verrez alors apparaître des colonnes qui ne sont pas de véritables délimiteurs (Il semble que bash n'autorise pas un tel échappement. La fonction de bas niveau utilisée lors de l'itération sur $PATHsimplement recherche :dans une chaîne C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls

J'ai révisé la réponse pour, espérons-le $PATH, :clarifier l' exemple de fractionnement .
sls

1
Bienvenue chez SO! Merci pour cette réponse si approfondie :)
Steven Lu

1

Est-ce pratique? Ou est-il essentiellement inutile et je devrais simplement remettre directement IFS à ce qu'il doit être pour ses utilisations ultérieures?

Pourquoi risquer une faute de frappe sur IFS $' \t\n'alors que tout ce que vous avez à faire est

OIFS=$IFS
do_your_thing
IFS=$OIFS

Alternativement, vous pouvez appeler un sous-shell si vous n'avez pas besoin de variables définies / modifiées dans:

( IFS=:; do_your_thing; )

Ceci est dangereux car cela ne fonctionne pas s'il IFSn'était pas réglé au départ.
Gilles 'SO- arrête d'être méchant'
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.