Forum Programmation.shell Quoter une variable contenant des arguments de ligne de commande

Posté par . Licence CC by-sa.
Tags :
0
16
juin
2013

J'ai un script bash qui fait une sauvegarde incrémentale dans un tar.gz. Une des variables de configuration doit contenir un nom de répertoires avec caractère générique (glob), et je n'arrive pas à le faire passer

## Configuration de sauvegarde
##############################
repertoires="/home/jgo
/var/spool/mail/jgo"
liste_exclusion="--exclude \"/home/jgo/divers/*\" --exclude \"/home/jgo/.recoll/xapiandb/*.DB\""
ciblelocale=/var/backup/

## autres options
#################

timestampsauvegarde=/home/jgo/timestamp-sauvegarde
nouvelledate=`date "+%s"`
nomclair=${ciblelocale}backup_${nouvelledate}.tar.gz
temp=/tmp/
liste=${temp}/sauvegarde-liste

## fin de la configuration
##########################

# Les fichiers à archiver
rm -f $liste
touch $liste
for i in $repertoires ; do
  find $i -type f -newer $timestampsauvegarde >> ${liste}
done

# marche
tar cfz test1-${nomclair} --files-from $liste --exclude "/home/jgo/divers/*" --exclude "/home/jgo/.recoll/xapiandb/*.DB"

# ne marche pas (tar fonctionne mais les fichiers ne sont pas exclus)
tar cfz test2-${nomclair} --files-from $liste ${liste_exclusion}

# décommenter quand ça marche
#touch ${timestampsauvegarde}

Le problème est dans la variable $liste_exclusion. Si je copie le contenu de cette variable sur la ligne du tar, tout marche bien (ligne marhe). Mais pour faire plus propre, je voudrais mettre ce contenu dans une variable au début du fichier.

J'ai essayé diverses manières de mettre les guillemets : simples, doubles, le mode ansi C quoting de bash, j'ai essayé d'échapper les quotes (\") et aussi les astérisques, et la présence ou l'absence guillemets autour de la variable à la ligne du tar (ou pas).

La version plus haut est un exemple. Si je mets tar en pause (ctrl-Z) pendant son exécution pour avoir le temps de taper ps -fe, j'obtiens la ligne de commande telle qu'exécutée :
tar cfz /tmp/backup.tar.gz --files-from /tmp//sauvegarde-liste --exclude "/home/jgo/divers/*" --exclude "/home/jgo/.recoll/xapiandb/*.DB"

Si je copie directement les arguments sur la bonne ligne, j'ai la même chose sans les guillemets autour des répertoires (et ça marche). Si je mets les guillemets dans la variable ça ne marche pas parce que tar ne comprend pas les noms de fichiers entourés de guillemets, et si je ne mets pas les guillemets autour des noms de répertoires alors bash fait l'expansion de * (avec guillemets simples ou doubles dans la définition de la variable), ce qui met tar dans la confusion.

J'ai bien une autre solution, utiliser echo dans le script pour écrire dans un fichier (echo #!/bin/bash >> /tmp/script, etc.) avec la syntaxe qui marche, faire chmod 755 sur ce script et l'exécuter, mais c'est tout aussi moche que de mettre les paramètres sur la ligne de commande.

