web-dev-qa-db-fra.com

Comment retourner une valeur de chaîne à partir d'une fonction Bash

Je voudrais retourner une chaîne d'une fonction Bash.

J'écrirai l'exemple dans Java pour montrer ce que j'aimerais faire:

public String getSomeString() {
  return "tadaa";
}

String variable = getSomeString();

L'exemple ci-dessous fonctionne avec bash, mais existe-t-il une meilleure façon de le faire?

function getSomeString {
   echo "tadaa"
}

VARIABLE=$(getSomeString)
427
Tomas F

Il n'y a pas de meilleur moyen que je connaisse. Bash ne connaît que les codes de statut (nombres entiers) et les chaînes écrites sur la sortie standard.

267
Philipp

Vous pouvez demander à la fonction de prendre une variable en tant que premier argument et de la modifier avec la chaîne que vous souhaitez renvoyer.

#!/bin/bash
set -x
function pass_back_a_string() {
    eval "$1='foo bar rab oof'"
}

return_var=''
pass_back_a_string return_var
echo $return_var

Imprime "foo bar rab oof".

Edit: ajout de la citation à l'endroit approprié pour permettre aux espaces dans la chaîne d'adresser le commentaire de @Luca Borrione.

Edit: En guise de démonstration, voir le programme suivant. C'est une solution polyvalente: elle vous permet même de recevoir une chaîne dans une variable locale.

#!/bin/bash
set -x
function pass_back_a_string() {
    eval "$1='foo bar rab oof'"
}

return_var=''
pass_back_a_string return_var
echo $return_var

function call_a_string_func() {
     local lvar=''
     pass_back_a_string lvar
     echo "lvar='$lvar' locally"
}

call_a_string_func
echo "lvar='$lvar' globally"

Cela imprime:

