jersou / bash-utils

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bash-utils

Ce projet essaye de faciliter le développement et l'usage de script Bash, notamment en rassemblant quelques fonctions utilitaires.

test

Principes

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 utilitaires

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

Les variables d'environnement

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'

Supprimer les usages de bash-utils.sh

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

Bonnes pratiques d'écriture de scripts bash

⚠ 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 :

Arrêter l'exécution dès la première erreur

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.

MAIS ATTENTION, cette options n'est pas active dans certains cas

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 :

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) !

inherit_errexit

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.

pour récupérer le code de sortie malgré le errexit

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...

avec !

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

avec && true

#!/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

pour tester une fonction en gardant le principe de errexit actif

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

tester si le principe de errexit est actif

#!/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 💥

Détecter les variables non initialisées

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).

Initialiser les variables qui ont le droit d'être non initialisées

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.

Détecter les erreurs lorsque l'on utilise les pipes : cmd1 | cmd2

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

PIPESTATUS

à 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

Mettre tout le code dans des fonctions

Permet de clarifier le code, exécuter une fonction en particulier, ...

Utiliser une fonction main pour le code principal

Utiliser my_func() { ... } plutôt que function my_func { ... }

Utiliser des variables plutôt que des paramètres

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, ...).

Détecter si le script est sourcé ou exécuté

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.

Utiliser snake_case pour les noms des variables et des fonctions

#!/usr/bin/env bash plutôt que #!/bin/bash

Utilise Shellcheck pour détecter les erreurs

Utilise shfmt pour formater le code

Utiliser un IDE pour écrire des scripts

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.

Utiliser trap pour déclencher du code à la fin du script ou sur une erreur

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
}

Utiliser des namespaces

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(){ ... }

Faire des tests unitaires avec bats

https://github.com/bats-core/bats-core

@test "addition using bc" {
  result="$(echo 2+2 | bc)"
  [ "$result" -eq 4 ]
}

utiliser version longue des paramètres

curl --show-error ... plutôt que curl -S ...

Utiliser ${var} plutôt que $var

à nuancer

Naviguer dans les dossiers depuis un subshell

(
 cd dir/
 do_something
)

plutôt que

cd dir/
do_something
cd ..

Fonctions privées

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
}

Astuces

Activer l'affichage des commandes exécutées si TRACE=1

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

Permettre l'exécution d'une fonction en particulier

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.

Pour documenter l'aide d'une fonction

my_func ()
{
    declare help="help message here";
    ...
}
eval "$(type my_func | grep 'declare help=')"
echo $help

Pour les projets versionnés avec git

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.

Utiliser flock pour exécuter une partie du code une seule fois en même temps

lock_file=/var/lock/my_script.lock
(
    echo "wait $lock_file"
    flock -x 200
    echo "→ got $lock_file"
    do stuff
    ...
) 200>"$lock_file"

TODO list

  • é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

About


Languages

Language:Shell 90.2%Language:Roff 9.0%Language:Makefile 0.5%Language:Dockerfile 0.4%