Quelqu'un sait-il mettre les guillemets corrects dans la variable $liste_exclusion ? Même la page Advanced Bash-Scripting Guide contient des cas que personne ne comprend (il y a deux « Why? » dans les exemples), alors j'ai peu d'espoir de comprendre mieux moi-même.

  • # avec un tableau ?

    Posté par . Évalué à 3.

    Bonjour,

    liste_exclusion=( --exclude "/home/jgo/divers/*" --exclude "/home/jgo/.recoll/xapiandb/*.DB" )
    tar cfz "test2-$nomclair" --files-from "$liste" "${liste_exclusion[@]}"
    
    

    ±

  • # eval expr...

    Posté par . Évalué à 3.

    Eval te permet d'avoir une expansion en 2 temps.
    Le problème est que ton ${liste_exclusion} est passé à tar en temps qu'un seul argument.
    Eval va forcer son expansion avant d'appeller tar. Il te faudra aussi rajouter une paire de quote autour de tes globs pour qu'ils ne soient pas substitué parl le eval…

    • [^] # Re: eval expr...

      Posté par . Évalué à 2. Dernière modification le 17/06/13 à 00:35.

      Ou sinon plus simple, tu peux aussi faire:
      tar ... `echo $liste`.
      Je suppose que la solution du tableau fonctionne aussi même si moins portable.

      • [^] # Re: eval expr...

        Posté par . Évalué à 2.

        quand on écrit un script en bash, la portabilité…
        soit on a un shebang sh, qui dit que le script doit être portable,
        soit on a un shebang bash, et on tire le meilleur parti des extensions que propose bash.

        • [^] # Re: eval expr...

          Posté par . Évalué à 2.

          Et le jour on n'a pas de bash sous la main, on ne sait plus comment faire ;-)
          Note: je ne pense pas qu'une façon soit meilleure qu'une autre, je cite simplement d'autres moyens d'y arriver, disons pour la culture.

          • [^] # Re: eval expr...

            Posté par . Évalué à 2. Dernière modification le 17/06/13 à 11:21.

            Merci pour les suggestions.

            • La solution du tableau fonctionne telle quelle.
            • Pour la solution echo $liste, je tombe dans les mêmes contorsions pour savoir quels guillemets utiliser avoir l'expansion correcte. Ça doit être faisable en réfléchissant bien.
            • Je ne sais pas très bien utiliser eval pour obtenir directement la solution, mais merci pour la suggestion, c'est l'outil de débogage qui me manquait pour savoir quelle ligne de commande va être passée au shell (jusqu'à présent je faisait ctrl-z puis ps -fe, loin d'être idéal).
            • [^] # Re: eval expr...

              Posté par . Évalué à 2. Dernière modification le 17/06/13 à 13:26.

              Je voulais en avoir le coeur net.
              1) peu-importe le format de liste utilisé, il y a moyen d'arriver avec eval
              2) on ne sait pas utiliser les backquotes sans eval; le problème c'est que le shell fait l'expansion glob après celle des backquotes. Le eval dans ce cas sert à retirer les backslashs :-/

              showarg.sh (chmod +x):

              #! /bin/sh
              
              while [ -n "$1" ]
              do
                echo "arg: $1"
                shift 1
              done
              
              

              test.sh:

              mkdir -p /tmp/t/A /tmp/t/B; touch /tmp/t/A/a
              liste1="--exclude \"/tmp/t/A/*\" --exclude \"/tmp/t/B*\""
              liste2="--exclude '/tmp/t/A/*' --exclude '/tmp/t/B*'"
              liste3="--exclude /tmp/t/A/* --exclude /tmp/t/B*"
              
              set -x
              
              echo  OK
              eval ./showargs.sh $liste1
              eval ./showargs.sh $liste2
              eval ./showargs.sh `echo "$liste3" | sed 's/\*/\\\*/g' `
              
              echo  NOK: quote dans le argv
              ./showargs.sh $liste1
              ./showargs.sh $liste2
              ./showargs.sh `echo $liste1`
              ./showargs.sh `echo $liste2`
              ./showargs.sh `echo "$liste1"`
              ./showargs.sh `echo "$liste2"`
              
              echo  NOK: expansion glob
              ./showargs.sh $liste3
              eval ./showargs.sh $liste3
              ./showargs.sh `echo $liste3`
              ./showargs.sh `echo "$liste3"`
              ./showargs.sh `eval echo $liste1`
              ./showargs.sh `eval echo $liste2`
              ./showargs.sh `eval echo $liste3`
              
              echo NOK: backslashs
              ./showargs.sh `echo "$liste3" | sed 's/\*/\\\*/g' `
              
              
              • [^] # Re: eval expr...

                Posté par . Évalué à 2.

                Si je reprends une des formules qui marche chez toi :

                liste_exclusion="--exclude \"/home/jgo/divers/*\""
                eval tar cfz /tmp/backup.tar.gz /home/jgo --exclude $liste_exclusion
                
                

                tar: Suppression de « / » au début des noms des membres
                tar: /home/jgo/divers/* : stat impossible: Aucun fichier ou dossier de ce type
                tar: Arrêt avec code d'échec à cause des erreurs précédentes

                • [^] # Re: eval expr...

                  Posté par . Évalué à 1.

                  Dans ton exemple, il devrait y avoir deux --exclude !? Peux tu lancer ta commande avec set -x activé ?

                • [^] # Re: eval expr...

                  Posté par . Évalué à 2.

                  Si je reprends une des formules qui marche chez toi :

                  liste_exclusion="--exclude \"/home/jgo/divers/*\""
                  eval tar cfz /tmp/backup.tar.gz /home/jgo --exclude $liste_exclusion
                  

                  tar: Suppression de « / » au début des noms des membres
                  tar: /home/jgo/divers/* : stat impossible: Aucun fichier ou dossier de ce type
                  tar: Arrêt avec code d'échec à cause des erreurs précédentes

                  c'est marqué dedans et c'est logique que tu ais cette erreur /home/jgo/divers/* stat impossible

                  car de la maniere dont tu passes les options, tu lui demandes de faire un tar du dossier $liste_exclusion avec un exclude vide.

                  en mettant un = entre exclude et $liste_exclusion, et en mettant ton dossier source à la fin de la ligne
                  ta ligne devrait etre

                  liste_exclusion="--exclude \"/home/jgo/divers/*\""
                  eval tar cfz /tmp/backup.tar.gz --exclude=$liste_exclusion /home/jgo
                  
                  
                  • [^] # Re: eval expr...

                    Posté par . Évalué à 2.

                    Cet ordre d'options et l'absence de = marche très bien quand tu tapes les options sans utiliser de variables. Le problème survient uniquement quand je cherche à passer plusieurs arguments --exclude dans une variable, avec guillemets et astérisques inclus. D'ailleurs la page de man ne documente pas de = et les exemples plus haut qui marchent (mon script du début ainsi que les réponses de NBaH et benja) ne l'utilisent pas non plus. Quant à ta syntaxe, j'ai essayé sur un exemple simple : (sans astérisque, puisque je m'intéresse ici uniquement à la syntaxe tar)

                    (Au lieu de faire le backup de /home à chaque fois, je choisis un sous-répertoire contenant peu de fichiers.)

                    #!/bin/bash
                    liste_exclusion="~/Notes/Calendar"
                    # OK
                    eval tar cfz /tmp/backup.tar.gz ~/Notes --exclude "~/Notes/Calendar"
                    eval tar cfz /tmp/backup.tar.gz ~/Notes --exclude "$liste_exclusion"
                    
                    # NOK, l'exclusion n'est pas faite
                    eval tar cfz /tmp/backup.tar.gz ~/Notes --exclude="~/Notes/Calendar"
                    eval tar cfz /tmp/backup.tar.gz --exclude="~/Notes/Calendar" ~/Notes
                    eval tar cfz /tmp/backup.tar.gz --exclude="$liste_exclusion" ~/Notes
                    
                    

                    J'ai essayé avec et sans guillemets dans chacun des appels à eval, le résultat est le même.

                    • [^] # Re: eval expr...

                      Posté par . Évalué à 2.

                      ok pour l'ordre des options ou le =, en effet, ce n'est pas ca qui compte

                      ton script marche chez moi à une seule condition, virer les guillemets :
                      - lors de la creation de la variable liste_exclusion
                      - ET ne pas les mettre sur la ligne de commande

                      ce qui donne le script suivant

                      #!/bin/bash
                      exclusion=~/test/d3
                      tar zcvf /tmp/test.tgz ~/test --exclude $exclusion
                      
                      

                      mon arbre contient

                      :/tmp$tree ~/test/
                      /home/user/test/
                      ├── d1
                      │   ├── f11
                      │   ├── f12
                      │   ├── f13
                      │   └── f14
                      ├── d2
                      │   ├── f21
                      │   ├── f22
                      │   ├── f23
                      │   └── f24
                      └── d3
                          ├── d31
                          │   ├── f311
                          │   └── f312
                          ├── d32
                          └── f31
                      
                      

                      et le script donne bien une archive ne contenant pas ~/test/d3

                      /tmp/test.sh 
                      tar: Suppression de « / » au début des noms des membres
                      /home/user/test/
                      /home/user/test/d2/
                      /home/user/test/d2/f24
                      /home/user/test/d2/f23
                      /home/user/test/d2/f21
                      /home/user/test/d2/f22
                      /home/user/test/d1/
                      /home/user/test/d1/f12
                      /home/user/test/d1/f11
                      /home/user/test/d1/f14
                      /home/user/test/d1/f13
                      
                      
                      • [^] # Re: eval expr...

                        Posté par . Évalué à 2.

                        On est d'accord, sauf que dans mon exemple réel (le script que j'ai posté en premier), j'ai besoin d'utiliser plusieurs arguments dans la variable $exclusion. Avec plusieurs arguments séparés par des espaces tu es obligé de mettre les guillemets (sans compter les astérisques dont j'ai besoin pour *.DB). C'est là que mes problèmes ont commencé.

                        • [^] # Re: eval expr...

                          Posté par . Évalué à 2.

                          les cas qui fonctionnent chez moi

                          #!/bin/bash
                          exclusion="--exclude=test/d1 --exclude=d2/*.DB"
                          eval tar zcvf /tmp/test.tgz ~/test $exclusion
                          
                          
                          #!/bin/bash
                          exclusion="--exclude=test/d1 --exclude=/home/user/test/d2/*.DB"
                          eval tar zcvf /tmp/test.tgz ~/test $exclusion
                          
                          
                          #!/bin/bash
                          exclusion="--exclude=test/d1 --exclude=*.DB"
                          eval tar zcvf /tmp/test.tgz ~/test $exclusion
                          
                          

                          en fait cela fonctionne bien en mettant un bout du chemin, ou le chemin absolu
                          mais pas avec un relatif comme ~/test/d2/*.DB

  • # dans un fichier

    Posté par . Évalué à 3.

    mettre tes exlusions dans un fichier et utiliser l'option de tar pour prendre les exclusions à partir de ce fichier

    -X, --exclude-from FILE
    exclude patterns listed in FILE

    le fichier contiendrait alors

    /home/jgo/divers
    /home/jgo/.recoll/xapiandb/*.DB

    • [^] # Re: dans un fichier

      Posté par . Évalué à 2.

      Merci pour cette solution. Maintenir la liste dans un fichier séparé peut être un avantage (ou un désavantage) selon le type d'utilisation du script.

      D'après mes essais, pour fonctionner le fichier doit contenir un glob comme

      /home/jgo/divers/*

      au lieu du seul nom de répertoire (que le nom de répertoire soit écrit ou pas avec le slash final).

      • [^] # Re: dans un fichier

        Posté par . Évalué à 2.

        marrant chez moi je crees une arborescence fictive

        :~$ cd /tmp
        :/tmp$ mkdir -p test/d1
        :/tmp$ mkdir -p test/d2
        :/tmp$ touch test/d1/f11
        :/tmp$ touch test/d1/f12
        :/tmp$ touch test/d1/f13
        :/tmp$ touch test/d2/f21
        :/tmp$ touch test/d2/f22
        :/tmp$ touch test/d2/f23

        puis j'en fais une archive avec une exclusion, sans meme preciser le chemin complet

        :/tmp$ tar zcvf test.tgz --exclude=d1 test/
        test/
        test/d2/
        test/d2/f23
        test/d2/f21
        test/d2/f22

        avec un chemin partiel ca passe

        :/tmp$ tar zcvf test.tgz --exclude=test/d1 test/
        test/
        test/d2/
        test/d2/f23
        test/d2/f21
        test/d2/f22

        par contre en mettant le chemin complet ca ne fonctionne plus

        :/tmp$ tar zcvf test.tgz --exclude=/tmp/test/d1 test/
        test/
        test/d2/
        test/d2/f23
        test/d2/f21
        test/d2/f22
        test/d1/
        test/d1/f12
        test/d1/f11
        test/d1/f13

        ici c'est

        :/$ tar --version
        tar (GNU tar) 1.26

        :/$ bash --version
        GNU bash, version 4.2.45(1)-release (x86_64-pc-linux-gnu)

        • [^] # Re: dans un fichier

          Posté par . Évalué à 2. Dernière modification le 17/06/13 à 15:01.

          D'après mes tests :

          • --exclude /path/to ignore les fichiers contenus dans le répertoire inclus, dont /path/to/foo mais pas /path/to/.foo (l'argument se comporte comme /path/to/*).
          • --exclude-from n'ignore que les fichiers spécialement listés, pas les fichiers inclus dans le répertoire. Il faut écrire le glob /path/to/* explicitement (ce qui, comme ci-dessus, ne concerne pas les fichiers commençant par un point).

          J'ai les mêmes versions de tar et bash que toi.

          • [^] # Re: dans un fichier

            Posté par . Évalué à 2.

            toujours chez moi, ca marche il exclu bien tout le dossier d1 avec l'option exclude=d1

            :/tmp/test$ touch .f01
            :/tmp/test$ touch d1/.f11
            :/tmp/test$ touch d1/.f12
            :/tmp/test$ tar zcvf /tmp/test.tgz --exclude=d1 /tmp/test/
            tar: Suppression de « / » au début des noms des membres
            /tmp/test/
            /tmp/test/.f01
            /tmp/test/d2/
            /tmp/test/d2/f23
            /tmp/test/d2/f21
            /tmp/test/d2/f22

            d'ailleurs je viens de comprendre pourquoi mon test plus haut ne fonctionnait pas avec exclude=/tmp/test/d1 sans le glob, c'est que je faisait le tar depuis le dossier /tmp, mais sur le dossier test/

            du coup le motif /tmp/test/d1 n'apparait pas

            • [^] # Re: dans un fichier

              Posté par . Évalué à 2.

              Oui ça marche comme ça quand tu fais --exclude, mais c'est toi même qui me suggérais plus haut --exclude-from (pour répondre à ma question dans ce forum). J'ai donc essayé --exclude-from, et ma conclusion est que --exclude-from impose que tu passes le glob dans le fichier texte passé en paramètre contenant la liste des fichiers à exclure.

              • [^] # Re: dans un fichier

                Posté par . Évalué à 2.

                non, le exclude-from prend un fichier texte
                qui lui contient exactement le(s) motif(s) que tu veux exclure, ecrits exactement de la meme maniere que sur la ligne --exclude=motif_a_exclure

                simplement ca t'evite d'avoir à jouer des variables et des globs puisqu'il le fait pour toi

  • # Exemple à l'épreuve des balles

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

    liste_exclusion=(   '/tmp/*'                    \
                        '/mnt/*'                    \
                        '/chemin à la con/*'       \
                        '/lost+found/*'             \
                        "/var/${MA_VARIABLE}/*"     \
                )
    
    tar --des_options "${liste_exclusion[@]/#/--exclude=}" --autres_options tralala
    
    

    Les shells ont vraiment des syntaxes de merde /o\

Suivre le flux des commentaires

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