+ return_var=
+ pass_back_a_string return_var
+ eval 'return_var='\''foo bar rab oof'\'''
++ return_var='foo bar rab oof'
+ echo foo bar rab oof
foo bar rab oof
+ call_a_string_func
+ local lvar=
+ pass_back_a_string lvar
+ eval 'lvar='\''foo bar rab oof'\'''
++ lvar='foo bar rab oof'
+ echo 'lvar='\''foo bar rab oof'\'' locally'
lvar='foo bar rab oof' locally
+ echo 'lvar='\'''\'' globally'
lvar='' globally

Edit: démontrer que la valeur de la variable d'origine est disponible dans la fonction, comme l'a incorrectement critiqué @Xichen Li dans un commentaire .

#!/bin/bash
set -x
function pass_back_a_string() {
    eval "echo in pass_back_a_string, original $1 is \$$1"
    eval "$1='foo bar rab oof'"
}

return_var='original return_var'
pass_back_a_string return_var
echo $return_var

function call_a_string_func() {
     local lvar='original lvar'
     pass_back_a_string lvar
     echo "lvar='$lvar' locally"
}

call_a_string_func
echo "lvar='$lvar' globally"

Cela donne une sortie:

+ return_var='original return_var'
+ pass_back_a_string return_var
+ eval 'echo in pass_back_a_string, original return_var is $return_var'
++ echo in pass_back_a_string, original return_var is original return_var
in pass_back_a_string, original return_var is original return_var
+ eval 'return_var='\''foo bar rab oof'\'''
++ return_var='foo bar rab oof'
+ echo foo bar rab oof
foo bar rab oof
+ call_a_string_func
+ local 'lvar=original lvar'
+ pass_back_a_string lvar
+ eval 'echo in pass_back_a_string, original lvar is $lvar'
++ echo in pass_back_a_string, original lvar is original lvar
in pass_back_a_string, original lvar is original lvar
+ eval 'lvar='\''foo bar rab oof'\'''
++ lvar='foo bar rab oof'
+ echo 'lvar='\''foo bar rab oof'\'' locally'
lvar='foo bar rab oof' locally
+ echo 'lvar='\'''\'' globally'
lvar='' globally
186
bstpierre

Toutes les réponses ci-dessus ignorent ce qui a été dit dans la page de manuel de bash.

  • Toutes les variables déclarées dans une fonction seront partagées avec l'environnement appelant.
  • Toutes les variables déclarées locales ne seront pas partagées.

Exemple de code

#!/bin/bash

f()
{
    echo function starts
    local WillNotExists="It still does!"
    DoesNotExists="It still does!"
    echo function ends
}

echo $DoesNotExists #Should print empty line
echo $WillNotExists #Should print empty line
f                   #Call the function
echo $DoesNotExists #Should print It still does!
echo $WillNotExists #Should print empty line

Et sortie

$ sh -x ./x.sh
+ echo

+ echo

+ f
+ echo function starts 
function starts
+ local 'WillNotExists=It still does!'
+ DoesNotExists='It still does!'
+ echo function ends 
function ends
+ echo It still 'does!' 
It still does!
+ echo

Aussi, sous pdksh et ksh, ce script fait de même!

95
Vicky Ronnen

Bash, depuis la version 4.3 de février 2014 (?), Prend en charge explicitement les variables de référence ou les références de noms (namerefs), au-delà de "eval", avec les mêmes performances bénéfiques et le même effet indirectionnel, qui peuvent être plus claires dans vos scripts et aussi plus dures "oublier d'eval" et corriger cette erreur ":

declare [-aAfFgilnrtux] [-p] [name[=value] ...]
typeset [-aAfFgilnrtux] [-p] [name[=value] ...]
  Declare variables and/or give them attributes
  ...
  -n Give each name the nameref attribute, making it a name reference
     to another variable.  That other variable is defined by the value
     of name.  All references and assignments to name, except for⋅
     changing the -n attribute itself, are performed on the variable
     referenced by name's value.  The -n attribute cannot be applied to
     array variables.
...
When used in a function, declare and typeset make each name local,
as with the local command, unless the -g option is supplied...

et aussi:

PARAMÈTRES

L'attribut nameref peut être affecté à une variable à l'aide de l'option -n des commandes intégrées declare ou local (voir la description de declare et local ci-dessous) pour créer un nameref ou une référence à une autre variable. Cela permet aux variables d'être manipulées indirectement. Chaque fois que la variable nameref est référencée ou affectée, l'opération est réellement effectuée sur la variable spécifiée par la valeur de la variable nameref. Un nameref est couramment utilisé dans les fonctions du shell pour faire référence à une variable dont le nom est passé en argument à la fonction. Par exemple, si un nom de variable est passé à une fonction Shell en tant que premier argument,

      declare -n ref=$1

à l'intérieur de la fonction crée une variable de nommage ref dont la valeur est le nom de la variable transmis en tant que premier argument. Les références et les assignations à ref sont traitées comme des références et des assignations à la variable dont le nom a été passé comme $ 1. Si la variable de contrôle d'une boucle for possède l'attribut nameref, la liste de mots peut être une liste de variables Shell et une référence de nom sera établie pour chaque mot de la liste, à son tour, lors de l'exécution de la boucle. Les variables de tableau ne peuvent pas recevoir l'attribut -n. Cependant, les variables nameref peuvent faire référence à des variables de tableau et à des variables de tableau en indice. Namerefs peut être ⋅ non défini en utilisant l'option -n de la commande intégrée non définie. Autrement, si unset est exécuté avec le nom d'une variable nameref en tant qu'argument, la variable référencée par la variable nameref sera non définie.

Par exemple (EDIT 2: (merci Ron) namespaced (préfixé) le nom de la variable interne à la fonction, afin de minimiser les conflits de variables externes, qui devraient enfin répondre correctement, le problème soulevé dans les commentaires par Karsten) :

# $1 : string; your variable to contain the return value
function return_a_string () {
    declare -n ret=$1
    local MYLIB_return_a_string_message="The date is "
    MYLIB_return_a_string_message+=$(date)
    ret=$MYLIB_return_a_string_message
}

et tester cet exemple:

$ return_a_string result; echo $result
The date is 20160817

Notez que la commande intégrée "declare" de bash, lorsqu'elle est utilisée dans une fonction, rend la variable déclarée "locale" par défaut et que "-n" peut également être utilisé avec "local".

Je préfère distinguer les variables "important declare" des variables "ennuyeuses locales", donc utiliser "declare" et "local" de cette manière sert de documentation.

EDIT 1 - (Réponse au commentaire ci-dessous de Karsten) - Je ne peux plus ajouter de commentaires ci-dessous, mais le commentaire de Karsten m'a fait réfléchir. J'ai donc fait le test suivant WORKS FINE, AFAICT - Karsten si vous lisez ceci, veuillez fournir un ensemble exact d'étapes de test à partir de la ligne de commande, montrant le problème que vous supposez exister, car ces étapes fonctionnent parfaitement:

$ return_a_string ret; echo $ret
The date is 20170104

(Je l'ai lancé tout à l'heure, après avoir collé la fonction ci-dessus dans un terme bash - comme vous pouvez le constater, le résultat fonctionne parfaitement.)

43
zenaan

Comme bstpierre ci-dessus, j'utilise et recommande l'utilisation de nommer explicitement les variables de sortie:

function some_func() # OUTVAR ARG1
{
   local _outvar=$1
   local _result # Use some naming convention to avoid OUTVARs to clash
   ... some processing ....
   eval $_outvar=\$_result # Instead of just =$_result
}

Notez l'utilisation de la citation du $. Cela évitera d'interpréter le contenu de $result en tant que caractères spéciaux du shell. J'ai trouvé qu'il s'agit d'un ordre de grandeur plus rapide que l'idiogramme result=$(some_func "arg1") de capture d'un écho. La différence de vitesse semble encore plus notable lors de l'utilisation de bash sur MSYS, où la capture stdout à partir d'appels de fonction est presque catastrophique.

Il est correct d'envoyer une variable locale car la taille des sections locales est dynamique dans bash:

function another_func() # ARG
{
   local result
   some_func result "$1"
   echo result is $result
}
34
Markarian451

Vous pouvez également capturer la sortie de la fonction:

#!/bin/bash
function getSomeString() {
     echo "tadaa!"
}

return_var=$(getSomeString)
echo $return_var
# Alternative syntax:
return_var=`getSomeString`
echo $return_var

Ça a l'air bizarre, mais c'est mieux que d'utiliser des variables globales IMHO. Passer les paramètres fonctionne comme d'habitude, il suffit de les insérer dans les accolades ou les backticks.

22
chiborg

Comme mentionné précédemment, la méthode "correcte" pour renvoyer une chaîne d'une fonction consiste à utiliser une substitution de commande. Dans le cas où la fonction doit également sortir vers la console (comme @Mani le mentionne ci-dessus), créez un fd temporaire au début de la fonction et redirigez-le vers la console. Fermez le fd temporaire avant de retourner votre chaîne.

#!/bin/bash
# file:  func_return_test.sh
returnString() {
    exec 3>&1 >/dev/tty
    local s=$1
    s=${s:="some default string"}
    echo "writing directly to console"
    exec 3>&-     
    echo "$s"
}

my_string=$(returnString "$*")
echo "my_string:  [$my_string]"

l'exécution de script sans paramètres génère ...

# ./func_return_test.sh
writing directly to console
my_string:  [some default string]

espérons que cela aide les gens

-Andy

12
Andy

La solution la plus simple et la plus robuste consiste à utiliser la substitution de commande, comme d'autres personnes l'ont écrit:

assign()
{
    local x
    x="Test"
    echo "$x"
}

x=$(assign) # This assigns string "Test" to x

L'inconvénient est la performance car cela nécessite un processus séparé.

L'autre technique suggérée dans cette rubrique, à savoir transmettre le nom d'une variable à affecter en argument, a des effets secondaires et je ne le recommanderais pas dans sa forme de base. Le problème est que vous aurez probablement besoin de certaines variables dans la fonction pour calculer la valeur de retour, et il peut arriver que le nom de la variable destinée à stocker la valeur de retour interfère avec l'une d'entre elles:

assign()
{
    local x
    x="Test"
    eval "$1=\$x"
}

assign y # This assigns string "Test" to y, as expected

assign x # This will NOT assign anything to x in this scope
         # because the name "x" is declared as local inside the function

Vous pouvez bien sûr ne pas déclarer les variables internes de la fonction en tant que locales, mais vous devez toujours le faire, sinon vous pourriez écraser par inadvertance une variable non liée de la portée parente s'il en existe une du même nom. .

Une solution de contournement possible est une déclaration explicite de la variable transmise en tant que global:

assign()
{
    local x
    eval declare -g $1
    x="Test"
    eval "$1=\$x"
}

Si le nom "x" est passé en tant qu'argument, la deuxième ligne du corps de la fonction remplacera la déclaration locale précédente. Mais les noms eux-mêmes peuvent toujours interférer, donc si vous avez l'intention d'utiliser la valeur précédemment stockée dans la variable transmise avant d'y écrire la valeur de retour, sachez que vous devez la copier dans une autre variable locale au tout début. sinon le résultat sera imprévisible! En outre, cela ne fonctionnera que dans la version la plus récente de BASH, à savoir la version 4.2. Un code plus portable pourrait utiliser des constructions conditionnelles explicites avec le même effet:

assign()
{
    if [[ $1 != x ]]; then
      local x
    fi
    x="Test"
    eval "$1=\$x"
}

La solution la plus élégante est peut-être simplement de réserver un nom global pour les valeurs de retour de fonction et de l’utiliser systématiquement dans chaque fonction que vous écrivez.

10
Tomasz Żuk

Vous pouvez utiliser une variable globale:

declare globalvar='some string'

string ()
{
  eval  "$1='some other string'"
} # ----------  end of function string  ----------

string globalvar

echo "'${globalvar}'"

Cela donne

'some other string'
8
Fritz G. Mehner

Pour illustrer mon commentaire sur la réponse de Andy, avec une manipulation supplémentaire du descripteur de fichier afin d'éviter l'utilisation de /dev/tty:

#!/bin/bash

exec 3>&1

returnString() {
    exec 4>&1 >&3
    local s=$1
    s=${s:="some default string"}
    echo "writing to stdout"
    echo "writing to stderr" >&2
    exec >&4-
    echo "$s"
}

my_string=$(returnString "$*")
echo "my_string:  [$my_string]"

Toujours méchant, cependant.

6
jmb

La façon dont vous l'avez est le seul moyen de le faire sans casser la portée. Bash n'a pas de concept de type de retour, mais juste des codes de sortie et des descripteurs de fichier (stdin/out/err, etc.)

3
Daenyth

Adressage Vicky Ronnen va de haut en considérant le code suivant:

function use_global
{
    eval "$1='changed using a global var'"
}

function capture_output
{
    echo "always changed"
}

function test_inside_a_func
{
    local _myvar='local starting value'
    echo "3. $_myvar"

    use_global '_myvar'
    echo "4. $_myvar"

    _myvar=$( capture_output )
    echo "5. $_myvar"
}

function only_difference
{
    local _myvar='local starting value'
    echo "7. $_myvar"

    local use_global '_myvar'
    echo "8. $_myvar"

    local _myvar=$( capture_output )
    echo "9. $_myvar"
}

declare myvar='global starting value'
echo "0. $myvar"

use_global 'myvar'
echo "1. $myvar"

myvar=$( capture_output )
echo "2. $myvar"

test_inside_a_func
echo "6. $_myvar" # this was local inside the above function

only_difference



va donner

0. global starting value
1. changed using a global var
2. always changed
3. local starting value
4. changed using a global var
5. always changed
6. 
7. local starting value
8. local starting value
9. always changed

Le scénario normal consiste peut-être à utiliser la syntaxe utilisée dans la fonction test_inside_a_func. Vous pouvez donc utiliser les deux méthodes dans la majorité des cas, bien que la capture de la sortie soit la méthode la plus sûre, fonctionnant toujours dans toutes les situations et reproduisant la valeur renvoyée par une fonction que vous pouvez trouver dans d'autres langues, comme Vicky Ronnen a été correctement souligné.

3
Luca Borrione

Le problème clé de tout schéma de "variable de sortie nommée" dans lequel l'appelant peut transmettre le nom de la variable (que vous utilisiez eval ou declare -n) est un aliasing par inadvertance, c'est-à-dire des conflits de noms: Du point de vue de l'encapsulation, il est affreux de ne pas pouvoir ajouter ou renommer une variable locale dans une fonction sans vérifier ALL les appelants de la fonction avant de s'assurer qu'ils ne le sont pas vouloir passer le même nom que le paramètre de sortie. (Ou dans l'autre sens, je ne veux pas avoir à lire le source de la fonction que j'appelle juste pour m'assurer que le paramètre de sortie que j'ai l'intention d'utiliser n'est pas local dans cette fonction.)

La seule solution consiste à utiliser une seule variable de sortie dédiée, telle que REPLY (comme suggéré par Evi1M4chine ) ou une convention similaire à celle suggérée par Ron Burk .

Cependant, il est possible de faire en sorte que les fonctions utilisent une variable de sortie fixe en interne , puis ajoutez du sucre par dessus masque ce fait de l'appelant , comme je l'ai fait avec la fonction call dans l'exemple suivant. Considérez ceci comme une preuve de concept, mais les points clés sont

  • La fonction affecte toujours la valeur de retour à REPLY, et peut également renvoyer un code de sortie comme d'habitude
  • Du point de vue de l'appelant, la valeur de retour peut être assignée à n'importe quelle variable (locale ou globale), y compris REPLY (voir l'exemple wrapper.). Le code de sortie de la fonction est transmis. Vous pouvez donc les utiliser, par exemple. un if ou while ou des constructions similaires fonctionnent comme prévu.
  • Syntaxiquement, l'appel de fonction est toujours une simple déclaration.

Cela fonctionne parce que la fonction call elle-même n'a pas de locus et n'utilise aucune autre variable que REPLY, ce qui évite tout risque de conflit de noms. Au moment où le nom de variable de sortie défini par l'appelant est attribué, nous sommes effectivement dans la portée de l'appelant (techniquement dans la portée identique de la fonction call, plutôt que dans la portée de la fonction appelée.

#!/bin/bash
function call() { # var=func [args ...]
  REPLY=; "${1#*=}" "${@:2}"; eval "${1%%=*}=\$REPLY; return $?"
}

function greet() {
  case "$1" in
    us) REPLY="hello";;
    nz) REPLY="kia ora";;
    *) return 123;;
  esac
}

function wrapper() {
  call REPLY=greet "$@"
}

function main() {
  local a b c d
  call a=greet us
  echo "a='$a' ($?)"
  call b=greet nz
  echo "b='$b' ($?)"
  call c=greet de
  echo "c='$c' ($?)"
  call d=wrapper us
  echo "d='$d' ($?)"
}
main

Sortie:

a='hello' (0)
b='kia ora' (0)
c='' (123)
d='hello' (0)
2
Karsten

Les options ont toutes été énumérées, je pense. En choisir un peut résulter du style le mieux adapté à votre application, et dans cette optique, je souhaite proposer un style particulier que j'ai trouvé utile. Dans bash, les variables et les fonctions ne sont pas dans le même espace de noms. Ainsi, traiter la variable du même nom comme la valeur de la fonction est une convention qui minimise les conflits de noms et améliore la lisibilité, si je l’applique de manière rigoureuse. Un exemple de la vie réelle:

UnGetChar=
function GetChar() {
    # assume failure
    GetChar=
    # if someone previously "ungot" a char
    if ! [ -z "$UnGetChar" ]; then
        GetChar="$UnGetChar"
        UnGetChar=
        return 0               # success
    # else, if not at EOF
    Elif IFS= read -N1 GetChar ; then
        return 0           # success
    else
        return 1           # EOF
    fi
}

function UnGetChar(){
    UnGetChar="$1"
}

Et, un exemple d'utilisation de telles fonctions:

function GetToken() {
    # assume failure
    GetToken=
    # if at end of file
    if ! GetChar; then
        return 1              # EOF
    # if start of comment
    Elif [[ "$GetChar" == "#" ]]; then
        while [[ "$GetChar" != $'\n' ]]; do
            GetToken+="$GetChar"
            GetChar
        done
        UnGetChar "$GetChar"
    # if start of quoted string
    Elif [ "$GetChar" == '"' ]; then
# ... et cetera

Comme vous pouvez le constater, le statut de retour est à votre disposition si vous en avez besoin, ou si vous l’ignorez. La variable "retournée" peut également être utilisée ou ignorée, mais bien sûr, seule après la fonction est invoquée.

Bien sûr, ce n’est qu’une convention. Vous êtes libre de ne pas pouvoir définir la valeur associée avant de renvoyer (d'où ma convention de toujours l'annuler au début de la fonction) ou de piétiner sa valeur en appelant à nouveau la fonction (éventuellement indirectement). Néanmoins, c’est une convention que j’ai très utile de faire si j’utilise beaucoup les fonctions bash.

Contrairement au sentiment qu’il s’agit d’un signe, on devrait par exemple "move to Perl", ma philosophie est que les conventions sont toujours importantes pour gérer la complexité de toutes les langues.

2
Ron Burk

bash modèle pour renvoyer les deux objets scalaire et tablea objets de valeur:

définition

url_parse() { # parse 'url' into: 'url_Host', 'url_port', ...
   local "$@" # inject caller 'url' argument in local scope
   local url_Host="..." url_path="..." # calculate 'url_*' components
   declare -p ${!url_*} # return only 'url_*' object fields to the caller
}

invocation

main() { # invoke url parser and inject 'url_*' results in local scope
   eval "$(url_parse url=http://Host/path)" # parse 'url'
   echo "Host=$url_Host path=$url_path" # use 'url_*' components
}
1
Andrei Pozolotin

Vous pouvez echo une chaîne, mais attrapez-la en transmettant (|) la fonction à autre chose.

Vous pouvez le faire avec expr, cependant ShellCheck indique que cet usage est déconseillé.

1
apennebaker

Par convention, dans mes programmes, voici à quoi sert la variable $REPLY préexistante, que read utilise à cette fin précise.

function getSomeString {
  REPLY="tadaa"
}

getSomeString
echo $REPLY

Cette echoes

tadaa

Mais pour éviter les conflits, toute autre variable globale fera l'affaire.

declare result

function getSomeString {
  result="tadaa"
}

getSomeString
echo $result

Si cela ne suffit pas, je recommande la solution Markarian451.

0
Evi1M4chine