Forum Programmation.shell shell : pipe et variable locale

Posté par (page perso) .
Tags : aucun
1
15
nov.
2009

Bonjour.


Mon problème est le suivant, j'aimerai faire une boucle sur le liste des fichiers de mon répertoire.
Mon code ressemble à ça:

#!/bin/sh
VAR=0
ls | while read line
do
VAR=$(($VAR +1))
done
echo $VAR


Mais vu qu'une pipe lance un nouveau processus, la ligne echo $VAR affiche systématiquement 0
J'ai trouvé une solution qui marche pour bash:

#!/bin/bash
VAR=0
while read line
do
VAR=$(($VAR +1))
done < <(ls)
echo $VAR

Mais cette solution ne marche pas avec dash[1]. J'ai cherché sur internet mais cette fois ci google n'était pas mon ami.

Si jamais vous avez une idée pour faire la même chose en dash, ce serait avec grand plaisir.
Merci d'avance.

[1]Debian_Almquist_shell
  • # J'oubliais...

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

    Bien évidement, il existe une solution en utilisant un fichier temporaire, mais j'aimerai éviter les fichiers temporaires autant que possible.
    • [^] # Re: J'oubliais...

      Posté par . Évalué à 1.

      En utilisant l'indication de fin de fichier ça fonctionne comme tu veux.


      #!/bin/dash

      VAR=0;

      while read line
      do
      var=$(($var + 1))
      done <<EOF
      $(ls)
      EOF

      echo $var
  • # Reprend les bases

    Posté par . Évalué à 4.

    Je ne sais pas où tu as appris le shell, mais reprend les bases :

    Boucluer sur des fichiers (ou même des patterns, on utilise le globbing du shell) :
    for line in *; do
    ...
    done


    Et si tu veux compter le nombre de lignes d'une commande, ce qui est à priori ce que tu veux faire :
    ls | wc -l

    Ça me semble un tout petit peu moins "tordu" que tes solutions ...
    • [^] # Re: Reprend les bases

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

      Je ne sais pas où tu as appris le shell [...]
      Tous seul. Pourquoi  ? ;)
      Mon exemple est effectivement mauvais, j'ai trop voulu le  simplifier.
      En fait mon code s'apparente plutôt à cela:
      
      #!/bin/sh
      VAR=0
      ./script.sh | while read line 
           do
             ...
             if [ ... ]; then 
                VAR=$(($VAR +1))
             fi
           done
      echo $VAR  
      
      Et bien évidement, ./script.sh affiche des lignes qui contiennent des espaces. Et quand il y a des espaces, on ne peut plus utiliser la boucle for
      • [^] # Re: Reprend les bases

        Posté par . Évalué à 0.

        Si tu veux lire ligne par ligne, le principe du for marche toujours :
        for line in `./script.sh`; do
        ...
        done


        Je te conseille d'aller lire un bon manuel de shell (j'ai pas de lien sous la main, désolé).
        • [^] # Re: Reprend les bases

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


          cantor@lenny:~$ ./toto
          première ligne
          seconde ligne

          cantor@lenny:~ $ for line in `./toto` ; do echo $line; done
          première
          ligne
          seconde
          ligne


          Et donc non... cela ne marche pas à cause des espaces...
          D'où le truc bizarre avec le

          ./script.sh | while read line; do
          ...
          done

          qui lui marche. Mais le problème c'est que les variables modifiés dans le while sont des variables locales (car la pipe crée un nouveau processus).
          Mais si tu as une solution plus élégante je suis prenant...
          • [^] # Re: Reprend les bases

            Posté par . Évalué à 2.

            Ouai, les espaces, la galère en shell ...
            Mais bon, si tu pouvais directement nous présenter le problème en entier au lieu de rajouter des petits détails "cachés" à chaque fois qui changent tout, ça aiderait.

            (genre je sens que ton traitement dans le if pourrait peut-être remplacer par un grep, etc)

            Là, la solution simple serait de changer script.sh pour qu'il renvoie des chaînes correctement échappées.

            Ou faire autre chose que du shell (encore plus du dash; c'est pour les traitements "simples"). Si tu es dans un environnement avec peu de ressources, j'ai déjà vu des gens utiliser awk par exemple (c'est le cas d'OpenWRT, qui n'a qu'un shell réduit, mais busybox fournit awk qui peut faire plein de chose quand on l'apprend)(oui, encore un nouveau langage).
            • [^] # Re: Reprend les bases

              Posté par . Évalué à 3.

              peut-être que c'est pour un script d'init ?

              Moi je trouve que c'est une bonne question, y'a plein de raisons qui poussent à utiliser un shell en particulier dans des circonstances particulières.

              Il ne va pas non plus passer plus de temps à poser la question et nous pondre un roman sur le contexte qu'il n'en a passé à écrire le script, alors que son problème n'est pas "réaliser telle opération", mais "contourner une limite des shell bourne", à savoir la création de sous-shell en cas de pipe... C'est typiquement le genre de question intelligente et dont les réponses peuvent servir à d'autres que j'aime voir dans les forums.
              • [^] # Re: Reprend les bases

                Posté par . Évalué à 2.

                Je comprend que des fois on ne veut pas indiquer tous les détails, mais vu la gueule du script, je me suis dit dans ma tête "c'est un débutant", et le script a l'air foireux, donc je l'ai dirigé autre part.
                Après, c'était aussi un peu sous-entendu que je n'avais pas vraiment de solution dans le sens qu'il voulait /o\ (mais bon, un sous-shell créé pour un pipe c'est dans la définition même du fonctionnement d'unix, alors si tu veux contourner ça .... ça va être coton).
                • [^] # Re: Reprend les bases

                  Posté par . Évalué à 3.

                  un sous-shell créé pour un pipe c'est dans la définition même du fonctionnement d'unix, alors si tu veux contourner ça .... ça va être coton).

                  Pas nécessairement. Le problème vient plus du while des shells bourne que du pipe. avec ksh ou zsh, il n'y a qu'un sous-shell dans ce cas là, et pas un par itération. Du coup, pas de problème. Pas de chance, POSIX a choisi la façon d'agir de sh...

                  Cela dit, pour un contournement sans fichier temporaire, cf mon post plus bas :-)
        • [^] # Re: Reprend les bases

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

          • [^] # Re: Reprend les bases

            Posté par . Évalué à 2.

            sauf qu'il faut chercher les tutos de scripting SH car c'est le shell utilisé dans son script


            #!/bin/sh
            VAR=0
            ./script.sh | while read line
            do
            ...
            if [ ... ]; then
            VAR=$(($VAR +1))
            fi
            done
            echo $VAR

            • [^] # Re: Reprend les bases

              Posté par . Évalué à 2.

              de plus, il a bien précisé que la solution bash ne lui convenait pas, puisqu'il voulait utiliser dash. Ce qu'il lui faut c'est une solution POSIX, qui marche quel que soit le shell.

              À ce compte là on peut aussi dire que ce problème ne se poserait pas avec ksh ou zsh, puisqu'ils ne créent pas de sous-shell pour les pipes.

              Franchement la question est pertinente, le coup du pipe et des variables locales c'est tordu.
              Répondre un STFW (ou un RTFM) c'est un peu rude.
      • [^] # Re: Reprend les bases

        Posté par . Évalué à 7.

        meuh si on peut... Il suffit de positionner l'IFS à newline (voire à null dans les cas extreme, mais encore faut-il que ton programme en entrée soit prévu pour).


        BACKUPIFS=$IFS
        IFS=$'\n'

        for trucmuche in $($script); do
        ...
        done
        IFS=$BACKUPIFS


        (note : si tu n'a pas modifié l'IFS avant, unset IFS est plus simple que la sauvegarde / restauration).

        Bon cela dit, c'est pas toujours une bonne solution, parce que si tu as plus de résultats que ton shell ne prend d'arguments, ça foire... Le while read est donc une solution bien plus élégante.

        Dans ce cas là, tu peux utiliser un pipe nommé : ça complexifie un peu, parce qu'il faut le créer (et donc le supprimer, comme un fichier temporaire). L'avantage, c'est que c'est juste un fichier device en mode caractère et donc qu'il n'y a pas d'écriture sur disque.


        #/bin/dash

        script="/bin/ls"

        TMPDIR=$(mktemp -dt script.XXXXXX)
        FIFO=$TMPDIR/script.pipe

        trap "rm -rf $TMPDIR" EXIT

        mkfifo $FIFO


        $script > $FIFO &

        COUNT=0
        while read trucmuche; do
        COUNT=$(($COUNT +1))
        done < $FIFO

        echo $COUNT
        • [^] # Re: Reprend les bases

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

          Merci beaucoup,
          la pipe nommé semble être la meilleur solution.
          • [^] # Re: Reprend les bases

            Posté par . Évalué à 1.

            Overkill, cf. mon message sur '< <( )'.

            La gent féminine, pas la "gente", pas de "e" ! La gent féminine ! Et ça se prononce comme "gens". Pas "jante".

            • [^] # Re: Reprend les bases

              Posté par . Évalué à 2.

              Si t'avais bien lu le tout début de cette page, t'aurais vu qu'il a essayé, mais que ça ne marche pas en dash ...
      • [^] # Re: Reprend les bases

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

        Je ne sais pas si ca te convient (genre ca ne marchera pas si tu affiches d'autres choses)
        #!/bin/sh
        ./script.sh | while read line 
             do
               ...
               if [ ... ]; then 
                  echo + 1
               fi
             done | xargs expr 1 
        
      • [^] # Re: Reprend les bases

        Posté par . Évalué à 4.

        awk me semble bien adapté à ce cas non ?

        script.sh |awk '{ if (...) { VAR+=1} } END {print VAR }'

        tu peux même faire mieux : script.sh |awk ' /regexp/ {VAR+=1} } END {print VAR }'
        • [^] # Re: Reprend les bases

          Posté par . Évalué à 2.

          Effectivement, je l'avais évoqué plus mais n'avais pas réfléchi au code : c'est concis et sympa.
        • [^] # Re: Reprend les bases

          Posté par . Évalué à 2.

          si le reste du script est en shell c'est un peu dommage de faire du awk (quoique ça paraîtra parfois plus simple que de faire un pipe nommé, mais ça fait un interpréteur de plus).

          Par contre si l'essentiel du script se résumé à ça, c'est en effet une solution plus élégante que le pipe nommé (perso je suis une loutre en awk alors j'y pense rarement au dela du one-liner jetable).
          • [^] # Re: Reprend les bases

            Posté par . Évalué à 3.

            Je ne pense pas qu'un awk prenne beaucoup de place, et ça permet d'économiser une autre commande qui tôt ou tard devra arriver. D'autre part, il m'est souvent arrivé de remplacer des scripts shell par des scripts awk : gain de temps considérable à l'exécution car le shell passe son temps à "faire faire", ce qui correspond à autant de fork+exec que de commande lancée.

            Je suis même presque convaincu que tu peux remplacer avantageusement ton script.sh + le awk que j'ai fourni ci-dessus par un awk bien senti.
          • [^] # Re: Reprend les bases

            Posté par . Évalué à 2.

            (perso je suis une loutre en awk alors j'y pense rarement au dela du one-liner jetable).

            Awk, c'est bon, mangez-en ... J'ai même réussi à lui faire résoudre les grilles de Sudoku ... :)
  • # < <( script )

    Posté par . Évalué à 1.

    Avec bash tu dois pouvoir faire :

    while read line
    do
    VAR=$(($VAR +1))
    done < <( ls )

    La gent féminine, pas la "gente", pas de "e" ! La gent féminine ! Et ça se prononce comme "gens". Pas "jante".

    • [^] # Re: < <( script )

      Posté par . Évalué à 5.

      Mmm...
      Faut lire les questions jusqu'au bout avant de répondre...
      • [^] # Re: < <( script )

        Posté par . Évalué à 3.

        Bon sang, en effet, je retourne me coucher... désolé.

        La gent féminine, pas la "gente", pas de "e" ! La gent féminine ! Et ça se prononce comme "gens". Pas "jante".

Suivre le flux des commentaires

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