web-dev-qa-db-fra.com

Moyen compatible POSIX d'étendre les variables à une fonction dans un script shell

Existe-t-il un moyen conforme à POSIX pour limiter la portée d'une variable à la fonction dans laquelle elle est déclarée? c'est à dire.:

Testing()
{
    TEST="testing"
}

Testing
echo "Test is: $TEST"

devrait afficher "Test is:". J'ai lu des informations sur les mots clés declare, local et typeset, mais il ne semble pas que ce soient des intégrés POSIX obligatoires.

44
John

Cela se fait normalement avec le mot clé local, qui, comme vous semblez le savoir, n'est pas défini par POSIX. Voici une information discussion sur l'ajout de "local" à POSIX .

Cependant, même le Shell le plus primitif conforme à POSIX que je connaisse est utilisé par certaines distributions GNU/Linux comme /bin/sh par défaut, dash (Debian Almquist Shell), le supporte. FreeBSD et NetBSD utilisent ash, le shell Almquist d'origine, qui le prend également en charge. OpenBSD utilise une implémentation ksh pour /bin/sh qui le supporte également. Donc, à moins que vous ne souhaitiez prendre en charge des systèmes non GNU non BSD tels que Solaris ou ceux utilisant ksh standard, etc., vous pouvez vous en sortir en utilisant local. (Pourrait vouloir mettre un commentaire juste au début du script, en dessous de la ligne Shebang, notant qu'il ne s'agit pas strictement d'un script sh POSIX. Juste pour ne pas être mauvais.) Cela dit, vous voudrez peut-être vérifier les respectives les pages de manuel de toutes ces implémentations sh qui prennent en charge local, car elles peuvent avoir de subtiles différences dans la façon dont elles fonctionnent exactement. Ou n'utilisez simplement pas local:

Si vous voulez vraiment vous conformer pleinement à POSIX, ou ne voulez pas jouer avec les problèmes possibles, et donc ne pas utiliser local, alors vous avez quelques options. La réponse donnée par Lars Brinkhoff est solide, vous pouvez simplement envelopper la fonction dans un sous-shell. Cela pourrait cependant avoir d'autres effets indésirables. Soit dit en passant, la grammaire Shell (par POSIX) permet ce qui suit:

my_function()
(
  # Already in a sub-Shell here,
  # I'm using ( and ) for the function's body and not { and }.
)

Bien que vous puissiez éviter cela pour être super portable, certains vieux shells Bourne peuvent même ne pas être conformes à POSIX. Je voulais juste mentionner que POSIX le permet.

Une autre option serait de unset variables à la fin de vos corps de fonction, mais c'est pas va restaurer l'ancienne valeur bien sûr donc ce n'est pas vraiment ce que vous voulez, je suppose , cela empêchera simplement la valeur en fonction de la variable de fuir à l'extérieur. Pas très utile je suppose.

Une dernière et folle idée à laquelle je peux penser est d'implémenter local vous-même. Le Shell a eval, qui, bien que maléfique, laisse place à des possibilités insensées. Ce qui suit implémente essentiellement la portée dynamique d'un ancien Lisps, j'utiliserai le mot clé let au lieu de local pour d'autres points de refroidissement, bien que vous deviez utiliser le soi-disant unlet à la fin:

# If you want you can add some error-checking and what-not to this.  At present,
# wrong usage (e.g. passing a string with whitespace in it to `let', not
# balancing `let' and `unlet' calls for a variable, etc.) will probably yield
# very very confusing error messages or breakage.  It's also very dirty code, I
# just wrote it down pretty much at one go.  Could clean up.

let()
{
    dynvar_name=$1;
    dynvar_value=$2;

    dynvar_count_var=${dynvar_name}_dynvar_count
    if [ "$(eval echo $dynvar_count_var)" ]
    then
        eval $dynvar_count_var='$(( $'$dynvar_count_var' + 1 ))'
    else
        eval $dynvar_count_var=0
    fi

    eval dynvar_oldval_var=${dynvar_name}_oldval_'$'$dynvar_count_var
    eval $dynvar_oldval_var='$'$dynvar_name

    eval $dynvar_name='$'dynvar_value
}

unlet()
for dynvar_name
do
    dynvar_count_var=${dynvar_name}_dynvar_count
    eval dynvar_oldval_var=${dynvar_name}_oldval_'$'$dynvar_count_var
    eval $dynvar_name='$'$dynvar_oldval_var
    eval unset $dynvar_oldval_var
    eval $dynvar_count_var='$(( $'$dynvar_count_var' - 1 ))'
done

Maintenant vous pouvez:

$ let foobar test_value_1
$ echo $foobar
test_value_1
$ let foobar test_value_2
$ echo $foobar
test_value_2
$ let foobar test_value_3
$ echo $foobar
test_value_3
$ unlet foobar
$ echo $foobar
test_value_2
$ unlet foobar
$ echo $foobar
test_value_1

