Journal astuce bash: de l'usage du elif

Posté par .
12
21
juin
2011

Bash est un langage dont la coolitude ne cesse de me surprendre. J'en apprends littéralement tous les jours.

Il existe des tas de sites où vous trouverez des tas d'astuces sur l'utilisation avancée du Bourne-Again SHell. Par exemple sur l'utilisation des redirections, des substitutions de processus, les tableaux associatifs, les documents-en-place (here-document), chaines-en-place (here-string), les tuyaux nommés, les nombreux types d'expansion, etc, etc.

Mais récemment y'a une astuce toute bête que je n'ai jamais vraiment lu nul part sur une structure des plus banales: le if then elif else fi.

Il faut savoir qu'après un if ou un elif, on n'est pas obligé de ne mettre qu'une commande. On peut mettre une liste de commandes.

Ainsi, une commande telle que:

echo "I am going to run foo"
if foo
then echo "I did run foo"
fi

peut être remplacée par:

if
    echo "I am going to run foo"
    foo
then echo "I did run foo"
fi

« Oui, et alors ? », me direz-vous. Et bien, le truc c'est que grâce à ça on n'a pas à imbriquer des if.

Ainsi:

echo "I am going to run foo"
if foo
then
    echo "I did run foo"
    echo "now I am going to run bar"
    if bar
    then "I did run bar"
    fi
fi

peut être écrit dans une seule structure if:

if
    echo "I am going to run foo"
    ! foo
then echo "oops"
elif
    echo "I did run foo"
    echo "now I am going to run bar"
    bar
then echo "I did run bar"
fi

Et là on comprends que cette idée est très utile pour gérer les erreurs:

if ! foo
then error "something went wrong trying to run foo"
elif
    echo "I did run foo"
    echo "Now I'm going to run bar"
    ! bar
then error "something went wrong trying to run bar"
elif
    echo "I did run bar"
    echo "Now I'm going to run jiz"
    ! jiz
then error "something went wrong trying to run jiz"
else echo "I could go on like this forever"
fi

AMHA ce style de programmation présente parmi ses avantages, celui d'inciter le programmeur à développer du code robuste, car il doit penser à gérer les erreurs d'abords, pour chaque étape du programme.

