j’ai besoin d’effectuer des traitements sur une liste de fichier filtrés à partir d’une commande git.
Rien de compliqué mais voilà: je connais mes utilisateurs et je souhaite prévoir le cas ou le nom de fichier contient des espaces, guillemets.
ex:
$ git diff --name-only --cached -- '*.rs'
fichier1.rs
"nom \"a la noix\" pour 'moi.rs"
Ultra simplifié pour l’exemple voici ce que je cherche à faire:
$ git diff --name-only --cached -- '*.rs' | grep --color=no -E '.*' | xargs cat # Le grep est juste la pour exemple: commande git -> filtrage -> passage des noms de fichier en param d'une commande.
Et là c’est le drame:
xargs: guillemets simple non appairés ; par défaut les guillemets sont particuliers à xargs à moins d'utiliser l'option -0
cat: 'nom \a': Aucun fichier ou dossier de ce type
cat: la: Aucun fichier ou dossier de ce type
cat: 'noix"': Aucun fichier ou dossier de ce type
cat: pour: Aucun fichier ou dossier de ce type
$ git diff --name-only --cached -- '*.rs' | grep --color=no -E '.*' | xargs -rd '\n ' cat
cat: '"nom \"a la noix\" pour '\''moi.rs"': Aucun fichier ou dossier de ce type
Peut être devrais-je le passer tel quel à xargs avec l’option -0 ?
$ git diff --name-only --cached -- '*.rs' | grep --color=no -E '.*' | xargs -0 cat
cat: '''fichier1.rs'$'\n''"nom \"a la noix\" pour '\''moi.rs"'$'\n': Aucun fichier ou dossier de ce type
C’est mieux comme ça ? xargs est une plaie à utiliser.
Es-tu sûr de faire la commande dans le dossier racine de ton dépôt git ? Car git diff donne des chemins relatif à la racine. Il existe bien une option relative, mais elle n’affiche que les différences internes au dossier courant.
Pour pouvoir utiliser xargs avec -0, qui signifie que les noms de fichiers sont transmis comme des chaînes C, il faut utiliser grep avec --null ou l’équivalent de ton implémentation.
Ceci dit, je ne vois pas l’utilité du -E à grep dans tes exemples, mais c’est une autre histoire.
Autre possibilité à envisager : tu es obligé d’utiliser Bash ? De mon point de vue ça ressemble au genre de tâches qui se fait très bien avec n’importe quel langage de script capable d’accéder à des API Git et de lancer des programmes arbitraires.
Les intérêts dans ton cas, ce serait un script plus lisible, plus maintenable, plus souple dans les possibilités de filtrage, sans être obligé de jongler avec des chaines de caractères pour essayer de retrouver les noms de fichiers concernés.
Évidemment c’est surtout intéressant si tu as besoin d’un script pérenne et pas juste d’un truc lancé une seule fois.
git diff --name-only --cached -- '*.rs' | grep --color=no -E '.*' | xargs -I {} cat {}
xargs: guillemets simple non appairés ; par défaut les guillemets sont particuliers à xargs à moins d'utiliser l'option -0
Avec l’option -0:
$ git diff --name-only --cached -- '*.rs' | grep --color=no -E '.*' | xargs -0 -I {} cat "{}"
cat: '''fichier1.rs'$'\n''"nom \"a la noix\" pour '\''moi.rs"'$'\n': Aucun fichier ou dossier de ce type
Cela ne pose pas de pb: la finalité est l’utilisation dans un hook (j’ajoute pour les curieux que j’ai des contraintes qui m’empêchent d’utiliser une lib genre pre-commit avec laquelle je travaille pourtant d’habitude…)
À moins que je n’ai mal compris ta solution, le résultat est similaire:
$ git diff --name-only --cached -- '*.rs' | grep --null --color=no -E '.*' | xargs -0 cat
cat: '''fichier1.rs'$'\n''"nom \"a la noix\" pour '\''moi.rs"'$'\n': Aucun fichier ou dossier de ce type
En effet, le grep ici ne sert qu’a montrer un contexte simplifé, je l’ai ajouté pour préciser qu’un ensemble de traitement était réalisé dont un grep
J’en conviens, et il me semble envisageable d’utiliser Python pour ce script ce que je ferais en l’absence de solution. L’avantage que je vois à BASH c’est que, lançant un grand nombre de commande aux retours parfois mmh… exotique, je m’évite de fastidieux traitement de sortie. Et puis je suis à l’aise avec BASH pour cet usage d’habitude .
Cependant ma curiosité restera entière: comment passer à xargs une entrée telle que celle ci ? Cette question risque de hanter mes nuits c’est pourquoi je persiste
Je ne suis pas contre cette option cependant en quoi plusieurs lignes permettraient de régler le problème ?
Je ne suis pas contre cette option cependant en quoi plusieurs lignes permettraient de régler le problème ?
Déjà, tu ne t’embêtes plus avec xargs et sa gestion particulière des chaînes.
Ensuite, ça permet de bien structurer ce que tu fais, des structures de contrôles et l’utilisation de variable pour nommer les étapes intermédiaires. Ici, on pourrait séparer l’étape où on liste les fichiers de celle où on filtre les fichiers. Je ne l’ai pas fait mais ça ressemblerais à ça.
Bref, le problème à l’ai plus complexe que juste ça.
Est-ce que tu pourrais nous transmettre tes versions de xargs, grep et git ? Histoire que l’on comprennent pourquoi une commande qui marche chez nous ne marche pas chez toi.
fichier1.rs "nom \"a la noix\" pour 'moi.rs"
On cat fichier1.rs
Fichier 1 est impriméFichier 1 est imprimé
On cat "nom
cat: '"nom': Aucun fichier ou dossier de ce type
On cat \"a
cat: '\"a': Aucun fichier ou dossier de ce type
On cat la
cat: la: Aucun fichier ou dossier de ce type
On cat noix\"
cat: 'noix\"': Aucun fichier ou dossier de ce type
On cat pour
cat: pour: Aucun fichier ou dossier de ce type
On cat 'moi.rs"
cat: ''\''moi.rs"': Aucun fichier ou dossier de ce type
Une manière simple de reproduire sans installer git peut être d’utiliser ls -1
$ ls --version
ls (GNU coreutils) 8.30
# Conditions initiales
touch 'nom "a la noix" pour '\''moi.rs'
echo "Victoire de \"cat\" pour nom à la noix !" >> 'nom "a la noix" pour '\''moi.rs'
touch fichier1.rs
echo"Fichier 1 est imprimé" >> fichier1.rs
fichier1.rs nom "a la noix" pour 'moi.rs
On cat fichier1.rs
Fichier 1 est impriméFichier 1 est imprimé
On cat nom
cat: nom: Aucun fichier ou dossier de ce type
On cat "a
cat: '"a': Aucun fichier ou dossier de ce type
On cat la
cat: la: Aucun fichier ou dossier de ce type
On cat noix"
cat: 'noix"': Aucun fichier ou dossier de ce type
On cat pour
cat: pour: Aucun fichier ou dossier de ce type
On cat 'moi.rs
cat: "'moi.rs": Aucun fichier ou dossier de ce type
J’en profite pour vois remmercier de vos réponses et pour votre temps
$ git diff --name-only --cached -- '*.rs' | grep --color=no -E '.*' | xargs -rd '\n ' cat
cat: '"nom \"a la noix\" pour '\''moi.rs"': Aucun fichier ou dossier de ce type
Cela me fait tiquer et en remontant je vois c’est le bon nom qui est transmis au chat…
$ git diff --name-only --cached -- '*.rs'
fichier1.rs
"nom \"a la noix\" pour 'moi.rs"
En fait git-diff fait le « quoting » des noms contenant des caractères problématiques (espaces, simples ou doubles quotes, etc.)
Ceci explique ton constat :
La même remarque que pour le « grepage » s’applique donc ici et chez moi je vois que c’est l’option -z
git diff -z --name-only --cached -- '*.rs' | xargs -0 ls
# si le résultat est satisfaisant, remplacer ls par cat
édition : Mais cela ne répond qu’en partie je pense, vu que tu as d’autres traitements au milieu (qui doivent comprendre que leurs entrées sont « null-delimited » et produire une sortie pareille jusqu’au bout.
Attention que les chaînes restent particulières et qu’il faut prendre des précautions.
Ce n’est pas xargs en lui-même qui a une gestion particulière des chaînes, c’est plutôt qu’il a un mécanisme pour se prémunir des particularités/limites du shell…
[…]
Je fais l’exercice
0 $ mkdir tst
0 $ cd tst
0 $ touch 'nom "a la noix" pour '\''moi.rs'
0 $ echo "Victoire de \"cat\" pour nom à la noix !" >> 'nom "a la noix" pour '\''moi.rs'
> ^C
1 $ echo "Victoire de \"cat\" pour nom à la noix \!" >> 'nom "a la noix" pour '\''moi.rs'
0 $ # 'nom^I pour autocompleter
0 $ touch fichier1.rs
0 $ echo "Fichier 1 est imprimé" >> fichier1.rs
0 $ ls -1
fichier1.rs
nom "a la noix" pour 'moi.rs
Avant de poursuivre, tu remarques que tu ne passes pas ton nom de fichier dans la boucle mais les morceaux découpés par le shell (c’est aussi confus pour la boucle for ici que pour xargs héhé (raison pour laquelle je dis qu’il ne faut pas accuser ce dernier trop tôt.)
0 $ for f in $(ls -1 *.rs); do echo "$f"; done
fichier1.rs
nom
"a
la
noix"
pour
'moi.rs
0 $ for f in "$(ls -1 *.rs)"; do echo "$f"; done
fichier1.rs
nom "a la noix" pour 'moi.rs
Est-ce gagné ? Non… On a juste réussi à faire un écho simple.
0 $ for f in "$(ls -1 *.rs)"; do echo "==on cat $f=="; done
==on cat fichier1.rs
nom "a la noix" pour 'moi.rs==
0 $ for f in $(ls -1 *.rs); do echo "==on cat $f=="; done
==on cat fichier1.rs==
==on cat nom==
==on cat "a==
==on cat la==
==on cat noix"==
==on cat pour==
==on cat 'moi.rs==
0 $ for f in "$(ls -1 *.rs)"; do cat "$f"; done
cat: fichier1.rs
nom "a la noix" pour 'moi.rs: No such file or directory
1 $ for f in $(ls -1 *.rs); do cat "$f"; done
Fichier 1 est imprimé
cat: nom: No such file or directory
cat: "a: No such file or directory
cat: la: No such file or directory
cat: noix": No such file or directory
cat: pour: No such file or directory
cat: 'moi.rs: No such file or directory
C’est le cas dans lequel tu étais. Et c’est typiquement le genre d’exemple qui montre que ls est bien pour un usage interactif mais pas en script… (ou en utilisant quelque option obscure et non portable…) La même boucle tournée autrement
1 $ find . -name '*.rs' -exec echo "{}" \;
./nom "a la noix" pour 'moi.rs
./fichier1.rs
0 $ find . -name '*.rs' -exec echo "==on cat {}==" \;
==on cat ./nom "a la noix" pour 'moi.rs==
==on cat ./fichier1.rs==
0 $ find . -name '*.rs' -print
./nom "a la noix" pour 'moi.rs
./fichier1.rs
0 $ # -print est le comportement par defaut
0 $ # -print est comme -exec echo {} \;
# -exec est un xargs -n 1 simple...
0 $ find . -name '*.rs' | xargs -n 1 echo
./nom
a la noix
pour
xargs: unterminated quote
1 $ # c'est le traitement du shell : separation aux blancs...
1 $ find . -name '*.rs' -print0 | xargs -0 -n 1 echo
./nom "a la noix" pour 'moi.rs
./fichier1.rs
0 $ # on dit de ne pas considerer les blancs mais...
# ...que les chaines finissent par \0 plutot
0 $ find . -name '*.rs' -print0 | xargs -0 -I FILE -n 1 echo "==on cat FILE=="
==on cat ./nom "a la noix" pour 'moi.rs==
==on cat ./fichier1.rs==
0 $ find . -name '*.rs' -print0 | xargs -0 -n 1 cat
Victoire de "cat" pour nom à la noix \!
Fichier 1 est imprimé
Je suis sous FreeBSD là et mon xargs n’est pas l’implémentation GNU, donc pas d’option -d.
Mais ceci devrait marcher aussi : find . -name '*.rs' | xargs -d '\n' -n 1 echo
Du coup, j’ai plutôt choisi de transmettre les noms encodés comme pour un programme C (et je pense que c’est ce que voient les API) d’un côté, et j’en informe xargs de l’autre côté (jusque là, ce n’est pas lui qui a un traitement particulier des chaînes de caractères.)
Même chose, comme je mentionnais déjà, pour git diff à qui il faut dire (via -z) de transmettre le nom tel quel (sinon il rajoute en plus des "quotes" et c’est plus le même nom qui est vu ensuite) et en format natif (i.e. pour API)
Jusque là, tu noteras que je n’ai pas encore évoqué le cas de grep …parce-que sa fonction est d’extraire les lignes qui matchent… Donc dans l’enchaînement, ce n’est plus le fichier qui est vu par ton xargs mais les lignes issues de grep Passer en script permet d’y voir probablement plus clair. En attendant, je reviens à la boucle sans ls cette fois
0 $ for f in $(find . -name '*.rs'); do echo "$f"; done
./nom
"a
la
noix"
pour
'moi.rs
./fichier1.rs
0 $ for f in "$(find . -name '*.rs')"; do echo "$f"; done
./nom "a la noix" pour 'moi.rs
./fichier1.rs
0 $ # sournoiserie de shell ?
0 $ for f in "$(find . -name '*.rs')"; do echo "==on cat $f=="; done
==on cat ./nom "a la noix" pour 'moi.rs
./fichier1.rs==
0 $ for f in "$(find . -name '*.rs' -print0)"; do echo "==on cat $f=="; done
==on cat ./nom "a la noix" pour 'moi.rs./fichier1.rs==
0 $ for f in $(find . -name '*.rs' -print0); do echo "==on cat $f=="; done
==on cat ./nom==
==on cat "a==
==on cat la==
==on cat noix"==
==on cat pour==
==on cat 'moi.rs./fichier1.rs==
0 $ # on est limite par le traitement du shell...
0 $ # et on voit la chaine avec les \0 et non des \n
0 $ for f in "$(find . -name '*.rs')"; do cat "$f"; done
cat: ./nom "a la noix" pour 'moi.rs
./fichier1.rs: No such file or directory
1 $ # en mode script ce ne serait pas la voix...
Alors, qu’est-ce que conseille si on doit faire un script dans ce cas ? On y était presque…
Le truc est d’utiliser while au lieu de for en ayant soin de d’adapter les délimiteurs partout.
(testé et approuvé chez moi, avec bash comme shell)
1 $ find . -name '*.rs' -print0 | while IFS= read -r -d '' line; do echo "==on cat $line=="; done
==on cat ./nom "a la noix" pour 'moi.rs==
==on cat ./fichier1.rs==
0 $ find . -name '*.rs' -print0 | while IFS= read -r -d '' line; do echo "==on cat $line=="; cat "$line"; done
==on cat ./nom "a la noix" pour 'moi.rs==
Victoire de "cat" pour nom à la noix \!
==on cat ./fichier1.rs==
Fichier 1 est imprimé
On peut en général garder aussi, dans ces cas-ci, la séparation par saut de ligne habituelle…
(et du coup, on peut utiliser ls aussi jusqu’au prochain souci…)
0 $ ls -1 *.rs | while IFS=$'\n' read -r -d $'\n' line; do echo "==on cat $line=="; cat "$line"; done
==on cat fichier1.rs==
Fichier 1 est imprimé
==on cat nom "a la noix" pour 'moi.rs==
Victoire de "cat" pour nom à la noix \!
Merci Gil Cot pour ces réponses qui me font reconsidérer l’usage d’un while avec read dans ces cas mais aussi l’existence du séparateur NUL qui m’était totalement passé sous les radars.
Je constate qu’il est aussi possible de faire:
git diff --name-only -z --cached -- '*.rs' |
while IFS=$'\0' read -r -d $'\0' line; do
echo $line
done
afin de traiter les fichiers un à un en évitant les problèmes de quoting de git-diff.
Du coup, je propose la résolution du tout premier problème que je décrivais avec des pipes:
# git diff -z : utilise le caractère NULL dans la sortie et revoie les chaînes brutes (sans quotes comme dit dans le réponse de Gil Cot)
# grep --null-data : permet d'accepter les chaînes brutes en entrée de grep
git diff --name-only --cached -z -- '*.rs' | grep --null-data --color=no -E '.*' | xargs -0 cat
Attention que ton grep envoie les lignes qui matchent (ou ces lignes préfixé du nom du fichiers) alors que tu xargs est supposé donner des noms de fichiers à cat
$ grep '.*' fichier1.rs
Fichier 1 est imprimé
$# toute la ligne en couleur
Je te suggère de remplacer --color=no par --files-with-matches et éventuellement de rajouter (une fois que tu as fini de tester) --no-messages
J’ai l’impression qu’il y a confusion, ici l’input (la sortie de git diff) est déjà une liste de noms de fichiers, qui est juste filtrée par grep avant d’être filée comme arguments à cat. L’option --files-with-matches est utile avec rgrep, mais pas ici (ça va juste répondre «(standard input)»).
Je pensais qu’il s’agissait de rechercher un motif dans les fichiers modifiés, et m’étais demandé initialement pourquoi n’avoir pas passé le grep à xargs …ou sinon, t’as raison lxnv, il aurait fallu effectivement écrire
grep --null --color=no -l -s '.*' $(git diff --name-only --cached -z -- '*.rs') | xargs -0 cat
Je n’avais pas du tout pensé qu’il s’agissait de refiltrer les noms des fichiers (parce-que ayant vu la restriction *.rs avant et le motif '.*' ensuite.) En plus j’avais oublié que le pipe impliquait qu’on avait le contenu dans lequel chercher (soit stdin, et donc plus de fichier à lui passer) Du coup, c’est plutôt
git diff --name-only --cached -z -- '*.rs' | grep -z --line-buffered -s '.*' | xargs -0 cat
que j’aurais du recommander.
Connectez-vous pour pouvoir poster un message.
Connexion
Pas encore membre ?
Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte