Compte tenu de la récente multitude d'articles couvrant les aspects fondamentaux de Bash (énumérés à la fin de cet article), il est inévitable qu'un de vos nouveaux collègues en lance un dans le cloud. Au fur et à mesure, les prochaines étapes logiques sont :
- Vérifier que quelque chose d'absolument critique dépend du bon fonctionnement du script "cloud".
- Vérifiez que l'auteur original du script a complètement oublié comment cela fonctionne réellement.
- Vérifiez que le nouvel administrateur est chargé de le modifier fondamentalement sans aucune validation.
Dans cet article, j'assiste tous les administrateurs et vous aide à éviter toutes les erreurs ci-dessus. À son tour, le résultat conduit à une gestion plus heureuse et, espérons-le, à notre emploi continu.
Comment épeler "script bash"
Afin de devenir éclairé (et pour votre amour de $DEITY), vérifiez votre code dans un outil de gestion de code source (SCM). Même pendant que vous apprenez, utilisez un référentiel local comme terrain de jeu. Non seulement cette pratique vous permet de consigner vos efforts au fil du temps, mais elle vous permet également de corriger facilement les erreurs. Il existe de nombreux articles formidables sur la prise en main de git
que je recommande vivement.
Une note rapide sur l'utilisation et l'apprentissage de Bash, car ce langage de script apporte un ensemble unique de -ismes et de préférences stylistiques de l'auteur :dès que vous repérez quelque chose qui vous semble nouveau (syntaxe, style ou construction de langage), recherchez-le immédiatement . Passez votre temps à comprendre le nouvel élément de la page de manuel (premier choix) ou du Guide de script avancé de Bash (les deux sont accessibles hors ligne). Faire cela vous ralentira au début, mais au fil du temps, cette pratique vous aidera à approfondir vos connaissances pour savoir où trouver des réponses.
Écrire des nuggets Bash réutilisables sous forme de bibliothèques
Les scripts d'automatisation sont mieux écrits en adoptant la philosophie Unix :de nombreux petits outils qui ne font qu'une seule chose. Cela signifie que vous réussirez beaucoup mieux à écrire de petits scripts et bibliothèques spécialisés qu'avec un seul "évier de cuisine" géant. Bien que, certes, j'ai écrit et maintenu quelques mastodontes (parfois, ils servent un but).
Les scripts d'automatisation doivent souvent être compréhensibles et maintenables par plusieurs auteurs. Avec de nombreux petits scripts circulant (et suivis dans le contrôle de version), vous aurez rapidement besoin de partager une référence aux valeurs des noms, des versions, des chemins ou des URL. L'écriture de ces éléments communs dans des bibliothèques fournit également un espace mental supplémentaire aux mainteneurs pour apprécier la documentation en ligne. De plus, cela rend l'utilisation des tests unitaires presque triviale (nous aborderons ce sujet à la fin).
Pratiquons dès le départ une bonne hygiène du code en créant un dépôt "play" local. Dans votre nouveau référentiel, créez un emplacement pour contenir nos scripts et nos fichiers de bibliothèque. J'aime m'en tenir aux normes FHS bien comprises pour plus de simplicité. Créez les répertoires ./bin/
et ./lib/
à la racine du référentiel. Dans un projet d'automatisation plus vaste, j'utiliserais toujours ces noms, mais ils pourraient être profondément enfouis (par exemple, sous un scripts
ou tools
sous-répertoire).
Parler de chemins m'amène à un excellent sujet pour une première bibliothèque. Nous avons besoin d'un moyen pour nos futurs composants de référencer des éléments structurels et des valeurs de haut niveau. À l'aide de votre éditeur préféré, créez le fichier ./lib/anchors.sh
et ajoutez le contenu ci-dessous :
# A Library of fundamental values
# Intended for use by other scripts, not to be executed directly.
# Set non-'false' by nearly every CI system in existence.
CI="${CI:-false}" # true: _unlikely_ human-presence at the controls.
[[ $CI == "false" ]] || CI='true' # Err on the side of automation
# Absolute realpath anchors for important directory tree roots.
LIB_PATH=$(realpath $(dirname "${BASH_SOURCE[0]}"))
REPO_PATH=$(realpath "$LIB_PATH/../") # Specific to THIS repository
SCRIPT_PATH=$(realpath "$(dirname $0)")
Le fichier commence par deux lignes vides, et le premier commentaire explique pourquoi. La bibliothèque ne devrait pas être défini comme exécutable (malgré le nom se terminant par .sh, indiquant son type). Si la bibliothèque a été exécutée directement, il est possible que le shell de l'utilisateur disparaisse (ou pire). Désactivation de l'exécution directe (chmod -x ./lib/anchors.sh
) est le premier niveau de protection des administrateurs débutants. Le commentaire au début du fichier est le deuxième niveau.
Par convention, les commentaires doivent toujours décrire le pourquoi (pas le quoi ) des déclarations qui suivent. Un lecteur peut simplement lire la déclaration pour comprendre ce qu'elle fait, mais il ne peut pas deviner de manière fiable ce que l'auteur pensait à ce moment-là. Cependant, avant d'aller plus loin, je dois détailler un problème qui prend souvent les gens au dépourvu avec Bash.
Les valeurs par défaut de Bash fournissent une chaîne vide lors de la référence à une variable indéfinie. Le CI
variable (ou quelque chose d'analogue dans votre automatisation) est destinée à indiquer l'absence probable d'un humain. Malheureusement, pour les maîtres robots, les humains devront probablement exécuter le script manuellement au moins une fois. À ce stade, il est probable qu'ils oublient de définir une valeur pour CI
avant d'appuyer sur Entrée.
Nous devons donc définir une valeur par défaut pour la valeur et nous assurer qu'elle est toujours vraie ou fausse. L'exemple de code de bibliothèque ci-dessus montre à la fois comment tester si une chaîne est vide et comment forcer la chaîne à contenir l'une d'une paire de valeurs. La façon dont j'ai lu le premier groupe d'instructions dans anchors.sh
est :
Définir 'CI
' à l'avenir suite à :
- Examen de la valeur précédente de
CI
(il peut être indéfini, donc une chaîne vide). - Le '
:-
' partie signifie :- Si la valeur était une chaîne vide, représentez la chaîne '
false
' à la place. - Si la valeur n'était pas une chaîne vide, utilisez ce qu'elle était (y compris '
DaRtH
VaDeR
').
- Si la valeur était une chaîne vide, représentez la chaîne '
Testez la chose à l'intérieur de '[[
' et ' ]]
' :
- Si la nouvelle valeur de '
CI
' est égal à la chaîne littérale "false
", lancer le code de sortie0
(ce qui signifie succès ou vérité). - Sinon, lancez le code de sortie
1
(signifiant échec ou non-vérité)
Si le test s'est terminé avec 0
, passez à la ligne suivante, ou (le ' ||
' partie), supposez qu'un administrateur novice définit CI=YES!PLEASE
ou un ensemble machine parfaite CI=true
. Définissez immédiatement la valeur de 'CI
' à la chaîne littérale 'true
'; parce que des machines parfaites c'est mieux, ne vous y trompez pas.
Pour le reste de cette bibliothèque, les valeurs de chemin d'ancrage sont presque toujours utiles dans les scripts exécutés dans l'automatisation à partir d'un référentiel. Lorsqu'il est utilisé dans un projet plus important, vous devrez ajuster l'emplacement relatif du répertoire de la bibliothèque par rapport à la racine du référentiel. Sinon, je partirai comprendre ces affirmations, comme un exercice de recherche pour le lecteur (faites-le maintenant, ça vous aidera plus tard).
Utilisation des bibliothèques Bash dans les scripts Bash
Pour charger une bibliothèque, utilisez le source
commande intégrée. Cette commande n'est pas fantaisiste. Donnez-lui l'emplacement d'un fichier, puis il lit et exécute le contenu sur-le-champ, ce qui signifie que le contexte d'exécution du code de la bibliothèque sera en fait le script qui source
d-le.
Pour éviter qu'une trop grande partie de votre cerveau ne s'écoule de vos oreilles, voici un simple ./bin/example.sh
script à illustrer :
#!/bin/bash
LIB_PATH="$PWD/$(dirname $0)/../lib/anchors.sh"
echo "Before loading: $LIB_PATH"
set -ax
cd /var/tmp
source $LIB_PATH
echo "After loading: $(export -p | grep ' LIB_PATH=')"
Vous remarquerez peut-être immédiatement que le script a modifié le contexte d'exécution avant de charger la bibliothèque. Il définit également LIB_PATH
localement et le pointe vers un fichier (ce qui prête à confusion, au lieu d'un répertoire) avec un chemin relatif (à des fins d'illustration).
Allez-y, exécutez ce script et examinez la sortie. Notez que toutes les opérations de la bibliothèque anchors.sh
exécuté dans /var/tmp/
répertoire et exporte automatiquement ses définitions. L'ancienne définition de LIB_PATH
a été écrasé et exporté par le a
dans le set -ax
. Ce fait est visible dans la sortie du declare -x
provenant de l'export
commande. Espérons que la sortie de débogage (le x
dans le set -ax
) est compréhensible.
Lors d'un débogage comme celui-ci, Bash imprime toutes les valeurs intermédiaires lors de l'analyse de chaque ligne. J'ai inclus ce script ici pour montrer pourquoi vous ne voudriez jamais set -ax
ou changer de répertoire à l'aide des commandes du niveau supérieur d'une bibliothèque. N'oubliez pas que les instructions de la bibliothèque sont évaluées au moment du chargement dans le script. Donc, changer l'environnement dans une bibliothèque entraîne des effets secondaires d'exécution dans n'importe quel script utilisé source
pour le charger. Des effets secondaires comme celui-ci sont garantis pour rendre au moins un administrateur système complètement fou. On ne sait jamais, cet administrateur pourrait être moi, alors ne le faites pas.
Comme exemple pratique, considérons une bibliothèque imaginaire qui définit une fonction à l'aide d'une variable d'environnement nom d'utilisateur/mot de passe pour accéder à un service distant. Si la bibliothèque a fait un set -ax
de haut niveau avant cette fonction, chaque fois qu'elle est chargée, la sortie de débogage inclura l'affichage de ces variables, éclaboussant vos secrets partout pour que tout le monde puisse les voir. Pire encore, il sera difficile (du point de vue d'un script d'appel) pour un collègue débutant de désactiver la sortie sans crier sur son clavier.
En conclusion, la clé ici est de rester conscient que les bibliothèques "se produisent" dans le contexte de leur appelant. Ce facteur est également la raison pour laquelle l'exemple anchors.sh
peut utiliser $0
(le chemin du script exécutable et le nom du fichier), mais le chemin vers la bibliothèque elle-même n'est disponible que via le "magic" '${BASH_SOURCE[0]}'
(élément de tableau). Ce facteur peut être déroutant au début, mais vous devriez essayer de rester discipliné. Évitez les commandes larges et étendues dans les bibliothèques. Lorsque vous le ferez, tous les nouveaux administrateurs embauchés insisteront pour payer vos donuts.
Écrire des tests unitaires pour les bibliothèques
L'écriture de tests unitaires peut sembler une tâche intimidante jusqu'à ce que vous réalisiez qu'une couverture parfaite est généralement une perte de temps. Cependant, c'est une bonne habitude de toujours utiliser et mettre à jour votre code de test lorsque vous touchez votre code de bibliothèque. L'objectif de l'écriture de test est de prendre en charge les cas d'utilisation les plus courants et les plus évidents, puis de passer à autre chose. Ne prêtez pas beaucoup d'attention aux cas d'angle ou moins que les utilisations permanentes. Je suggère également de concentrer initialement vos efforts de test de bibliothèque sur le niveau des tests unitaires plutôt que sur les tests d'intégration.
Prenons un autre exemple :le script exécutable ./lib/test-anchors.sh
:
#!/bin/bash
# Unit-tests for library script in the current directory
# Also verifies test script is derived from library filename
TEST_FILENAME=$(basename $0) # prefix-replace needs this in a variable
SUBJ_FILENAME="${TEST_FILENAME#test-}"; unset TEST_FILENAME
TEST_DIR=$(dirname $0)/
ANY_FAILED=0
# Print text after executing command, set ANY_FAILED non-zero on failure
# usage: test_cmd "description" <command> [arg...]
test_cmd() {
local text="${1:-no test text given}"
shift
if ! "$@"; then
echo "fail - $text"; ANY_FAILED=1;
else
echo "pass - $text"
fi
}
test_paths() {
source $TEST_DIR/$SUBJ_FILENAME
test_cmd "Library $SUBJ_FILENAME is not executable" \
test ! -x "$SCRIPT_PATH/$SUBJ_FILENAME"
test_cmd "Unit-test and library in same directory" \
test "$LIB_PATH" == "$SCRIPT_PATH"
for path_var in LIB_PATH REPO_PATH SCRIPT_PATH; do
test_cmd "\$$path_var is defined and non-empty: ${!path_var}" \
test -n "${!path_var}"
test_cmd "\$$path_var referrs to existing directory" \
test -d "${!path_var}"
done
}
# CI must only/always be either 'true' or 'false'.
# Usage: test_ci <initial value> <expected value>
test_ci() {
local prev_CI="$CI" # original value restored at the end
CI="$1"
source $TEST_DIR/$SUBJ_FILENAME
test_cmd "Library $SUBJ_FILENAME loaded from $TEST_DIR" \
test "$?" -eq 0
test_cmd "\$CI='$1' becomes 'true' or 'false'" \
test "$CI" = "true" -o "$CI" = "false"
test_cmd "\$CI value '$2' was expected" \
test "$CI" = "$2"
CI="$prev_CI"
}
test_paths
test_ci "" "false"
test_ci "$RANDOM" "true"
test_ci "FoObAr" "true"
test_ci "false" "false"
test_ci "true" "true"
# Always run all tests and report, exit non-zero if any failed
test_cmd "All tests passed" \
test "$ANY_FAILED" -eq 0
[[ "$CI" == "false" ]] || exit $ANY_FAILED # useful to automation
exit(0)
La raison pour laquelle j'ai mis ce script dans ./lib
(par opposition à ./bin
) est à la fois par commodité et parce que les tests ne doivent jamais s'appuyer sur l'utilisation du code qu'ils vérifient. Étant donné que ce test doit vérifier les chemins, il est plus facile de le placer dans le même chemin que la bibliothèque. Sinon, cette approche est une question de préférence personnelle. N'hésitez pas à exécuter le test maintenant, car cela pourrait vous aider à comprendre le code.
Conclusion
Cet article ne représente en aucun cas l'intégralité de l'utilisation de Bash dans l'automatisation. Cependant, j'ai essayé d'inculquer des connaissances de base et des recommandations qui (si elles sont suivies) vous faciliteront sans aucun doute la vie. Ensuite, même lorsque les choses deviennent difficiles, une bonne compréhension de l'importance du contexte d'exécution sera utile.
Enfin, les scripts dans ou pour l'automatisation peuvent être impitoyables pour les erreurs. Avoir même des tests unitaires de base en place pour vos bibliothèques renforcera la confiance et aidera la prochaine personne à venir (ce qui pourrait être vous après cinq ans d'oubli). Vous pouvez trouver tous les exemples de code utilisés dans cet article en ligne ici.
Vous souhaitez approfondir vos bases de Bash ? Consulter :
- Comment programmer avec Bash :opérateurs logiques et extensions du shell
- 5 façons d'améliorer vos scripts Bash
- Démarrer avec les scripts shell
- 10 commandes Linux de base que vous devez connaître
- Trucs et astuces pour les variables d'environnement Linux
[ Voulez-vous essayer Red Hat Enterprise Linux ? Télécharge le maintenant gratuitement. ]