(Par ailleurs, unlet peut recevoir n'importe quel nombre de variables à la fois (sous forme d'arguments différents), pour plus de commodité, non présenté ci-dessus.)

N'essayez pas cela à la maison, ne le montrez pas aux enfants, ne le montrez pas à vos collègues, ne le montrez pas à #bash à Freenode, ne le montrez pas aux membres du comité POSIX, ne le montrez pas à M. Bourne, peut-être montrez-le au père le fantôme de McCarthy pour lui faire rire. Vous avez été prévenu et vous ne l'avez pas appris de moi.

MODIFIER:

Apparemment, j'ai été battu, envoyant le IRC bot greybot sur Freenode (appartient à #bash) la commande "posixlocal" lui donnera un code obscur qui montre un moyen de réaliser des variables locales dans POSIX sh. Voici une version quelque peu nettoyée, car l'original était difficile à déchiffrer:

f()
{
    if [ "$_called_f" ]
    then
        x=test1
        y=test2
        echo $x $y
    else
        _called_f=X x= y= command eval '{ typeset +x x y; } 2>/dev/null; f "$@"'
    fi
}

Cette transcription montre l'utilisation:

$ x=a
$ y=b
$ f
test1 test2
$ echo $x $y
a b

Il permet donc d'utiliser les variables x et y comme locales dans la branche then du formulaire if. Plus de variables peuvent être ajoutées à la branche else; notez qu'il faut les ajouter deux fois, une fois comme variable= dans la liste initiale, et une fois passé en argument à typeset. Notez qu'aucun unlet ou plus n'est nécessaire (c'est une implémentation "transparente"), et aucun eval de manipulation de nom et excessif n'est fait. Cela semble donc être une mise en œuvre beaucoup plus propre dans l'ensemble.

EDIT 2:

Apparaît typeset n'est pas défini par POSIX, et les implémentations d'Almquist Shell (FreeBSD, NetBSD, Debian) ne le prennent pas en charge. Le hack ci-dessus ne fonctionnera donc pas sur ces plateformes.

63
TaylanUB

Je crois que la chose la plus proche serait de mettre le corps de fonction à l'intérieur d'un sous-shell.

Par exemple. essaye ça

foo()
{
  ( x=43 ; echo $x )
}

x=42
echo $x
foo
echo $x
9
Lars Brinkhoff

Si vous souhaitez voyager en enfer avec moi, j'ai fait une implémentation plus élaborée du concept eval.

Celui-ci tient automatiquement compte de vos variables quasi-étendues, peut être appelé avec une syntaxe plus familière et correctement désactive (par opposition à la simple annulation) des variables lorsque vous quittez des étendues imbriquées.


Usage

Comme vous pouvez le voir, vous Push_scope pour saisir une étendue, _local pour déclarer vos variables quasi-locales, et pop_scope pour laisser une portée. Utilisation _unset pour annuler la définition d'une variable et pop_scope le réinitialisera lorsque vous reviendrez dans cette portée.

your_func() {
    Push_scope
    _local x="baby" y="you" z

    x="can"
    y="have"
    z="whatever"
    _unset z

    Push_scope
    _local x="you"
    _local y="like"
    pop_scope

    pop_scope
}

Code

Tous les suffixes de nom de variable charabia doivent être extrêmement sûrs contre les collisions de noms.

# Simulate entering of a nested variable scope
# To be used in conjunction with Push_scope(), pop_scope(), and _local()
Push_scope() {
    SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D=$(( $SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D + 1 ))
}

# Store the present value of the specified variable(s), allowing use in a new scope.
# To be used in conjunction with Push_scope(), pop_scope(), and _local()
#
# Parameters:
# $@ : string; name of variable to store the value of
scope_var() {
    for varname_FB94CFD263CF11E89500036F7F345232 in "${@}"; do
        eval "active_varnames_FB94CFD263CF11E89500036F7F345232=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES}\""

        # echo "Active varnames: ${active_varnames_FB94CFD263CF11E89500036F7F345232}"

        case " ${active_varnames_FB94CFD263CF11E89500036F7F345232} " in
            *" ${varname_FB94CFD263CF11E89500036F7F345232} "* )
                # This variable was already stored in a previous call
                # in the same scope. Do not store again.
                # echo "Push \${varname_FB94CFD263CF11E89500036F7F345232}, but already stored."
                :
                ;;

            * )
                if eval "[ -n \"\${${varname_FB94CFD263CF11E89500036F7F345232}+x}\" ]"; then
                    # Store the existing value from the previous scope.
                    # Only variables that were set (including set-but-empty) are stored
                    # echo "Pushing value of \$${varname_FB94CFD263CF11E89500036F7F345232}"
                    eval "SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_FB94CFD263CF11E89500036F7F345232}=\"\${${varname_FB94CFD263CF11E89500036F7F345232}}\""
                else
                    # Variable is unset. Do not store the value; an unstored
                    # value will be used to indicate its unset state. The
                    # variable name will still be registered.
                    # echo "Not pushing value of \$${varname_FB94CFD263CF11E89500036F7F345232}; was previously unset."
                    :
                fi

                # Add to list of variables managed in this scope.
                # List of variable names is space-delimited.
                eval "SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES}${varname_FB94CFD263CF11E89500036F7F345232} \""
                ;;
        esac

        unset active_varnames_FB94CFD263CF11E89500036F7F345232
    done

    unset varname_FB94CFD263CF11E89500036F7F345232
}