En fait il me semble que presque sans exagérer on peut dire qu'on devrait pouvoir écrire n'importe quel programme bash dans une seule grande structure if then elif ... else fi

  • # .

    Posté par (page perso) . Évalué à 10.

    c'est pas plus simple de faire juste

    foo || error "oulala"
    bar || error "oulalalala"

    ?

    • [^] # Re: .

      Posté par . Évalué à 2.

      Ben ici le "error" n'est qu'un exemple mais dans le cas général ça peut être n'importe quel liste de commande. Et surtout il n'est pas sensé quitter le programme, mais uniquement la boucle if.

      Donc dans ton exemple il faudrait un moyen de spécifier qu'après l'exécution de l'error qui suit l'échec de fou, le programme continue après la ligne bar. Bref c'est pas du tout le même programme.

      • [^] # Re: .

        Posté par (page perso) . Évalué à 2.

        Et surtout il n'est pas sensé quitter le programme, mais uniquement la boucle if

        Ton programme est donc beaucoup trop long! :)

        Ensuite tu peux te contenter d'un return dans le terme de droite de || si tu ne veux pas quitter le programme ou enfermer ta procédure susceptible de contenir des erreurs dans un sous-shell (grâce à ( et )). Si on programme un outil simple on peut se contenter de exit et éventuellement enregistrer un callback pour le pseudo-signal EXIT.

  • # Lisibilité

    Posté par . Évalué à 9.

    Intéressant, mais cela ne nuit pas à la lisibilité? Sur un script d'une dizaine de ligne ok, mais sur de gros script?

    • [^] # Re: Lisibilité

      Posté par . Évalué à 2.

      Au contraire, depuis que je programme comme ça, je trouve mon code beaucoup plus lisible.

      • [^] # Re: Lisibilité

        Posté par . Évalué à 5.

        Hahem...comment dire? j'aimerai pas avoir à maintenir tes scripts...

        Le prend pas mal, mais j'ai déjà repris des scripts faisant plusieurs centaines de lignes et honnêtement, avec des gestions de condition comme ça, je me prend pas la tête : je réécrit tout!

  • # Complètement imbitable...

    Posté par (page perso) . Évalué à 10.

    Apprends à faire des fonctions, des returns, et utiliser des codes d'erreur. Et youpi, pas besoin d'imbriquer plusieurs niveaux de if. Et ça marche pour d'autres langages.

    Parce que là c'est à la limite de l'obfuscation de code... Fausse bonne idée. Je te conseille de lire la bible du bash: le Advanced Bash-Scripting Guide. Une traduction française plus ancienne du même guide est aussi disponible: Guide avancé d'écriture des scripts Bash.

    • [^] # Re: Complètement imbitable...

      Posté par . Évalué à -2.

      Il est absurde de créer une fonction pour une portion de code qu'on n'utilisera qu'une fois. Parce qu'ici foo et bar ne sont que des exemples mais ça pourrait être n'importe quel test, genre [[ "$1" = "dothis" ]] ou que-sais-je-encore.

      Je persiste donc à penser que ce style d'utilisation des structures if est une bonne idée.

      • [^] # Re: Complètement imbitable...

        Posté par (page perso) . Évalué à 6.

        Je ne te dis pas de faire 15 fonctions, une pour chacun de tes tests, mais une où tu fais tes traitements, pour pouvoir faire un return. Ou alors tu peux faire des exit, mais en général on évite de mettre 15000 exit dans un programme. Code non testé, toussa...

        E_FOO=1
        E_BAR=2
        E_JIZ=3
        
        result=`foo`
        if ! $result ; then 
            error "something went wrong trying to run foo"
            return E_FOO
        fi
        
        echo "I did run foo"
        echo "Now I'm going to run bar"
        
        result=`bar`
        if ! $result ; then 
            error "something went wrong trying to run bar"
            return E_BAR
        fi
        
        echo "I did run bar"
        echo "Now I'm going to run jiz"
        
        result=`jiz`
        if ! $result ; then
            error "something went wrong trying to run jiz"
            return E_JIZ
        fi
        
        echo "I could go on like this forever"
        
        • [^] # Re: Complètement imbitable...

          Posté par . Évalué à -2.

          retourner le code d'erreur ne ne change pas grand chose à l'affaire.

          erreur() { 
              err="$1"; shift
              echo "$@" 2>&1
              : do whatever you want with "$err"
          }
          
          if
              echo "I am going to run foo"
              foo ; err="$?"
              (($err))
          then error "$err" "couldn't run foo" 
          elif
              echo "I did run foo"
              : do whatever you want here
              echo "Now I am going to run bar"
              bar ; err="$?"
              (($err))
          then error "$err" "couldn't run bar"
          else echo "I could do this all day long"
          fi    
          
          • [^] # Re: Complètement imbitable...

            Posté par . Évalué à 5.

            Le problème de ta structure, c'est qu'elle est contre-intuitive : tu codes un programme qui marche s'il a planté tout le long de son exécution.

            De plus, tu as àmha mal compris ce qu'on t'as indiqué. Ce que tu peux faire c'est :

            erreur () {
                local err="$1"; shift 1
                 echo "$*" >&2
                 exit $err
            }
            ( 
              foo || erreur 1 "couldn't run foo"
              echo "I did run foo"
            
              echo "Now I am going to run bar"
              bar || erreur 2 "couldn't run bar"
              echo "I did run bar"
            
              echo "I could do this all day long"
            )
            
            case $? in
               #  traiter l'erreur..
            esac
            

            Comme ça la séquence est stoppée à la première erreur mais le code erreur peut encore être exploité...

        • [^] # Re: Complètement imbitable...

          Posté par (page perso) . Évalué à 1.

          Code non testé, toussa...

          Heureusement ;-)

          if ! $result ; then

          Tu voulais dire $?, je suppose.

      • [^] # Re: Complètement imbitable...

        Posté par . Évalué à 10.

        Il est absurde de créer une fonction pour une portion de code qu'on n'utilisera qu'une fois.

        argh !
        j'ai trop vu de fonction de plusieurs milliers de lignes dans ma vie pour ne pas réagir. La ré-utilisabilité n'est pas la seule raison qui pousse à structurer du code en fonction. La maintenabilité (mot valise qui cache pleins de qualités du code) est un argument au moins aussi fort (voire plus fort selon moi) pour découper son code en fonction, même si c'est des fonctions de 3 lignes et même si elles ne sont utilisées qu'une seule fois.

      • [^] # Re: Complètement imbitable...

        Posté par . Évalué à 8.

        Il est absurde de créer une fonction pour une portion de code qu'on n'utilisera qu'une fois.

        Permet moi de m'inscrire en faux. Meme pour du code lineaire, l'utilisation de fonctions est parfois tres interessant.

        Exemple:

        void configure( void ) {
          /* 125 lignes de code pour configurer le bouzin */
        }
        void utilise( void ) {
          /* 200 lignes pour utiliser le bouzin */
        }
        void libere( void ) {
          /* 80 lignes pour liberer le bouzin */
        }
        
        int tout_le_boulot( void ) {
          configure();
          utilise();
          libere();
        }
        

        C'est tout de meme plus lisible qu'une fonctions avec 305 lignes de code.

        Ensuite, tu utilises des prototypes pour les 3 sous-fonctions, tu mets leurs corps en bas, et tout de suite, la structure macroscopique du programme apparait : en fait, tu configures, tu utilises et tu liberes le bouzin. Ca devient lumineux ! ;-)

        Ensuite, que ces trois fonctions soient compliquees, c'est pas grave, l'idee generale est la, visible du premier coup d'oeil.

        Maintenant, si tu veux optimiser, et eviter trois appels de fonction (qui seront de toute facon invisibles par rapport au temps necessaire a executer le reste du code), tu declare tes fonction inline, et/ou static.. Et tout bon compilateur qui se respecte saura optimiser ca sans soucis.

        Bien entendu, dans l'evolution n+27 de ton logiciel (que ton client te demanderas, style c'est juste une evolution mineure ! ;-] ), tu seras bien content d'avoir factorise tout ca, parce que tu auras non plus un, ni meme deux, mais 42 bouzins a configurer, utiliser plusieurs fois, et ensuite liberer !

        Hop,
        Moi.

        PS. oui, je sais : les accents... J'ai juste oublier de reconfigurer compose sur la nouvelle becane... M'en vais faire ca de suite...

        • [^] # Re: Complètement imbitable...

          Posté par . Évalué à 6.

          C'est tout de meme plus lisible qu'une fonctions avec 3405 lignes de code.

          Les mouches devraient faire attention a leur cul, à toute heure...

    • [^] # Re: Complètement imbitable...

      Posté par . Évalué à 2.

      Parce que là c'est à la limite de l'obfuscation de code...

      Y'a une autre raison de programmer en *sh qu'une volonté d'obfuscation et d'introduire des bugs improbables ? Attention j'ai dit programmer pas écrire 5 lignes de wrapper.

      • [^] # Re: Complètement imbitable...

        Posté par . Évalué à 3.

        Oui, vieux serveur UNIX de production sur lesquels on a pas le droit d'installer quoi que ce soit... Bienvenu dans le monde bancaire...

        • [^] # Re: Complètement imbitable...

          Posté par . Évalué à 3.

          Il y a pas un interpréteur perl ou python et ils laissent un shell complet ?

          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Complètement imbitable...

        Posté par . Évalué à 2.

        Quels bugs improbables ? c'est comme dans toute programmation : à partir du moment où tu structures logiquement ton programme en petits traitements facilement testables isolément, ça tient la route. Après, effectivement, le shell a ses propres subtilités qui font qu'on ne pose pas à la base les problèmes de la même manière qu'en Perl ou qu'en Python. Mais une fois que tu as compris ça, pour les gros scripts il n'y a de mon point de vue pas plus de problèmes que dans les langages précédemment cités.

  • # Shell, illisible

    Posté par (page perso) . Évalué à 10.

    C'est du Shell, pas du Bash. Je veux dire, ce n'est pas spécifique à Bash, et c'est donc utilisable dans tous les shells de type sh, en particulier dash.

    Ceci étant, je vous déconseille fortement cette façon de coder, parce qu'elle est difficile à lire, or du code est fait pour être :

    1. lu ;
    2. correct ;
    3. exécuté ;
    4. parfois, rapide et efficace.
    • [^] # Re: Shell, illisible

      Posté par . Évalué à 1.

      2,5: élégant/joli/toussa :)

      • [^] # Re: Shell, illisible

        Posté par . Évalué à 3.

        On pourrait arrêter d'écrire "toussa" s'il vous plaît ?

        • [^] # Re: Shell, illisible

          Posté par . Évalué à 6.

          Je ne vois pas ce que tu reproches au passé simple du verbe tousser à la troisième personne du singulier. ;)

        • [^] # Re: Shell, illisible

          Posté par (page perso) . Évalué à 7.

          • élégant/joli/patin couffin (jamais compris d'où ça venait)
          • élégant/joli/plop (mouai)
          • élégant/joli/etc (mouai)
          • élégant/joli/autre (mouai)
          • élégant/joli/tout ça (nope)

          Ben finalement toussa say bien.
          Arrête donc de faire ton maichant !

          • [^] # Re: Shell, illisible

            Posté par . Évalué à 3.

            élégant/joli/*/

            C'est tout de même plus simple est plus agréable.

            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

            • [^] # Re: Shell, illisible

              Posté par . Évalué à 10.

              tu veux dire plus simple, plus agréable, toussa ? :)

            • [^] # Re: Shell, illisible

              Posté par (page perso) . Évalué à 3.

              $ sed élégant/joli/*/
              sed: -e expression n°1, caractère 1: commande inconnue: `é'
              
              • [^] # Re: Shell, illisible

                Posté par . Évalué à 2.

                J'ai pas fais gaffe markdown m'a bouffé la moitié.
                élégant/joli/**/*

                Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Shell, illisible

        Posté par (page perso) . Évalué à 2.

        Ça rentre dans le point 1, ça. Puisque du code est fait pour être lu, il faut avant tout l'écrire pour faciliter cette lecture, et l'élégance est un bon moyen pour cela.

  • # set -eu + trap

    Posté par (page perso) . Évalué à 2.

    On m'a récemment fait découvrir set -eu. Associé à trap, je trouve ça assez pratique pour la gestion d'erreurs : il n'y a pas besoin de la mettre explicitement dans le code, ce qui le rend moins bloated et plus lisible. Le problème c'est pour les subshells par contre : http://fvue.nl/wiki/Bash:_Error_handling#Caveat_3:_.60Exit_on_error.27_not_exitting_command_substition_on_error

    pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

  • # au temps pour moi

    Posté par . Évalué à 3.

    Bon c'est vrai que la structure

    foo || erreur
    

    est la plus adaptée dans 99% des cas. J'avoue que je l'avais juste oubliée.

    Je viens de réécrire mon code avec ce type de gestion d'erreur, et c'est incontestablement plus lisible.

    Le fait de pouvoir écrire une liste de commandes après un if ou un elif n'est pas si astucieux, donc.

    désolé.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.