Paramétrer IFS pour une seule déclaration

42

Je sais qu'une valeur IFS personnalisée peut être définie pour la portée d'une seule commande / intégrée. Est-il possible de définir une valeur IFS personnalisée pour une seule instruction? Apparemment non, puisque la valeur IFS globale est affectée lorsque cela est tenté

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001
iruvar
la source

Réponses:

39

Dans certains coquillages (y compris bash):

IFS=: command eval 'p=($PATH)'

(avec bash, vous pouvez omettre l’ commandémulation si pas dans sh / POSIX). Mais gardez à l'esprit que lorsque vous utilisez des variables non entre guillemets, vous en avez généralement besoin set -f, et il n'y a pas de portée locale pour cela dans la plupart des shells.

Avec zsh, vous pouvez faire:

(){ local IFS=:; p=($=PATH); }

$=PATHconsiste à forcer le fractionnement des mots, ce qui n'est pas fait par défaut dans zsh(la suppression lors du développement de variable n'est pas effectuée non plus, donc vous n'avez pas besoin, set -fsauf dans l'émulation sh).

(){...}(ou function {...}) sont appelées fonctions anonymes et sont généralement utilisées pour définir une étendue locale. avec d'autres shells prenant en charge l'étendue locale dans les fonctions, vous pouvez faire quelque chose de similaire avec:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

Pour implémenter une portée locale pour les variables et les options dans les shells POSIX, vous pouvez également utiliser les fonctions fournies à l' adresse https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . Ensuite, vous pouvez l'utiliser comme:

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'

(Soit dit en passant, il est incorrect de diviser de $PATHcette façon ci-dessus, sauf zshque, comme dans les autres shells, IFS est un délimiteur de champ, et non un séparateur de champ).

IFS=$'\n' a=($str)

Est-ce juste deux missions, l'une après l'autre, tout comme a=1 b=2.

Une note d'explication sur var=value cmd:

Dans:

var=value cmd arg

Le shell s'exécute /path/to/cmddans un nouveau processus et passe cmdet argdans argv[]et var=valuedans envp[]. Ce n'est pas vraiment une affectation de variable, mais davantage de variables d'environnement transmises à la commande exécutée . Dans le shell Bourne ou Korn, avec set -k, vous pouvez même l'écrire cmd var=value arg.

Cela ne s'applique pas aux fonctions intégrées ou aux fonctions qui ne sont pas exécutées . Dans le shell Bourne, dans var=value some-builtin, varfinit par être défini, comme avec un var=valueseul. Cela signifie par exemple que le comportement de var=value echo foo(ce qui n’est pas utile) varie selon qu’il echosoit intégré ou non.

POSIX et / ou ont kshmodifié ce comportement en ce sens que le comportement Bourne ne se produit que pour une catégorie de fonctions intégrées appelées fonctions spéciales . evalest un spécial intégré, readn'est pas. Pour les commandes intégrées non spéciales, les var=value builtinensembles varne concernent que l'exécution de la commande intégrée, ce qui lui permet de se comporter de manière similaire à l'exécution d'une commande externe.

La commandcommande peut être utilisée pour supprimer l' attribut spécial de ces fonctions spéciales . Ce qui est bien négligé Posix que pour les evalet .builtins, cela voudrait dire que des obus aurait à mettre en œuvre une pile variable (même si elle ne précise pas les localou la typesetportée de limitation des commandes), parce que vous pouvez faire:

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

Ou même:

a=1 command eval myfunction

avec myfunctionêtre une fonction à l' aide ou la mise $aet potentiellement appeler command eval.

C'était vraiment un oubli parce que ksh(la spécification est principalement basée sur) ne l'a pas implémentée (et AT & T kshet zshne le fait toujours pas), mais de nos jours, à l'exception de ces deux là, la plupart des shells l'implémentent. Le comportement varie selon les coquillages, par exemple:

a=0; a=1 command eval a=2; echo "$a"

bien que. L'utilisation localsur les shells qui le supportent constitue un moyen plus fiable d'implémenter la portée locale.

Stéphane Chazelas
la source
Bizarrement, ne IFS=: command eval …définit IFSque pour la durée de eval, comme prescrit par POSIX, en dash, pdksh et bash, mais pas en ksh 93u. Il est inhabituel de voir que ksh est l'intrus-non-conforme-un-out.
Gilles, arrête de faire le mal
12

Sauvegarde et restauration standard extraites de "The Unix Programming Environment" de Kernighan et Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
msw
la source
2
merci et +1. Oui, je suis conscient de cette option, mais j'aimerais savoir s'il existe une option "plus propre" si vous voyez ce que je veux dire
iruvar
Vous pourriez le placer sur une ligne avec des points-virgules, mais je ne pense pas que ce soit plus propre. Cela pourrait être bien si tout ce que vous vouliez exprimer avait un support syntaxique spécial, mais alors nous devrions probablement apprendre la charpenterie ou la sumptine au lieu de coder;)
msw
10
Cela ne parvient pas à restaurer $IFScorrectement s'il était précédemment non défini.
Stéphane Chazelas
2
Si c'est non défini, Bash le traite comme $'\t\n'' ', comme expliqué ici: wiki.bash-hackers.org/syntax/expansion/…
davide
2
@davide, ce serait $' \t\n'. L'espace doit être le premier utilisé "$*". Notez que c'est la même chose dans tous les obus Bourne-like.
Stéphane Chazelas
8