# Simulate declaration of a local variable
# To be used in conjunction with Push_scope(), pop_scope(), and _local()
#
# This function is a convenience wrapper over scope_var().
#
# Can be called just like the local keyword.
# Example usage: _local foo="foofoofoo" bar="barbarbar" qux qaz=""
_local() {
    for varcouple_44D4987063D111E8A46923403DDBE0C7 in "${@}"; do
        # Example string: foo="barbarbar"
        varname_44D4987063D111E8A46923403DDBE0C7="${varcouple_44D4987063D111E8A46923403DDBE0C7%%=*}"
        varvalue_44D4987063D111E8A46923403DDBE0C7="${varcouple_44D4987063D111E8A46923403DDBE0C7#*=}"
        varvalue_44D4987063D111E8A46923403DDBE0C7="${varvalue_44D4987063D111E8A46923403DDBE0C7#${varcouple_44D4987063D111E8A46923403DDBE0C7}}"

        # Store the value for the previous scope.
        scope_var "${varname_44D4987063D111E8A46923403DDBE0C7}"

        # Set the value for this scope.
        eval "${varname_44D4987063D111E8A46923403DDBE0C7}=\"\${varvalue_44D4987063D111E8A46923403DDBE0C7}\""

        unset varname_44D4987063D111E8A46923403DDBE0C7
        unset varvalue_44D4987063D111E8A46923403DDBE0C7
        unset active_varnames_44D4987063D111E8A46923403DDBE0C7
    done

    unset varcouple_44D4987063D111E8A46923403DDBE0C7
}

# Simulate unsetting a local variable.
#
# This function is a convenience wrapper over scope_var().
# 
# Can be called just like the unset keyword.
# Example usage: _unset foo bar qux
_unset() {
    for varname_6E40DA2E63D211E88CE68BFA58FE2BCA in "${@}"; do
        scope_var "${varname_6E40DA2E63D211E88CE68BFA58FE2BCA}"
        unset "${varname_6E40DA2E63D211E88CE68BFA58FE2BCA}"
    done
}

# Simulate exiting out of a nested variable scope
# To be used in conjunction with Push_scope(), pop_scope(), and _local()
pop_scope() {
    eval "varnames_2581E94263D011E88919B3D175643B87=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES}\""

    # Cannot iterate over $varnames by setting $IFS; $IFS does not work
    # properly on zsh. Workaround using string manipulation.
    while [ -n "${varnames_2581E94263D011E88919B3D175643B87}" ]; do
        # Strip enclosing spaces from $varnames.
        while true; do
            varnames_old_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87}"
            varnames_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87# }"
            varnames_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87% }"

            if [ "${varnames_2581E94263D011E88919B3D175643B87}" = "${varnames_2581E94263D011E88919B3D175643B87}" ]; then
                break
            fi
        done

        # Extract the variable name for the current iteration and delete it from the queue.
        varname_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87%% *}"
        varnames_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87#${varname_2581E94263D011E88919B3D175643B87}}"

        # echo "pop_scope() iteration on \$SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}"
        # echo "varname: ${varname_2581E94263D011E88919B3D175643B87}"

        if eval "[ -n \""\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}+x}"\" ]"; then
            # echo "Value found. Restoring value from previous scope."
            # echo eval "${varname_2581E94263D011E88919B3D175643B87}=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}}\""
            eval "${varname_2581E94263D011E88919B3D175643B87}=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}}\""
            unset "SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}"
        else
            # echo "Unsetting \$${varname_2581E94263D011E88919B3D175643B87}"
            unset "${varname_2581E94263D011E88919B3D175643B87}"
        fi

        # Variable cleanup.
        unset varnames_old_2581E94263D011E88919B3D175643B87
    done

    unset SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES
    unset varname_2581E94263D011E88919B3D175643B87
    unset varnames_2581E94263D011E88919B3D175643B87

    SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D=$(( $SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D - 1 ))
}
1
sorbet

Voici une fonction qui permet la portée:

scope() {
  eval "$(set)" command eval '\"\$@\"'
}

Exemple de script:

x() {
  y='in x'
  echo "$y"
}
y='outside x'
echo "$y"
scope x
echo "$y"

Résultat:

outside x
in x
outside x
1
user7620483