Je dois écrire une fonction en bash. La fonction prendra environ 7 arguments. Je sais que je peux appeler une fonction comme celle-ci:
Pour appeler une fonction avec des paramètres:
function_name $arg1 $arg2
Et je peux référencer mes paramètres comme celui-ci dans la fonction:
function_name () {
echo "Parameter #1 is $1"
}
Ma question est, existe-t-il une meilleure façon de se référer aux paramètres à l'intérieur de la fonction? Puis-je éviter les choses $ 1, $ 2, $ 3, .... et utiliser simplement $ arg1, $ arg2, ...?
Existe-t-il une méthode appropriée pour cela ou dois-je réaffecter ces paramètres à d'autres variables à l'intérieur de la fonction? Par exemple.:
function_name () {
$ARG1=$1
echo "Parameter #1 is $ARG1"
}
Tout exemple serait très apprécié.
La façon la plus courante de procéder consiste à affecter les arguments aux variables locales de la fonction, à savoir:
copy() {
local from=${1}
local to=${2}
# ...
}
Une autre solution peut être getopt - analyse des options de style.
copy() {
local arg from to
while getopts 'f:t:' arg
do
case ${arg} in
f) from=${OPTARG};;
t) to=${OPTARG};;
*) return 1 # illegal option
esac
done
}
copy -f /tmp/a -t /tmp/b
Malheureusement, bash ne peut pas gérer les options longues qui seraient plus lisibles, à savoir:
copy --from /tmp/a --to /tmp/b
Pour cela, vous devez utiliser le programme externe getopt
(qui, je pense, ne prend en charge les options longues que sur les systèmes GNU)) ou implémenter l'analyseur d'options longues à la main, à savoir:
copy() {
local from to
while [[ ${1} ]]; do
case "${1}" in
--from)
from=${2}
shift
;;
--to)
to=${2}
shift
;;
*)
echo "Unknown parameter: ${1}" >&2
return 1
esac
if ! shift; then
echo 'Missing parameter argument.' >&2
return 1
fi
done
}
copy --from /tmp/a --to /tmp/b
Vous pouvez également être paresseux et simplement passer les `` variables '' comme arguments à la fonction, c'est-à-dire:
copy() {
local "${@}"
# ...
}
copy from=/tmp/a to=/tmp/b
et vous aurez ${from}
et ${to}
dans la fonction en tant que variables locales.
Notez simplement que le même problème que ci-dessous s'applique - si une variable particulière n'est pas transmise, elle sera héritée de l'environnement parent. Vous voudrez peut-être ajouter une "ligne de sécurité" comme:
copy() {
local from to # reset first
local "${@}"
# ...
}
pour vous assurer que ${from}
et ${to}
seront désactivés lorsqu'ils ne seront pas transmis.
Et si quelque chose très mauvais vous intéresse, vous pouvez également affecter les arguments comme variables globales lors de l'appel de la fonction, à savoir:
from=/tmp/a to=/tmp/b copy
Ensuite, vous pouvez simplement utiliser ${from}
Et ${to}
Dans la fonction copy()
. Notez simplement que vous devez alors toujours passer tous les paramètres. Sinon, une variable aléatoire peut s'infiltrer dans la fonction.
from= to=/tmp/b copy # safe
to=/tmp/b copy # unsafe: ${from} may be declared elsewhere
Si vous avez bash 4.1 (je pense), vous pouvez également essayer d'utiliser des tableaux associatifs. Il vous permettra de passer des arguments nommés mais il sera laid. Quelque chose comme:
args=( [from]=/tmp/a [to]=/tmp/b )
copy args
Et puis dans copy()
, vous devez saisir le tablea .
Vous pouvez toujours faire passer des choses dans l'environnement:
#!/bin/sh
foo() {
echo arg1 = $arg1
echo arg2 = $arg2
}
arg1=banana arg2=Apple foo
Les fonctions shell ont un accès complet à toutes les variables disponibles dans leur portée d'appel, sauf pour les noms de variables qui sont utilisés comme variables locales à l'intérieur de la fonction elle-même. De plus, toute variable non locale définie dans une fonction est disponible à l'extérieur après l'appel de la fonction. Prenons l'exemple suivant:
A=aaa
B=bbb
echo "A=$A B=$B C=$C"
example() {
echo "example(): A=$A B=$B C=$C"
A=AAA
local B=BBB
C=CCC
echo "example(): A=$A B=$B C=$C"
}
example
echo "A=$A B=$B C=$C"
Cet extrait a la sortie suivante:
A=aaa B=bbb C=
example(): A=aaa B=bbb C=
example(): A=AAA B=BBB C=CCC
A=AAA B=bbb C=CCC
L'inconvénient évident de cette approche est que les fonctions ne sont plus autonomes et que la définition d'une variable en dehors d'une fonction peut avoir des effets secondaires inattendus. Cela rendrait également les choses plus difficiles si vous vouliez passer des données à une fonction sans les affecter d'abord à une variable, car cette fonction n'utilise plus de paramètres de position.
La façon la plus courante de gérer cela est d'utiliser variables locales pour les arguments et toute variable temporaire dans une fonction:
example() {
local A="$1" B="$2" C="$3" TMP="/tmp"
...
}
Cela évite de polluer l'espace de noms Shell avec des variables fonction-locales.
Tout ce que vous avez à faire est de nommer les variables en chemin vers l'appel de fonction.
function test() {
echo $a
}
a='hello world' test
#prove variable didnt leak
echo $a .
Ce n'est pas seulement une fonction des fonctions, vous pouvez avoir cette fonction dans son propre script et appeler a='hello world' test.sh
et cela fonctionnerait tout de même
Pour vous amuser un peu, vous pouvez combiner cette méthode avec des arguments de position (par exemple, vous créez un script et certains utilisateurs ne connaissent peut-être pas les noms des variables).
Heck, pourquoi ne pas laisser les valeurs par défaut pour ces arguments aussi? Bien sûr, petit pois facile!
function test2() {
[[ -n "$1" ]] && local a="$1"; [[ -z "$a" ]] && local a='hi'
[[ -n "$2" ]] && local b="$2"; [[ -z "$b" ]] && local b='bye'
echo $a $b
}
#see the defaults
test2
#use positional as usual
test2 '' there
#use named parameter
a=well test2
#mix it up
b=one test2 Nice
#prove variables didnt leak
echo $a $b .
Notez que si test
était son propre script, vous n'avez pas besoin d'utiliser le mot clé local
.
Je pense que j'ai une solution pour vous. Avec quelques astuces, vous pouvez réellement transmettre des paramètres nommés aux fonctions, ainsi que des tableaux.
La méthode que j'ai développée vous permet d'accéder aux paramètres passés à une fonction comme celle-ci:
testPassingParams() {
@var hello
l=4 @array anArrayWithFourElements
l=2 @array anotherArrayWithTwo
@var anotherSingle
@reference table # references only work in bash >=4.3
@params anArrayOfVariedSize
test "$hello" = "$1" && echo correct
#
test "${anArrayWithFourElements[0]}" = "$2" && echo correct
test "${anArrayWithFourElements[1]}" = "$3" && echo correct
test "${anArrayWithFourElements[2]}" = "$4" && echo correct
# etc...
#
test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
#
test "$anotherSingle" = "$8" && echo correct
#
test "${table[test]}" = "works"
table[inside]="adding a new value"
#
# I'm using * just in this example:
test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}
fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"
testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."
test "${assocArray[inside]}" = "adding a new value"
En d'autres termes, non seulement vous pouvez appeler vos paramètres par leurs noms (ce qui constitue un noyau plus lisible), vous pouvez en fait passer des tableaux (et des références à des variables - cette fonctionnalité ne fonctionne que dans bash 4.3 cependant)! De plus, les variables mappées sont toutes dans la portée locale, tout comme $ 1 (et autres).
Le code qui fait ce travail est assez léger et fonctionne à la fois en bash 3 et bash 4 (ce sont les seules versions avec lesquelles je l'ai testé). Si vous êtes intéressé par plus de trucs comme celui-ci qui rendent le développement avec bash beaucoup plus agréable et plus facile, vous pouvez jeter un œil à mon Bash Infinity Framework , le code ci-dessous a été développé à cet effet.
Function.AssignParamLocally() {
local commandWithArgs=( $1 )
local command="${commandWithArgs[0]}"
shift
if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
then
paramNo+=-1
return 0
fi
if [[ "$command" != "local" ]]
then
assignNormalCodeStarted=true
fi
local varDeclaration="${commandWithArgs[1]}"
if [[ $varDeclaration == '-n' ]]
then
varDeclaration="${commandWithArgs[2]}"
fi
local varName="${varDeclaration%%=*}"
# var value is only important if making an object later on from it
local varValue="${varDeclaration#*=}"
if [[ ! -z $assignVarType ]]
then
local previousParamNo=$(expr $paramNo - 1)
if [[ "$assignVarType" == "array" ]]
then
# passing array:
execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
eval "$execute"
paramNo+=$(expr $assignArrLength - 1)
unset assignArrLength
Elif [[ "$assignVarType" == "params" ]]
then
execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
eval "$execute"
Elif [[ "$assignVarType" == "reference" ]]
then
execute="$assignVarName=\"\$$previousParamNo\""
eval "$execute"
Elif [[ ! -z "${!previousParamNo}" ]]
then
execute="$assignVarName=\"\$$previousParamNo\""
eval "$execute"
fi
fi
assignVarType="$__capture_type"
assignVarName="$varName"
assignArrLength="$__capture_arrLength"
}
Function.CaptureParams() {
__capture_type="$_type"
__capture_arrLength="$l"
}
alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias @param='@trapAssign local'
alias @reference='_type=reference @trapAssign local -n'
alias @var='_type=var @param'
alias @params='_type=params @param'
alias @array='_type=array @param'
J'espérais personnellement voir une sorte de syntaxe comme
func(a b){
echo $a
echo $b
}
Mais comme ce n'est pas une chose, et que je vois pas mal de références à des variables globales (non sans la mise en garde des conflits de portée et de nommage), je partagerai mon approche.
Utiliser la fonction copy
de réponse de Michal :
copy(){
cp $from $to
}
from=/tmp/a
to=/tmp/b
copy
C'est mauvais, parce que from
et to
sont des mots si larges que n'importe quel nombre de fonctions pourraient l'utiliser. Vous pourriez rapidement vous retrouver avec un conflit de dénomination ou une "fuite" sur vos mains.
letter(){
echo "From: $from"
echo "To: $to"
echo
echo "$1"
}
to=Emily
letter "Hello Emily, you're fired for missing two days of work."
# Result:
# From: /tmp/a
# To: Emily
# Hello Emily, you're fired for missing two days of work.
Mon approche consiste donc à les "nommer". Je nomme la variable après la fonction et la supprime une fois la fonction terminée. Bien sûr, je ne l'utilise que pour les valeurs facultatives qui ont des valeurs par défaut. Sinon, j'utilise simplement des arguments positionnels.
copy(){
if [[ $copy_from ]] && [[ $copy_to ]]; then
cp $copy_from $copy_to
unset copy_from copy_to
fi
}
copy_from=/tmp/a
copy_to=/tmp/b
copy # Copies /tmp/a to /tmp/b
copy # Does nothing, as it ought to
letter "Emily, you're 'not' re-hired for the 'not' bribe ;)"
# From: (no /tmp/a here!)
# To:
# Emily, you're 'not' re-hired for the 'not' bribe ;)
Je ferais un terrible patron ...
En pratique, mes noms de fonction sont plus élaborés que "copie" ou "lettre".
L'exemple le plus récent dans ma mémoire est get_input()
, qui a gi_no_sort
Et gi_Prompt
.
gi_no_sort
Est une valeur vraie/fausse qui détermine si les suggestions de fin sont triées ou non. Par défaut à truegi_Prompt
Est une chaîne qui est ... eh bien, cela va de soi. La valeur par défaut est "".Les arguments réels que la fonction prend sont la source des "suggestions d'achèvement" susmentionnées pour l'invite d'entrée, et comme cette liste est tirée de $@
Dans la fonction, les "arguments nommés" sont facultatifs[1], et il n'y a aucun moyen évident de faire la distinction entre une chaîne signifiée comme un achèvement et un message booléen/d'invite, ou vraiment quoi que ce soit séparé par un espace dans bash, d'ailleurs[2]; la solution ci-dessus a fini par me sauver beaucoup de problèmes .
remarques:
Ainsi, un shift
et $1
, $2
, Etc. codés en dur sont hors de question.
Par exemple. "0 Enter a command: {1..9} $(ls)"
une valeur de 0
, "Enter a command:"
et un ensemble de 1 2 3 4 5 6 7 8 9 <directory contents>
? Ou "0"
, "Enter"
, "a"
Et "command:"
Font également partie de cet ensemble? Bash assumera ce dernier, que cela vous plaise ou non.
Les arguments sont envoyés aux fonctions en tant que Tuple d'éléments individuels, ils n'ont donc pas de nom en tant que tel, juste des positions. cela permet des possibilités intéressantes comme ci-dessous, mais cela signifie que vous êtes coincé avec 1 $. $ 2, etc. quant à l'opportunité de les mapper à de meilleurs noms, la question se résume à la taille de la fonction et à la clarté de la lecture du code. si c'est complexe, alors mapper des noms significatifs ($ BatchID, $ FirstName, $ SourceFilePath) est une bonne idée. pour les trucs simples cependant, ce n'est probablement pas nécessaire. Je ne dérangerais certainement pas si vous utilisez des noms comme $ arg1.
maintenant, si vous voulez simplement renvoyer les paramètres en écho, vous pouvez les parcourir:
for $arg in "$@"
do
echo "$arg"
done
juste un fait amusant; sauf si vous traitez une liste, vous êtes probablement intéressé par quelque chose de plus utile
c'est un sujet plus ancien, mais je voudrais quand même partager la fonction ci-dessous (nécessite bash 4). Il analyse les arguments nommés et définit les variables dans l'environnement de scripts. Assurez-vous simplement d'avoir des valeurs par défaut raisonnables pour tous les paramètres dont vous avez besoin. La déclaration d'exportation à la fin pourrait également être juste un eval. C'est génial en combinaison avec shift pour étendre les scripts existants qui prennent déjà quelques paramètres de position et vous ne voulez pas changer la syntaxe, mais ajoutez encore une certaine flexibilité.
parseOptions()
{
args=("$@")
for opt in "${args[@]}"; do
if [[ ! "${opt}" =~ .*=.* ]]; then
echo "badly formatted option \"${opt}\" should be: option=value, stopping..."
return 1
fi
local var="${opt%%=*}"
local value="${opt#*=}"
export ${var}="${value}"
done
return 0
}