Placez votre script dans une fonction et appelez-la en lui transmettant les arguments de la ligne de commande. Comme IFS est défini localement, ses modifications n’affectent pas l’IFS global.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
méthode d'aide
la source
6

Pour cette commande:

IFS=$'\n' a=($str)

Il existe une solution alternative: donner à la première affectation ( IFS=$'\n') une commande à exécuter (une fonction):

$ split(){ a=( $str ); }
$ IFS=$'\n' split

Cela mettra IFS dans l’environnement à appeler le fractionnement, mais ne sera pas conservé dans l’environnement actuel.

Cela évite également l’utilisation toujours risquée d’eval.


la source
Dans ksh93 et mksh, et bash et zsh en mode POSIX, que les feuilles encore $IFSfixées à la $'\n'suite , au besoin par POSIX.
Stéphane Chazelas Le
4

La réponse proposée par @helpermethod est certainement une approche intéressante. Mais c’est aussi un piège, car dans BASH, la portée de la variable locale s’étend de l’appelant à la fonction appelée. Par conséquent, si vous définissez IFS dans main (), cette valeur persistera pour les fonctions appelées depuis main (). Voici un exemple:

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

Et la sortie ...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

Si IFS déclaré dans main () n'était pas encore dans la portée de func (), le tableau n'aurait pas été correctement analysé dans func () B. Décommentez la première ligne de func () et vous obtenez ce résultat:

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

C'est ce que vous devriez obtenir si IFS était hors de portée.

Une solution bien meilleure, à mon humble avis, consiste à renoncer à changer ou à s’appuyer sur IFS au niveau mondial / local. Créez plutôt un nouveau shell et jouez avec IFS. Par exemple, si vous appelez func () dans main () comme suit, transmettez le tableau sous forme de chaîne avec un séparateur de champ de barre oblique inversée:

func $(IFS='\'; echo "${m_args[*]}")

... ce changement dans IFS ne sera pas reflété dans func (). Le tableau sera passé sous forme de chaîne:

ick\blick\flick

... mais à l'intérieur de func (), l'IFS sera toujours "/" (comme défini dans main ()), sauf si modifié localement dans func ().

Pour plus d'informations sur l'isolation des modifications apportées à IFS, consultez les liens suivants:

Comment convertir une variable de tableau bash en une chaîne délimitée par des nouvelles lignes?

Chaîne Bash à dresser avec IFS

Trucs et astuces pour la programmation de scripts shell en général - Voir "REMARQUE concernant l'utilisation de sous-coques ..."

Markeissler
la source
intéressant en effet ...
iruvar
"Bash string to array with IFS" IFS=$'\n' declare -a astr=(...)parfait merci!
Aquarius Power
1

Cet extrait de la question:

IFS=$'\n' a=($str)

est interprété comme deux affectations de variable globales distinctes évaluées de gauche à droite et équivaut à:

IFS=$'\n'; a=($str)

ou

IFS=$'\n'
a=($str)

Cela explique à la fois pourquoi le global a IFSété modifié et pourquoi le découpage des mots $stren éléments de tableau a été effectué à l'aide de la nouvelle valeur de IFS.

Vous pourriez être tenté d'utiliser un sous-shell pour limiter l'effet de la IFSmodification comme ceci:

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"

mais vous remarquerez rapidement que la modification de aest également limitée au sous-shell:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

Ensuite, vous seriez tenté de sauvegarder / restaurer IFS en utilisant la solution de cette réponse précédente par @msw ou d’essayer d’utiliser local IFSune fonction interne suggérée par @helpermethod. Mais très vite, vous remarquerez que vous avez toutes sortes de problèmes, en particulier si vous êtes un auteur de bibliothèque qui doit faire preuve de rigueur face aux appels de scripts incorrects:

  • Et si IFSétait initialement non défini?
  • Et si nous courons avec set -u(aka set -o nounset)?
  • Que faire si a IFSété faite en lecture seule via declare -r IFS?
  • Que se passe-t-il si j'ai besoin du mécanisme de sauvegarde / restauration pour fonctionner avec une récursivité ou une exécution asynchrone (comme un trapgestionnaire`)?

S'il vous plaît ne pas enregistrer / restaurer IFS. Au lieu de cela, tenez-vous aux modifications temporaires:

  • Pour limiter la modification de variable à une seule commande, à une commande intégrée ou à un appel de fonction, utilisez IFS="value" command.

    • Pour lire plusieurs variables en séparant un caractère spécifique ( :utilisé ci-dessous à titre d'exemple), utilisez:

      IFS=":" read -r var1 var2 <<< "$str"
    • Pour lire dans un tableau, utilisez (faites ceci à la place de array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Limitez les effets de la modification de la variable à un sous-shell.

    • Pour sortir les éléments d'un tableau séparés par une virgule:

      (IFS=","; echo "${array[*]}")
    • Pour capturer cela dans une chaîne:

      csv="$(IFS=","; echo "${array[*]}")"
sls
la source
0

La solution la plus simple consiste à prendre une copie de l’original $IFS, comme par exemple la réponse de msw. Cependant, cette solution ne fait pas la distinction entre un non défini IFSet un IFSensemble égal à la chaîne vide, ce qui est important pour de nombreuses applications. Voici une solution plus générale qui capture cette distinction:

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
jmd_dk
la source