Au travail, j'écris fréquemment des scripts bash. Mon superviseur a suggéré que le script entier soit divisé en fonctions, comme dans l'exemple suivant:
#!/bin/bash
# Configure variables
declare_variables() {
noun=geese
count=three
}
# Announce something
i_am_foo() {
echo "I am foo"
sleep 0.5
echo "hear me roar!"
}
# Tell a joke
walk_into_bar() {
echo "So these ${count} ${noun} walk into a bar..."
}
# Emulate a pendulum clock for a bit
do_baz() {
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
}
# Establish run order
main() {
declare_variables
i_am_foo
walk_into_bar
do_baz
}
main
Y a-t-il une autre raison à cela que la "lisibilité", qui je pense pourrait être tout aussi bien établie avec quelques commentaires de plus et un certain espacement des lignes?
Est-ce que cela rend le script plus efficace (je m'attendrais en fait à l'opposé, le cas échéant), ou cela facilite-t-il la modification du code au-delà du potentiel de lisibilité susmentionné? Ou est-ce vraiment juste une préférence stylistique?
Veuillez noter que bien que le script ne le montre pas bien, "l'ordre d'exécution" des fonctions dans nos scripts réels a tendance à être très linéaire - walk_into_bar
dépend de choses qui i_am_foo
a terminé et do_baz
agit sur les éléments configurés par walk_into_bar
- donc être en mesure d'échanger arbitrairement l'ordre d'exécution n'est pas quelque chose que nous ferions généralement. Par exemple, vous ne voudriez pas soudainement mettre declare_variables
après walk_into_bar
, ça casserait les choses.
Un exemple de la façon dont j'écrirais le script ci-dessus serait:
#!/bin/bash
# Configure variables
noun=geese
count=three
# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"
# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."
# Emulate a pendulum clock for a bit
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
J'ai commencé à utiliser ce même style de programmation bash après avoir lu Article de blog de Kfir Lavi "Programmation défensive Bash" . Il donne plusieurs bonnes raisons, mais personnellement, je trouve celles-ci les plus importantes:
les procédures deviennent descriptives: il est beaucoup plus facile de comprendre ce qu'une partie particulière du code est censée faire. Au lieu du mur de code, vous voyez "Oh, la fonction find_log_errors
Lit ce fichier journal pour les erreurs". Comparez-le avec la recherche de nombreuses lignes awk/grep/sed qui utilisent Dieu sait quel type de regex au milieu d'un long script - vous n'avez aucune idée de ce qu'il fait là-bas à moins qu'il y ait des commentaires.
vous pouvez déboguer des fonctions en les joignant à set -x
et set +x
. Une fois que vous savez que le reste du code fonctionne bien, vous pouvez utiliser cette astuce pour vous concentrer sur le débogage uniquement de cette fonction spécifique. Bien sûr, vous pouvez joindre des parties de script, mais que se passe-t-il si c'est une longue portion? Il est plus facile de faire quelque chose comme ça:
set -x
parse_process_list
set +x
utilisation de l'impression avec cat <<- EOF . . . EOF
. Je l'ai utilisé plusieurs fois pour rendre mon code beaucoup plus professionnel. De plus, parse_args()
avec fonction getopts
est très pratique. Encore une fois, cela aide à la lisibilité, au lieu de tout mettre dans le script comme un mur de texte géant. Il est également pratique de les réutiliser.
Et évidemment, c'est beaucoup plus lisible pour quelqu'un qui connaît C ou Java, ou Vala, mais a une expérience bash limitée. En ce qui concerne l'efficacité, il n'y a pas beaucoup de ce que vous pouvez faire - bash lui-même n'est pas le langage le plus efficace et les gens préfèrent Perl et python quand il s'agit de vitesse et d'efficacité. Cependant, vous pouvez Nice
une fonction:
Nice -10 resource_hungry_function
Comparé à appeler Nice sur chaque ligne de code, cela diminue beaucoup de frappe ET peut être utilisé de manière pratique lorsque vous ne souhaitez exécuter qu'une partie de votre script avec une priorité inférieure.
L'exécution de fonctions en arrière-plan, à mon avis, est également utile lorsque vous souhaitez avoir tout un tas d'instructions à exécuter en arrière-plan.
Certains des exemples où j'ai utilisé ce style:
Lisibilité est une chose. Mais il y a plus à modularisation que juste cela. ( Semi-modularisation est peut-être plus correct pour les fonctions.)
Dans les fonctions, vous pouvez garder certaines variables locales, ce qui augmente fiabilité, ce qui diminue le risque de gâcher les choses.
Un autre pro des fonctions est réutilisation. Une fois qu'une fonction est codée, elle peut être appliquée plusieurs fois dans le script. Vous pouvez également le porter vers un autre script.
Votre code peut maintenant être linéaire, mais à l'avenir, vous pourrez entrer dans le domaine de multi-threading, ou multi-processing dans le monde Bash. Une fois que vous aurez appris à faire des choses dans les fonctions, vous serez bien équipé pour passer au parallèle.
Un dernier point à ajouter. Comme Etsitpab Nioliv le remarque dans le commentaire ci-dessous, il est facile de rediriger à partir des fonctions en tant qu'entité cohérente. Mais il y a un autre aspect des redirections avec des fonctions. À savoir, les redirections peuvent être définies le long de la définition de la fonction. Par exemple.:
f () { echo something; } > log
Désormais, aucune redirection explicite n'est requise par les appels de fonction.
$ f
Cela peut épargner de nombreuses répétitions, ce qui augmente encore la fiabilité et aide à garder les choses en ordre.
Voir aussi
Dans mon commentaire, j'ai mentionné trois avantages des fonctions:
Ils sont plus faciles à tester et à vérifier l'exactitude.
Les fonctions peuvent être facilement réutilisées (sourcées) dans les futurs scripts
Votre patron les aime.
Et, ne sous-estimez jamais l'importance du numéro 3.
Je voudrais aborder un autre problème:
... donc être en mesure d'échanger arbitrairement l'ordre d'exécution n'est pas quelque chose que nous ferions généralement. Par exemple, vous ne voudriez pas soudainement mettre
declare_variables
aprèswalk_into_bar
, ça casserait les choses.
Pour obtenir l'avantage de diviser le code en fonctions, il faut essayer de rendre les fonctions aussi indépendantes que possible. Si walk_into_bar
requiert une variable qui n'est pas utilisée ailleurs, alors cette variable doit être définie dans et rendue locale à walk_into_bar
. Le processus de séparation du code en fonctions et de minimisation de leurs interdépendances devrait rendre le code plus clair et plus simple.
Idéalement, les fonctions devraient être faciles à tester individuellement. Si, à cause des interactions, ils ne sont pas faciles à tester, c'est un signe qu'ils pourraient bénéficier d'une refactorisation.
Vous divisez le code en fonctions pour la même raison que vous le feriez pour C/C++, python, Perl, Ruby ou tout autre code de langage de programmation. La raison la plus profonde est l'abstraction - vous encapsulez des tâches de niveau inférieur en primitives (fonctions) de niveau supérieur afin que vous n'ayez pas à vous soucier de la façon dont les choses sont faites. En même temps, le code devient plus lisible (et maintenable), et la logique du programme devient plus claire.
Cependant, en regardant votre code, je trouve assez étrange d'avoir une fonction pour déclarer des variables; cela me fait vraiment lever un sourcil.
Bien que je sois totalement d'accord avec la réutilisabilité , lisibilité , et embrassant délicatement le les patrons mais il y a un autre avantage des fonctions dans bash : portée variable . Comme LDP montre :
#!/bin/bash
# ex62.sh: Global and local variables inside a function.
func ()
{
local loc_var=23 # Declared as local variable.
echo # Uses the 'local' builtin.
echo "\"loc_var\" in function = $loc_var"
global_var=999 # Not declared as local.
# Therefore, defaults to global.
echo "\"global_var\" in function = $global_var"
}
func
# Now, to see if local variable "loc_var" exists outside the function.
echo
echo "\"loc_var\" outside function = $loc_var"
# $loc_var outside function =
# No, $loc_var not visible globally.
echo "\"global_var\" outside function = $global_var"
# $global_var outside function = 999
# $global_var is visible globally.
echo
exit 0
# In contrast to C, a Bash variable declared inside a function
#+ is local ONLY if declared as such.
Je ne vois pas cela très souvent dans les scripts Shell du monde réel, mais cela semble être une bonne idée pour des scripts plus complexes. Réduire cohésion aide à éviter les bogues où vous détruisez une variable attendue dans une autre partie du code.
La réutilisation signifie souvent la création d'une bibliothèque commune de fonctions et source
ing cette bibliothèque dans tous vos scripts. Cela ne les aidera pas à fonctionner plus rapidement, mais cela vous aidera à les écrire plus rapidement.
Une raison complètement différente de celles déjà données dans d'autres réponses: une raison pour laquelle cette technique est parfois utilisée, où la seule instruction de non-fonction-définition au niveau supérieur est un appel à main
, est de s'assurer que le script ne fait rien accidentellement si le script est tronqué. Le script peut être tronqué s'il est acheminé du processus A vers le processus B (le shell), et le processus A se termine pour une raison quelconque avant d'avoir fini d'écrire le script entier. Cela est particulièrement susceptible de se produire si le processus A récupère le script à partir d'une ressource distante. Alors que pour des raisons de sécurité ce n'est pas une bonne idée, c'est quelque chose qui est fait, et certains scripts ont été modifiés pour anticiper le problème.
Un processus nécessite une séquence. La plupart des tâches sont séquentielles. Cela n'a aucun sens de jouer avec la commande.
Mais la super grande chose à propos de la programmation - qui inclut les scripts - est le test. Test, test, test. De quels scripts de test disposez-vous actuellement pour valider l'exactitude de vos scripts?
Votre patron essaie de vous guider, d'être un gamin de script à un programmeur. C'est une bonne direction à prendre. Les gens qui viennent après vous vous aimeront.
MAIS. Rappelez-vous toujours vos racines orientées processus. S'il est logique de classer les fonctions dans l'ordre dans lequel elles sont généralement exécutées, faites-le, au moins comme première passe.
Plus tard, vous constaterez que certaines de vos fonctions gèrent les entrées, d'autres les sorties, d'autres le traitement, d'autres la modélisation des données et d'autres la manipulation des données, il peut donc être judicieux de regrouper des méthodes similaires, peut-être même de les déplacer dans des fichiers séparés .
Plus tard encore, vous vous rendrez peut-être compte que vous avez maintenant écrit des bibliothèques de petites fonctions d'assistance que vous utilisez dans de nombreux scripts.
Quelques truismes pertinents sur la programmation:
Les commentaires commencent comme un espace pour ne pas être en mesure d'exprimer clairement vos idées dans le code *, et s'aggravent (ou se trompent) avec le changement. Par conséquent, si possible, exprimez les concepts, les structures, le raisonnement, la sémantique, le flux, la gestion des erreurs et tout autre élément pertinent pour la compréhension du code en tant que code.
Cela dit, les fonctions Bash ont des problèmes introuvables dans la plupart des langues:
local
entraîne la pollution de l'espace de noms global.local foo="$(bar)"
entraîne perte le code de sortie de bar
."$@"
signifie dans différents contextes.* Je suis désolé si cela choque, mais après avoir utilisé les commentaires pendant quelques années et développé sans eux ** pendant plus d'années, il est assez clair qui est supérieur.
** L'utilisation de commentaires pour la licence, la documentation API et autres est toujours nécessaire.
Les commentaires et l'espacement ne peuvent pas se rapprocher de la lisibilité des fonctions, comme je vais le démontrer. Sans fonctions, vous ne pouvez pas voir la forêt pour les arbres - de gros problèmes se cachent parmi de nombreuses lignes de détails. En d'autres termes, les gens ne peuvent pas se concentrer simultanément sur les petits détails et sur la vue d'ensemble. Cela pourrait ne pas être évident dans un court script; tant qu'il reste court, il peut être suffisamment lisible. Le logiciel devient plus gros, cependant, pas plus petit, et il fait certainement partie de l'ensemble du système logiciel de votre entreprise, qui est certainement beaucoup plus grand, probablement des millions de lignes.
Considérez si je vous ai donné des instructions comme celle-ci:
Place your hands on your desk.
Tense your arm muscles.
Extend your knee and hip joints.
Relax your arms.
Move your arms backwards.
Move your left leg backwards.
Move your right leg backwards.
(continue for 10,000 more lines)
Au moment où vous avez atteint la moitié, voire 5%, vous auriez oublié quelles ont été les premières étapes. Vous ne pouviez pas repérer la plupart des problèmes, car vous ne pouviez pas voir la forêt pour les arbres. Comparer avec les fonctions:
stand_up();
walk_to(break_room);
pour(coffee);
walk_to(office);
C'est certainement beaucoup plus compréhensible, quel que soit le nombre de commentaires que vous pourriez mettre dans la version séquentielle ligne par ligne. Cela rend également loin plus probable que vous remarquerez que vous avez oublié de faire le café, et probablement oublié sit_down () à la fin. Lorsque votre esprit pense aux détails des expressions régulières grep et awk, vous ne pouvez pas penser à une vue d'ensemble - "et s'il n'y a pas de café fait"?
Les fonctions vous permettent principalement de voir la situation dans son ensemble et de remarquer que vous avez oublié de faire le café (ou que quelqu'un pourrait préférer le thé). À un autre moment, dans un état d'esprit différent, vous vous inquiétez de la mise en œuvre détaillée.
Il y a aussi d'autres avantages discutés dans d'autres réponses, bien sûr. Un autre avantage non clairement indiqué dans les autres réponses est que les fonctions offrent une garantie qui est importante pour prévenir et corriger les bogues. Si vous découvrez qu'une variable $ foo dans la fonction appropriée walk_to () était erronée, vous savez que vous n'avez qu'à regarder les 6 autres lignes de cette fonction pour trouver tout ce qui aurait pu être affecté par ce problème, et tout ce qui pourrait l'ont fait mal. Sans fonctions (correctes), tout et n'importe quoi dans tout le système pourrait être une cause de $ foo incorrect, et tout et tout pourrait être affecté par $ foo. Par conséquent, vous ne pouvez pas corriger $ foo en toute sécurité sans réexaminer chaque ligne du programme. Si $ foo est local à une fonction, vous pouvez garantir que toutes les modifications sont sûres et correctes en cochant uniquement cette fonction.
Il y a d'autres bonnes réponses qui éclairent les raisons techniques d'écrire modulairement un script, potentiellement long, développé en un environnement de travail, développé pour être utilisé par un groupe de personnes et pas seulement pour votre propre usage.
Je veux me concentrer sur une seule attente: dans un environnement de travail "le temps c'est de l'argent" . Ainsi, l'absence de bugs et les performances de votre code sont évaluées avec lisibilité , testabilité, maintenabilité, refactorabilité, réutilisabilité ...
Écrire en "modules" un code diminuera le temps de lecture nécessaire non seulement par le codeur lui-même, mais même le temps utilisé par les testeurs ou par le patron. De plus, notez que le temps d'un patron est généralement payé plus que le temps d'un codeur et que votre patron évaluera la qualité de votre travail.
De plus, écrire en indépendant "modules" un code (même un script bash) vous permettra de travailler en "Parallèle" avec d'autres composants de votre équipe raccourcissant le temps de production global et utilisant au mieux l'expertise du single, pour revoir ou réécrire une partie sans effet secondaire sur les autres, pour recycler le code que vous venez de écrit "tel quel" pour un autre programme/script, pour créer des bibliothèques (ou des bibliothèques d'extraits), pour réduire la taille globale et la probabilité d'erreurs associées, pour déboguer et tester minutieusement chaque partie ... et bien sûr, il organisera en section logique votre programme/script et améliorera sa lisibilité. Tout ce qui fera gagner du temps et donc de l'argent. L'inconvénient est que vous devez vous en tenir aux normes et commenter vos fonctions (ce que vous devez néanmoins faire dans un environnement de travail).
Adhérer à une norme ralentira votre travail au début mais accélérera le travail de tous les autres (et du vôtre aussi) par la suite. En effet, lorsque la collaboration se développe en nombre de personnes impliquées, cela devient un besoin inévitable. Ainsi, par exemple, même si je crois que les variables globales doivent être définies globalement et non dans une fonction, je peux comprendre un standard qui les inizialise dans une fonction nommée declare_variables()
appelé toujours dans la première ligne de la main()
une ...
Enfin, ne sous-estimez pas la possibilité dans les éditeurs de code source modernes d'afficher ou de masquer des routines séparées de manière sélective ( Pliage de code =). Cela gardera le code compact et concentrera l'utilisateur à gagner du temps.
Ci-dessus, vous pouvez voir comment c'est déplié seulement la fonction walk_into_bar()
. Même les autres ont une longueur de 1000 lignes chacune, vous pouvez toujours garder sous contrôle tout le code sur une seule page. Notez qu'il est replié même dans la section où vous allez déclarer/initialiser les variables.
Une autre raison souvent ignorée est l'analyse syntaxique de bash:
set -eu
echo "this shouldn't run"
{
echo "this shouldn't run either"
Ce script contient évidemment une erreur de syntaxe et bash ne devrait pas du tout l'exécuter, non? Faux.
~ $ bash t1.sh
this shouldn't run
t1.sh: line 7: syntax error: unexpected end of file
Si nous enveloppions le code dans une fonction, cela ne se produirait pas:
set -eu
main() {
echo "this shouldn't run"
{
echo "this shouldn't run either"
}
main
~ $ bash t1.sh
t1.sh: line 10: syntax error: unexpected end of file
Mis à part les raisons données dans d'autres réponses:
- Psychologie: Un programmeur dont la productivité est mesurée en lignes de code sera incité à écrire du code inutilement verbeux. Plus la gestion se concentre sur les lignes de code, plus le programmeur est incité à étendre son code avec une complexité inutile. Ceci n'est pas souhaitable car une complexité accrue peut entraîner une augmentation des coûts de maintenance et des efforts supplémentaires requis pour la correction des bogues.