Ce projet essaye de faciliter le développement et l'usage de script Bash, notamment en rassemblant quelques fonctions utilitaires.
Pour utiliser ce projet, il faut sourcer le fichier bash-utils.sh
pour ensuite appeler
la fonction utils:run
avec tous les arguments du script "$@"
:
#!/usr/bin/env bash
main() {
echo "main function"
test_function
}
test_function(){
test "main function"
}
source "./bash-utils.sh"
utils:run "$@"
Il faut alors que tout le code du script se trouve dans des fonctions,
sauf le code d'initailisation bien sûr (les 2 dernières lignes ci-dessus par ex),
et que le code qui doit être appelé par défaut soit dans une fonction main
.
Si ce script est dans un fichier my_script.sh
il est alors possible, de lancer la
fonction main
en lançant le script sans paramètre ou alors de lancer directement
la fonction test avec :
./my_script test_function
utils:run
lance donc la fonction passée en premier argument, si on veut passer
des paramètres à cette fonction, utils:run
passera les paramètres suivants, à partir
du deuxième donc, à la fonction :
./my_script test_function arg1 arg2
C'est très pratique pendant le développement pour ne tester qu'une partie du script, mais également pour proposer une interface en ligne de commande très simplement.
Il est également possible de lancer my_script.sh --help
pour avoir la listes des
fonctions du script, qui peuvent donc être appelées depuis la ligne de commande.
Dans ce cas, par défaut, les fonctions dont le nom commence par _
ne sont pas listées
ni accessibles depuis la ligne de commande.
La fonction utils:run
va appeler utils:init
qui va alors définir les options de bash qui
sont généralement considérées comme de bonnes pratiques : errexit
, nounset
, pipefail
.
Par défaut, la sortie standard est affichée avec la fonction utils:error
, c'est à dire, en rouge
en démarrant les lignes d'une croix ❌.
Il est également possible de tracer très simplement les commandes qui sont exécutées
par le script en définissant UTILS_DEBUG=TRACE
:
UTILS_DEBUG=TRACE ./my_script test_function
Chaque ligne exécutée sera alors affichée en indiquant le numéro de ligne et les valeurs des variables utilisées.
Avec UTILS_DEBUG=true
, il faudra appuyer sur une touche pour passer à l'instruction du script
suivante, c'est un mode "pas à pas". Avec UTILS_DEBUG=TERM
, c'est également le mode
"pas à pas" mais où le contrôle s'effectue depuis un autre terminal qui est automatiquement ouvert.
Cet ébauche de debugger pourrait être complété d'autres modes plus tard (ajout de
points d'arrets par exemple).
Les fonctions du script bash-utils.sh sont préfixées par utils:
:
- utils:color : print parameters 2... with $UTILS_PREFIX_COLOR at the beginning with color $1, except if UTILS_NO_COLOR=true, use UTILS_PRINTF_ENDLINE=\n by default
- utils:countdown
- utils:debug : print parameters in blue : 🐛 parameters
- utils:debugger
- utils:error : print parameters in red to stderr : ❌ parameters
- utils:exec : print parameter with blue background and execute parameters, print time if UTILS_PRINT_TIME=true
- utils:flock_exec : run <$2 ...> command with flock (mutex) on '/var/lock/$1.lock' file
- utils:get_param : same as 'utils:get_params' but return the first result only
- utils:get_params : print parameter value
$1 from "$ @", if $1 == '--' print last parameters that doesn't start with by '-' ---- ='e|error' return 'value' for '--error=value' or '-e=value' ---- accept '--error value' and '-e value' if ='e |error ' - utils:has_param
- utils:help : print the help of all functions (the declare help='...')
- utils:hr : print N horizontal line, N=1 by default, N is the first parameter
- utils:info : print parameters in green : ℹ️ parameters
- utils:init : init bash options: errexit, nounset, pipefail, xtrace if TRACE==true, trap _utils_print_stack_and_exit_code if UTILS_PRINT_STACK_ON_ERROR==true
- utils:list_colors
- utils:list_functions : list all functions of the parent script, set UTILS_HIDE_PRIVATE_FUNCTIONS!= true to list _* functions
- utils:log : print parameters in green : ℹ️ parameters
- utils:parse_params : set utils_params array from "$@" : --error=msg -a=123 -zer=5 opt1 'opt 2' -- file --opt3 →→ utils_params = [error]=msg ; [--]=opt1 / opt 2 / file / --opt3 ; [z]=5 ; [r]=5 ; [e]=5 ; [a]=123 (/ is \n here)
- utils:pipe : for each lines, execute parameters and append line
- utils:run : run utils:init and run the main function or the function $1, add color and use utils:pipe utils:error for stderr except if UTILS_PIPE_MAIN_STDERR!=true
- utils:stack : print current stack
- utils:warn : print parameters in orange to stderr : ️
⚠️ parameters
Use this environment variable to activate some features :
- UTILS_DEBUG=TRACE to print each line (with line number) before execution
- UTILS_DEBUG=true same as UTILS_DEBUG=TRACE but wait for a key to be pressed (UTILS_DEBUG=TERM to open a new terminal)
- UTILS_PRINT_STACK_ON_ERROR=true to print the stack on error
- TRACE=true to enable bash option 'xtrace'
Il y un script qui permet de supprimer les usages des fonctions de bash-utils.sh
si jamais l'on veut supprimer cette dépendance à ce projet : remove_bash-utils.sh
On peut ajouter les chemins vers les fichiers à traiter en paramètres :
./remove_bash-utils.sh my_script1.sh my_script2.sh
ou on peut tester une conversion :
< example.sh ./remove_bash-utils.sh > example_without_bash-utils.sh
Pour vérifier s'il reste des usages de fonctions utils:* :
grep -E "utils:[a-zA-Z0-9_]*" my_script.sh
⚠ Disclaimer : les pratiques indiquées ici sont considérées bonnes de mon point de vue, rien que dans les liens ci-dessous, les avis divergent quelques fois des miens et ne sont pas unanimes sur tous les sujets.
Ci-dessous, quelques bonnes pratiques acquises avec l'expérience mais surtout avec pas mal de lectures du web, entre autre :
- https://kvz.io/bash-best-practices.html
- https://bertvv.github.io/cheat-sheets/Bash.html
- https://github.com/progrium/bashstyle
- http://google.github.io/styleguide/shellguide.html
- https://google.github.io/styleguide/shellguide.html
- https://wiki.bash-hackers.org/scripting/obsolete
- https://jvns.ca/blog/2017/03/26/bash-quirks/
- https://sap1ens.com/blog/2017/07/01/bash-scripting-best-practices/
- https://blog.yossarian.net/2020/01/23/Anybody-can-write-good-bash-with-a-little-effort
- http://mywiki.wooledge.org/BashFAQ/031
Ajouter au début du script : set -o errexit
, toutes les commandes qui auront un code de sortie différent de 0 stopperont le déroulement du script.
Cette règle est très importante. Par exemple :
cd my_folder
rm -rf *
Sans l'option errexit
, ce script effacera tous les fichiers du dossier courant si my_folder
n'existe pas, alors qu'il s'arrêterait
en erreur sur le cd
s'il y avait eu un set -o errexit
avant.
Autre exemple :
#!/usr/bin/env bash
test_func() {
echo "→ test_func begin"
UNKNOWN_COMMAND_TO_PRODUCE_ERROR
echo "← test_func end"
}
echo '---------------------- begin ---------------------'
test_func
set -o errexit
echo '------------------- errexit on -------------------'
test_func
echo '----------------------- end ----------------------'
Va produire cette sortie :
---------------------- begin ---------------------
→ test_func begin
./test.sh: ligne 6: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
← test_func end
------------------- errexit on -------------------
→ test_func begin
./test.sh: ligne 6: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
Si l'on veut autoriser une commande à sortir en erreur, il faut ajouter || true
après cette dernière : my_func || true
:
#!/usr/bin/env bash
set -o errexit
test_func() {
echo "→ test_func begin"
UNKNOWN_COMMAND_TO_PRODUCE_ERROR || true
echo "← test_func end"
}
echo '------------------- 1 -------------------'
test_func
echo '------------------- 2 -------------------'
Va produire cette sortie :
------------------- 1 -------------------
→ test_func begin
./t.sh: ligne 7: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
← test_func end
------------------- 2 -------------------
set -o errexit
est l'équivalent plus long de set -e
. Privilégiez la version longue qui est plus explicite.
Ce problème fait que l'option errexit
n'est pas toujours considérée comme une bonne pratique ! à raison car le comportement
n'est pas du tout celui attendu !
Quelques liens sur ce sujet :
- http://mywiki.wooledge.org/BashFAQ/105 ← voir la partie
Exercises
en bas ! - https://stackoverflow.com/questions/29926013/exit-subshell-on-error ←
I think it is good reason to hate both set -e and shell scripting in general.
- https://david.rothlis.net/shell-set-e/ ←
Bash experts seem to agree: “never rely on set -e” (gnu.bash.bug, BashFAQ). The recommendation is to do your own error checking by stringing together a series of commands with “&&”
- https://fvue.nl/wiki/Bash:_Error_handling ←
Opinions differ about whether it's wise to use set -e, because of its seemingly non-intuitive problems...
- https://www.in-ulm.de/~mascheck/various/set-e/
cf le man de bash :
The shell does not exit if the command that fails is part of the command list immediately following
a while or until keyword, part of the test following the if or elif reserved words, part of any command
executed in a && or || list except the command following the final && or ||, any command in a pipeline
but the last, or if the command's return value is being inverted with !.
If a compound command other than a subshell returns a non-zero status because a command failed
while -e was being ignored, the shell does not exit.
A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment
and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above),
and may cause subshells to exit before executing all the commands in the subshell.
If a compound command or shell function executes in a context where -e is being ignored, none of the
commands executed within the compound command or function body will be affected by the -e setting,
even if -e is set and a command returns a failure status. If a compound command or shell function
sets -e while executing in a context where -e is ignored, that setting will not have any effect until the
compound command or the command containing the function call completes.
Donc, attention, errexit
ne marchera pas pour le code d'une fonction dont l'appel est utilisé dans :
while, until, if, elif , && , ||, !, and | (si pipefail est activé) :
- while cmd; ...
- until cmd; ...
- if cmd; ...
- elif cmd; ...
- cmd && ...
- cmd || ...
- ! cmd
- set -o pipefail; cmd | ...
Ça fait beaucoup de cas !
Par exemple :
#!/usr/bin/env bash
set -o errexit
test_func() {
echo "→ test_func begin" >&2 # stderr
UNKNOWN_COMMAND_TO_PRODUCE_ERROR
echo "← test_func end" >&2 # stderr
}
echo 'test_func || echo "err_||"'
test_func || echo "err_||"
echo '------------------- 1 -------------------'
echo 'if test_func ; then echo "err_if"; fi :'
if test_func ; then echo "err_if"; fi
echo '------------------- 2 -------------------'
echo 'out=$(test_func) # [inherit_errexit off]'
out=$(test_func)
echo "1 - exitcode=$?"
echo '------------------- 3 -------------------'
shopt -s inherit_errexit
echo 'out=$(test_func) # [inherit_errexit on]'
out=$(test_func)
echo "2 - exitcode=$?"
Va produire cette sortie :
test_func || echo "err_||"
→ test_func begin
./test.sh: ligne 7: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
← test_func end
------------------- 1 -------------------
if test_func ; then echo "err_if"; fi :
→ test_func begin
./test.sh: ligne 7: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
← test_func end
err_if
------------------- 2 -------------------
out=$(test_func) # [inherit_errexit off]
→ test_func begin
./test.sh: ligne 7: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
← test_func end
1 - exitcode=0
------------------- 3 -------------------
out=$(test_func) # [inherit_errexit on]
→ test_func begin
./test.sh: ligne 7: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
On voit bien que malgré le set -o errexit
, le echo "← test_func end"
est atteint dans les
2 premiers appels de test_func
, malgré le UNKNOWN_COMMAND_TO_PRODUCE_ERROR
(qui a un code de sortie de 127) !
On peut aussi voir, dans ce dernier exemple,que le shopt -s inherit_errexit
fait arrêter le script sur le out=$(test_func)
alors que sans cette option, le script continu.
Si l'option errexit
est activée, on ne peut pas obtenir le code de sortie de la précédente commande avec $?
, car
si ce code de sortie est différent de 0, le script s'arrete en erreur...
une astuce consiste alors à utiliser !
devant la commande, qui autorise la commande à échouer,
mais où on peut alors récupérer le code de sortie dans ${PIPESTATUS[0]}
:
#!/usr/bin/env bash
set -o errexit
(exit 0)
echo " (exit 0) → \$?=$? -- \${PIPESTATUS[0]}=${PIPESTATUS[0]}"
! (exit 0)
echo "! (exit 0) → \$?=$? -- \${PIPESTATUS[0]}=${PIPESTATUS[0]}"
! (exit 15)
echo "! (exit 15) → \$?=$? -- \${PIPESTATUS[0]}=${PIPESTATUS[0]}"
Va produire cette sortie :
(exit 0) → $?=0 -- ${PIPESTATUS[0]}=0
! (exit 0) → $?=1 -- ${PIPESTATUS[0]}=0
! (exit 15) → $?=0 -- ${PIPESTATUS[0]}=15
${PIPESTATUS[0]}
contient bien le code de sortie souhaité. Le fonctionnement de PIPESTATUS
est décrit plus bas.
Attention, les remarque du paragraphe cette options n'est pas active dans certains cas
s'appliquent aussi à ! commande
#!/usr/bin/env bash
set -e
false && true
echo "false && true → $? ← exit code of 'false'"
f1() { false && true; echo f1-end; }
f2() { false && true; }
f1 && true
echo "f1 && true → $? ← exit code of 'echo f1-end'"
f1
echo "f1 → $? ← exit code of 'echo f1-end'"
f2 && true
echo "f2 && true → $? ← exit code of 'false && true'"
echo "'f2' exit script with the exit code = 1"
f2
echo "f2 → $?"
Va produire :
false && true → 1 ← exit code of 'false'
f1-end
f1 && true → 0 ← exit code of 'echo f1-end'
f1-end
f1 → 0 ← exit code of 'echo f1-end'
f2 && true → 1 ← exit code of 'false && true'
'f2' exit script with the exit code = 1
Attention, les remarque du paragraphe cette options n'est pas active dans certains cas
s'appliquent aussi à commande && true
Un contournement possible pour exécuter une fonction et savoir si elle a échouée
tout en gardant l'option le principe de errexit
actif (TODO à compléter) :
#!/usr/bin/env bash
set -o errexit
shopt -s inherit_errexit
func_ko() {
(exit 3)
echo "← func_ko end ⚠️ ⚠️ ⚠️"
}
func_ok() {
echo "→ func_ok"
}
fake_exit_code_func() { # "exit code" is 0 if true exit code is 0, else 1
local ex=$("$@" >&2; echo 0) # FIXME : stdout stderr are merged
if [[ $ex = 0 ]]; then
echo 0
else
echo 1
fi
}
echo "--- if func_ko; then echo pb; fi :"
if func_ko; then echo ok-0; else echo ko-0; fi
echo "--- if [[ $(fake_exit_code_func func_ko) = 0 ]] :"
if [[ $(fake_exit_code_func func_ko) = 0 ]]; then echo ok-1; else echo ko-1; fi
echo "--- if [[ \$fake_exit_code = 0 ]] :"
fake_exit_code=$(fake_exit_code_func func_ko)
if [[ $fake_exit_code = 0 ]]; then echo ok-2; else echo ko-2; fi
echo "fake_exit_code_func func_ko = $(fake_exit_code_func func_ko)"
echo "fake_exit_code_func func_ok = $(fake_exit_code_func func_ok)"
← func_ko end
ne sera pas affiché avec fake_exit_code_func
, mais la sortie standard de fake_exit_code_func
sera 1
.
La sortie du script :
--- if func_ko; then echo pb; fi :
← func_ko end ⚠️ ⚠️ ⚠️
ok-0
--- if [[ 1 = 0 ]] :
← func_ko end ⚠️ ⚠️ ⚠️
ok-1
--- if [[ $fake_exit_code = 0 ]] :
ko-2
fake_exit_code_func func_ko = 1
→ func_ok
fake_exit_code_func func_ok = 0
#!/usr/bin/env bash
set -o errexit
shopt -s inherit_errexit
fake_exit_code_func() { # "exit code" is 0 if true exit code is 0, else 1
local ex=$("$@" >&2; echo 0) # FIXME : stdout stderr are merged
if [[ $ex = 0 ]]; then echo 0; else echo 1; fi
}
_errexit_disable_test() { false; true; }
check_errexit_disable() { echo $(fake_exit_code_func _errexit_disable_test); }
func_ok() {
local errexit_disable=$(check_errexit_disable)
echo "➡ func_ok begin ✅"
[[ $errexit_disable = 0 ]] && echo "😒 errexit disable 😒" && return 5 # "return→exit" to end script here
# check_errexit_disable
echo "👍 errexit enable 👍"
echo "✅ func_ok end ✅"
}
func_ko_without_check() {
echo "🤞 func_ko_without_check begin 🤞"
(echo "💥 make error 💥"; exit 3)
echo "🔥 func_ko_without_check end 🔥"
}
func_ko() {
echo "➡ func_ko begin ❌"
local errexit_disable=$(check_errexit_disable)
[[ $errexit_disable = 0 ]] && echo "😒 errexit disable 😒" && return 5 # replace return by exit to end script here
# check_errexit_disable
echo "👍 errexit enable 👍"
(echo "💥 make error 💥"; exit 3)
echo "⚠️ func_ko end ⚠️"
}
echo '$ func_ok || echo "⏩ exit code = $?"'
func_ok || echo "⏩ exit code = $?" # ← errexit disable 😒, because of ||
echo '$ func_ok'
func_ok
echo '$ func_ko_without_check || echo "⏩ exit code = $?"'
func_ko_without_check || echo "⏩ exit code = $?" # ← errexit disable 😒, because of ||
echo '$ func_ko || echo "⏩ exit code = $?"'
func_ko || echo "⏩ exit code = $?" # ← errexit disable 😒, because of ||
echo '$ func_ko'
func_ko # ← errexit enable 👍 end script on "(exit 3)"
echo "⇨ unreachable..."
donne :
$ func_ok || echo "exit code = $?"
➡ func_ok begin ✅
😒 errexit disable 😒
⏩ exit code = 5
$ func_ok
➡ func_ok begin ✅
👍 errexit enable 👍
✅ func_ok end ✅
$ func_ko_without_check || echo "exit code = $?"
🤞 func_ko_without_check begin 🤞
💥 make error 💥
🔥 func_ko_without_check end 🔥
$ func_ko || echo "exit code = $?"
➡ func_ko begin ❌
😒 errexit disable 😒
⏩ exit code = 5
$ func_ko
➡ func_ko begin ❌
👍 errexit enable 👍
💥 make error 💥
Ajouter dans le script : set -o nounset
pour que le script s'arrete en erreur si une variable non initialisée est utilisée.
C'est l'équivalent plus long de set -u
. Privilégiez la version longue qui est plus explicite.
Penser à ${my_var:-}
pour initialiser my_var à une valeur vide ou non définie (voir ci-dessous).
Utiliser ${var:-default value}
pour définir une valeur par défaut si la variable
n'est pas initialisée
set -o nounset
directory=${DIRECTORY:-}
file=${FILE:-foo}
Sans ça, l'usage de set -o nounset
arrêtera le script si une variable non initialisée est utilisée.
Ajouter dans le script : set -o pipefail
pour que le code d'erreur soit celui de la première
commande et non celle utilisée dans le pipe.
Pour :
exit 1 | exit 0
echo $?
Sans l'option pipefail
et le script continuera même si l'option errexit
est activée car le code de retour considéré sera celui de exit 0
,
alors qu'avec l'option pipefail
, le code de sortie sera 1 et le script s'arretera alors si errexit
est activé
(le echo
ne sera pas exécuté dans ce cas là car le script se sera arrêté sur le exit 1
).
#!/usr/bin/env bash
echo 'UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail off -- errexit off'
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true
echo "exitcode=$? - \$PIPESTATUS=$PIPESTATUS"
set -o pipefail
echo 'UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail on -- errexit off'
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true
echo "exitcode=$? - \$PIPESTATUS=$PIPESTATUS"
set +o pipefail
set -o errexit
echo 'UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail off -- errexit on'
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true
echo "exitcode=$? - \$PIPESTATUS=$PIPESTATUS"
set -o pipefail
echo 'UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail on -- errexit on'
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true
echo "exitcode=$? - \$PIPESTATUS=$PIPESTATUS"
Va produire cette sortie :
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail off -- errexit off
./test.sh: ligne 4: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
exitcode=0 - $PIPESTATUS=127
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail on -- errexit off
./test.sh: ligne 10: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
exitcode=127 - $PIPESTATUS=127
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail off -- errexit on
./test.sh: ligne 17: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
exitcode=0 - $PIPESTATUS=127
UNKNOWN_COMMAND_TO_PRODUCE_ERROR | true # pipefail on -- errexit on
./test.sh: ligne 23: UNKNOWN_COMMAND_TO_PRODUCE_ERROR : commande introuvable
à noter que la variable $PIPESTATUS
contient le code de retour de la commande à droite du pipe.
C'est même un tableau :
#!/usr/bin/env bash
exit 0 | exit 1 | exit 2 | exit 3
echo "exit 0 | exit 1 | exit 2 | exit 3 : exitcode=$? - \$PIPESTATUS=${PIPESTATUS[@]}"
exit 0 | exit 1
echo "exit 0 | exit 1 : exitcode=$? - \$PIPESTATUS=${PIPESTATUS[@]}"
exit 1 | exit 0
echo "exit 1 | exit 0 : exitcode=$? - \$PIPESTATUS=${PIPESTATUS[@]}"
echo '→ set -o pipefail'
set -o pipefail
exit 0 | exit 1 | exit 2 | exit 3
echo "exit 0 | exit 1 | exit 2 | exit 3 : exitcode=$? - \$PIPESTATUS=${PIPESTATUS[@]}"
exit 0 | exit 1
echo "exit 0 | exit 1 : exitcode=$? - \$PIPESTATUS=${PIPESTATUS[@]}"
exit 1 | exit 0
echo "exit 1 | exit 0 : exitcode=$? - \$PIPESTATUS=${PIPESTATUS[@]}"
Va produire cette sortie :
exit 0 | exit 1 | exit 2 | exit 3 : exitcode=3 - $PIPESTATUS=0 1 2 3
exit 0 | exit 1 : exitcode=1 - $PIPESTATUS=0 1
exit 1 | exit 0 : exitcode=0 - $PIPESTATUS=1 0
→ set -o pipefail
exit 0 | exit 1 | exit 2 | exit 3 : exitcode=3 - $PIPESTATUS=0 1 2 3
exit 0 | exit 1 : exitcode=1 - $PIPESTATUS=0 1
exit 1 | exit 0 : exitcode=1 - $PIPESTATUS=1 0
Permet de clarifier le code, exécuter une fonction en particulier, ...
Plus discutable ! Mais très pratique, cela évite pas mal de code de gestion
des paramètres et avec l'option set -o nounset
, on détecte facilement les
problèmes d'initialisation des variables.
Pour exécuter un script avec des variables, dans ce cas :
var1=value1 ./my_script.sh
my_func() {
echo "${var1}${var2}"
}
foo(){
var1="b"
var2="ar"
my_func
}
et il est possible de lancer un script avec var1=b var2=ar ./my_script.sh
.
L'équivalent avec des paramètres nommés est beaucoup plus longue :
my_func() {
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
--var1)
var1="$2"
shift
shift
;;
--var2)
var2="$2"
shift
shift
;;
*)
shift
;;
esac
done
echo "${var1}${var2}"
}
foo(){
my_func --var2 "ar" --var1 "b"
}
Si on se base sur l'ordre des paramètres :
my_func() {
var1="$1"
var2="$2"
echo "${var1}${var2}"
}
foo(){
my_func "ar" "b"
}
Ça redevient plus cours mais il faut gérer l'ordre des paramètres et c'est moins clair coté appelant (le param numéro X correspond à quoi, ...).
N'exécuter main seulement si le script est exécuté et pas sourcé :
if [[ $0 == "${BASH_SOURCE[0]}" ]]; then
main "${@}"
fi
"sourcé" signifie source ./my_script.sh
ou . ./my_script.sh
, qui revient à importer le code en
quelque sorte, il est exécuté en réalité, et ses fonctions sont alors dorénavant accessibles.
Les IDE récents, notamment Intellij, permettent d'écrire du code bash en détectant des erreurs potentielles et en formatant le code, comme n'importe quel langage.
shfmt et shellsheck sont inclus dans Intellij par exemple.
trap cleanup EXIT ERR
cette ligne déclenchera l'exécution de la fonction cleanup
lorsque le script se terminera.
Dans ce cas, pensez à bien propager le code de sortie (si c'est souhaité) :
cleanup() {
exitcode=$?
...
exit $exitcode
}
Bash ne propose pas de namespaces à proprement parler, mais on peut nommer les fonctions avec un préfixe commun pour identifier les fonctions d'un script ou d'une partie donnée.
Le séparateur peut être _
ou :
par exemple :
mysh:print(){ ... }
mysh:foo(){ ... }
mysh:bar(){ ... }
https://github.com/bats-core/bats-core
@test "addition using bc" {
result="$(echo 2+2 | bc)"
[ "$result" -eq 4 ]
}
curl --show-error ...
plutôt que curl -S ...
à nuancer
(
cd dir/
do_something
)
plutôt que
cd dir/
do_something
cd ..
Pour les fonctions qui ne sont pas destinées à être utilisées dans d'autres scripts,
On peut préfixer le nom de la fonction par _
et ne pas utiliser de préfixe de namespace :
_my_private_function() {
}
my_script:my_func() {
_my_private_function
}
Placer [[ ${TRACE:-0} != 1 ]] || set -o xtrace
dans le script pour activer l'affichage
des lignes de code exécutées facilement.
Pour lancer le script en mode debug : TRACE=1 ./my_script.sh
set -o xtrace
est la version longue de set -x
Par exemple, si on passe des paramètres au script :
if [[ $# == 0 ]]; then # if the script has no argument, run the main() function
main
else
"$@"
fi
Si on exécute ce script avec ./my_script.sh test_function arg1 arg2
,
ça ne lancera que la fonction test_function
avec les 2 paramètres.
C'est très pratique pour le dev, debug et les TUs entre autre, ou pour proposer des fonctionnalités facilement depuis la ligne de commande.
my_func ()
{
declare help="help message here";
...
}
eval "$(type my_func | grep 'declare help=')"
echo $help
Vous pouvez
GIT_TOPLEVEL=$(cd "${BASH_SOURCE[0]%/*}" && git rev-parse --show-toplevel)
Ensuite il est possible de se déplacer dans les dossiers du projet ou référencer des fichiers de façon absolu, c'est pratique car en cas de déplacement d script, les chemins restent valides.
lock_file=/var/lock/my_script.lock
(
echo "wait $lock_file"
flock -x 200
echo "→ got $lock_file"
do stuff
...
) 200>"$lock_file"
- écrire la version anglaise du README
- permettre l'ajout de l'horodatage aux lignes de stderr et ou stdout
- ajout de "local" dans les bonnes pratiques
- mettre à jour le script remove_bash-utils.sh
- traiter les TODO du code