Linux sans magie #29 — find et xargs : transmettre proprement des fichiers à une commande
Comprendre comment combiner find et xargs sous Linux, pourquoi les noms de fichiers peuvent casser un pipeline classique et comment utiliser -print0 avec xargs -0.
Dans le #28, nous avons appris à construire une sélection fiable avec find.
La question suivante est naturelle : comment transmettre les chemins trouvés à une autre commande ?
Une première idée consiste souvent à écrire :
find ... | xargs commande
Cette forme peut fonctionner sur des noms simples. Elle devient toutefois fragile dès que les chemins contiennent des espaces, des tabulations, des retours à la ligne, des guillemets ou certains caractères interprétés par xargs.
L’objectif de cet article est de comprendre ce problème et d’adopter une forme robuste avec GNU Findutils :
find ... -print0 | xargs -0 ...
Nous resterons sur des commandes d’affichage et de lecture pour observer le comportement sans modifier les fichiers.
Ce que fait xargs
xargs lit des éléments depuis son entrée standard puis les utilise comme arguments pour construire et exécuter une ou plusieurs lignes de commande.
Exemple simple :
printf '%s\n' alpha beta gamma | xargs printf '<%s>\n'
Résultat attendu :
<alpha>
<beta>
<gamma>
Dans cet exemple, xargs reçoit trois éléments et les transmet à printf.
Cette logique devient utile lorsque la liste d’arguments provient d’une autre commande, notamment find.
Pourquoi un simple pipeline peut casser les noms de fichiers
Par défaut, GNU xargs sépare les éléments selon les blancs de l’entrée et traite aussi les guillemets et l’antislash de manière particulière.
Cela signifie qu’une sortie textuelle où « un chemin est affiché sur une ligne » n’est pas un format universellement sûr pour transporter des noms de fichiers arbitraires.
Un nom de fichier Unix peut notamment contenir :
- des espaces ;
- des tabulations ;
- des guillemets ;
- des antislashs ;
- des retours à la ligne.
Le caractère NUL, en revanche, ne peut pas faire partie d’un nom de fichier Unix. C’est ce qui en fait un séparateur adapté à ce besoin.
Préparer un lab avec des noms difficiles
Ce lab utilise Bash pour deux éléments précis : la syntaxe $'...' qui permet de créer un nom contenant un retour à la ligne, et printf %q pour afficher les arguments sous une forme échappée. Lancez donc les commandes suivantes dans Bash.
Créons un répertoire temporaire :
LAB_DIR="$(mktemp -d)"
printf 'Lab: %s\n' "$LAB_DIR"
cd "$LAB_DIR"
Puis quelques fichiers :
touch 'simple.txt'
touch 'rapport final.txt'
touch 'serveur 01.log'
touch $'ligne\nnouvelle.txt'
Le dernier nom contient réellement un retour à la ligne.
Pour observer les noms avec un affichage non ambigu côté Bash :
printf '%q\n' ./*
Une sortie possible ressemble à :
./ligne$'\n'nouvelle.txt
./rapport\ final.txt
./serveur\ 01.log
./simple.txt
Cette commande d’observation ne modifie rien.
Le cas fragile : find -print | xargs
Regardons d’abord la sortie classique de find :
find . -maxdepth 1 -type f -print
Elle est destinée à être lisible comme du texte.
Avec notre fichier contenant un retour à la ligne, un seul nom peut visuellement occuper plusieurs lignes.
Si nous transmettons directement cette sortie à xargs :
find . -maxdepth 1 -type f -print | xargs printf '<%s>\n'
les espaces et le retour à la ligne peuvent devenir des séparateurs d’arguments.
Autrement dit, la frontière entre deux noms de fichiers n’est plus représentée de façon fiable.
Le problème n’est pas find seul ni xargs seul : c’est le choix d’un format texte ambigu entre les deux commandes.
La solution : find -print0
GNU find fournit l’action -print0.
Au lieu de terminer chaque chemin par un retour à la ligne, -print0 utilise un caractère NUL.
Commande :
find . -maxdepth 1 -type f -print0
La sortie n’est plus conçue pour être lue directement dans un terminal.
Elle est destinée à être consommée par un outil capable de comprendre ce séparateur.
C’est le rôle de xargs -0.
Associer -print0 et xargs -0
La forme robuste est :
find . -maxdepth 1 -type f -print0 |
xargs -0 printf '<%s>\n'
Avec l’option -0, GNU xargs considère que chaque élément se termine par NUL.
Les blancs, guillemets et antislashs contenus dans les noms ne servent alors plus de syntaxe de découpage : ils sont transmis comme faisant partie de l’argument.
Le fichier dont le nom contient un retour à la ligne reste lui aussi un seul argument.
Vérifier les arguments réellement transmis
Pour voir plus clairement les frontières entre arguments, Bash peut afficher chaque chemin sous une forme échappée.
Nous pouvons utiliser un petit bash -c uniquement pour l’observation :
find . -maxdepth 1 -type f -print0 |
xargs -0 bash -c 'for arg do printf "%q\n" "$arg"; done' bash
Cette commande ne modifie aucun fichier.
Elle permet de vérifier que :
rapport final.txtreste un seul argument ;serveur 01.logreste un seul argument ;- le fichier contenant un retour à la ligne reste un seul argument.
Cette étape de vérification est utile avant d’associer une opération ayant des effets de bord.
Utiliser xargs avec une commande réelle mais non destructive
Prenons uniquement les fichiers .txt.
Avec des noms simples, on pourrait être tenté d’écrire :
find . -maxdepth 1 -type f -name '*.txt' |
xargs wc -c
Nous préférons :
find . -maxdepth 1 -type f -name '*.txt' -print0 |
xargs -0 wc -c
wc -c lit les fichiers et affiche leur taille en octets. Il ne modifie pas leur contenu.
La sélection reste sous la responsabilité de find, et le transport des chemins est réalisé avec un séparateur non ambigu.
Limiter le nombre d’arguments avec -n
xargs peut regrouper plusieurs éléments dans une même invocation de la commande.
L’option -n permet de limiter le nombre maximal d’arguments pris depuis l’entrée pour chaque invocation.
Exemple :
printf '%s\0' alpha beta gamma |
xargs -0 -n 1 printf 'argument: %s\n'
Résultat attendu :
argument: alpha
argument: beta
argument: gamma
Ici, printf est lancé avec un élément d’entrée à la fois.
Ce mécanisme peut être utile lorsqu’une commande doit être appelée par petits lots, mais il ne faut pas ajouter -n 1 mécaniquement : regrouper plusieurs arguments est précisément l’un des rôles de xargs.
Voir les commandes avec -t
Avant une opération sensible, GNU xargs peut afficher sur la sortie d’erreur la ligne de commande qu’il s’apprête à exécuter.
Exemple :
find . -maxdepth 1 -type f -print0 |
xargs -0 -t wc -c
L’option -t apporte une visibilité supplémentaire pendant un lab ou un diagnostic.
Elle ne remplace toutefois pas la vérification de la sélection produite par find.
Que se passe-t-il si find ne trouve rien ?
Ce cas mérite une attention particulière.
Avec GNU xargs, l’option -r ou --no-run-if-empty évite d’exécuter la commande lorsque l’entrée ne contient aucun élément.
Exemple :
find . -maxdepth 1 -type f -name '*.absent' -print0 |
xargs -0 -r printf '<%s>\n'
Aucun argument n’étant reçu, printf n’est pas lancé par xargs.
-r est une option GNU : sa portabilité doit être vérifiée si le script doit fonctionner avec d’autres implémentations de xargs.
Pourquoi ne pas utiliser simplement une substitution de commande ?
Une autre forme parfois rencontrée est :
commande $(find ...)
Pour des noms de fichiers arbitraires, cette approche introduit les mécanismes de découpage et d’expansion du shell.
Elle n’est donc pas un remplacement robuste de :
find ... -print0 | xargs -0 commande
Le point important est de préserver les frontières exactes entre arguments du producteur jusqu’à la commande finale.
find -exec ou xargs ?
find sait aussi lancer une commande avec -exec.
Par exemple :
find . -maxdepth 1 -type f -exec wc -c {} +
Cette forme transmet directement les chemins trouvés à la commande sans passer par un flux texte intermédiaire.
Elle est donc également adaptée aux noms de fichiers contenant des caractères particuliers.
Pour ce #29, l’objectif reste de comprendre xargs. Retenez toutefois qu’il ne faut pas introduire xargs par réflexe lorsque find -exec ... {} + exprime plus directement le besoin.
Le choix dépend de la structure du pipeline, de la lisibilité recherchée et de l’outil qui produit la liste d’arguments.
Construire le pipeline progressivement
Avant de lancer une commande finale, validez chaque étape.
Commencez par la sélection :
find . -maxdepth 1 -type f -name '*.txt'
Puis vérifiez la version NUL avec un consommateur sûr :
find . -maxdepth 1 -type f -name '*.txt' -print0 |
xargs -0 -r printf '<%s>\n'
Enfin seulement, remplacez la commande d’affichage par le traitement voulu.
Exemple non destructif :
find . -maxdepth 1 -type f -name '*.txt' -print0 |
xargs -0 -r wc -c
Cette progression conserve une étape de vérification observable avant le traitement.
Bonnes pratiques
Utilisez -print0 avec xargs -0 lorsque des chemins issus de find doivent être transportés de façon non ambiguë.
Ne supposez pas que « un nom par ligne » suffit : un nom de fichier peut lui-même contenir un retour à la ligne.
Citez les motifs passés à find :
find . -name '*.log'
Vérifiez la sélection avant d’associer une commande ayant des effets de bord.
Utilisez -t lorsque l’affichage des commandes générées facilite le diagnostic.
Sur GNU xargs, utilisez -r lorsqu’une exécution sans argument n’a pas de sens.
Considérez find -exec ... {} + lorsqu’il fournit une expression plus directe que le pipeline avec xargs.
Sécurité
Le principal risque n’est pas uniquement de « casser » un nom contenant un espace.
Une mauvaise reconstruction des arguments peut faire traiter par la commande finale des chaînes différentes des chemins réellement sélectionnés.
Ce problème devient particulièrement important avec des commandes qui :
- modifient des fichiers ;
- changent des permissions ;
- déplacent des données ;
- suppriment des fichiers ;
- exécutent un programme sur chaque chemin reçu.
Dans cet article, nous restons volontairement sur printf et wc.
Avant toute opération destructive, vérifiez à la fois la sélection find et les arguments transmis à la commande finale.
Coûts
Les exemples sont locaux et n’introduisent aucun coût de service cloud.
Sur de grandes arborescences, find peut néanmoins produire des entrées/sorties importantes, et la commande appelée par xargs peut elle-même consommer CPU, mémoire ou I/O selon le traitement demandé.
Limites et points de vigilance
Cet article ne couvre pas :
- l’exécution parallèle avec
xargs -P; - les limites détaillées de taille des lignes de commande ;
--show-limits;- les interactions complexes avec
bash -c; find -execdir;- les opérations destructives ;
- les différences détaillées entre implémentations GNU, BSD et autres environnements Unix.
Le couple -print0 / -0 présenté ici est documenté par GNU Findutils.
Pour un script multi-plateforme, vérifiez les options prises en charge par les implémentations cibles.
Rollback ou nettoyage
Les traitements du lab ne modifient pas le contenu des fichiers.
Pour quitter le répertoire temporaire puis le supprimer :
cd /
rm -rf -- "$LAB_DIR"
Avant la suppression, vous pouvez vérifier la variable :
printf '%s\n' "$LAB_DIR"
Le chemin doit correspondre au répertoire temporaire créé par mktemp -d.
Références officielles
- GNU Findutils Manual — GNU Project
- GNU Findutils Manual — Safe File Name Handling — GNU Project
- GNU Findutils Manual — Unusual Characters in File Names — GNU Project
- GNU Findutils Manual — xargs options — GNU Project
- GNU Findutils Manual — Invoking xargs — GNU Project
Conclusion
Le problème traité par find et xargs n’est pas seulement de faire passer du texte d’une commande à une autre.
Il faut préserver les frontières entre arguments.
Pour des noms de fichiers arbitraires, retenez le couple :
find ... -print0 | xargs -0 ...
Et lorsque find -exec ... {} + exprime plus simplement l’opération, utilisez-le sans ajouter inutilement un pipeline.
