Utilisation d'une liste de noms de fichiers générée comme liste d'arguments - avec des espaces

16

J'essaie d'invoquer un script avec une liste de noms de fichiers collectés par find. Rien de spécial, juste quelque chose comme ça:

$ myscript `find . -name something.txt`

Le problème est que certains des chemins d'accès contiennent des espaces, ils sont donc divisés en deux noms invalides lors de l'expansion des arguments. Normalement, j'entoure les noms de guillemets, mais ici, ils sont insérés par l'expansion des guillemets. J'ai essayé de filtrer la sortie findet d'entourer chaque nom de fichier avec des guillemets, mais au moment où bash les voit, il est trop tard pour les supprimer et ils sont traités comme faisant partie du nom de fichier:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

Oui, ce sont les règles de traitement de la ligne de commande, mais comment la contourner?

C'est embarrassant mais je n'arrive pas à trouver la bonne approche. J'ai finalement compris comment le faire avec xargs -0 -n 10000... mais c'est un hack si laid que je veux toujours demander: comment puis-je citer les résultats de l'expansion des guillemets, ou obtenir le même effet d'une autre manière?

Éditer: J'étais confus sur le fait que le xargs fait Collect tous les arguments dans une liste unique argument, à moins qu'il ne dit autrement , ou peut - être dépassé les limites du système. Merci à tous de m'avoir remis au clair! D'autres, gardez cela à l'esprit lorsque vous lisez la réponse acceptée car elle n'est pas indiquée très directement.

J'ai accepté la réponse, mais ma question demeure: n'y a-t-il pas un moyen de protéger les espaces en expansion (ou en backtick $(...))? (Notez que la solution acceptée est une réponse non bash).

alexis
la source
Je suppose que vous auriez besoin de changer ce que le shell utilise comme séparateurs de nom de fichier (par exemple, en jouant avec la valeur de IFS, une façon possible est IFS=", la nouvelle ligne, "). Mais est-il nécessaire d'exécuter le script sur tous les noms de fichiers? Sinon, envisagez d'utiliser find it pour exécuter le script de chaque fichier.
njsg le
Changer l'IFS est une excellente idée, je n'y avais pas pensé! Pas pratique pour une utilisation en ligne de commande, mais quand même. :-) Et oui, le but est de passer tous les arguments à la même invocation de mon script.
alexis

Réponses:

12

Vous pouvez effectuer les opérations suivantes en utilisant certaines implémentations de findet xargscomme ceci.

$ find . -type f -print0 | xargs -r0 ./myscript

ou, normalement, juste find:

$ find . -type f -exec ./myscript {} +

Exemple

Disons que j'ai l'exemple de répertoire suivant.

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

Maintenant, disons que je l'ai pour ./myscript.

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

Maintenant, lorsque j'exécute la commande suivante.

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Ou quand j'utilise le 2ème formulaire comme ceci:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Détails

trouver + xargs

Les 2 méthodes ci-dessus, bien que différentes, sont essentiellement les mêmes. La première consiste à prendre la sortie de find, à la diviser en utilisant NULLs ( \0) via le -print0commutateur pour trouver. Le xargs -0est spécialement conçu pour accepter une entrée divisée à l'aide de valeurs NULL. Cette syntaxe non standard a été introduite par GNU findet xargsse retrouve également de nos jours dans quelques autres comme les BSD les plus récents. L' -roption est requise pour éviter d'appeler myscriptsi findne trouve rien avec GNUfind mais pas avec BSD.

REMARQUE: toute cette approche repose sur le fait que vous ne passerez jamais une chaîne excessivement longue. Si c'est le cas, alors une 2e invocation de./myscript sera lancée avec le reste des résultats ultérieurs de find.

trouver avec +

C'est la méthode standard (bien qu'elle n'ait été ajoutée que relativement récemment (2005) à la mise en œuvre de GNU find). La capacité de faire ce que nous faisons xargsest littéralement intégrée find. Ainsi find, vous trouverez une liste de fichiers, puis passerez cette liste autant d'arguments que possible à la commande spécifiée après -exec(notez que {}cela ne peut être que juste avant +dans ce cas), en exécutant les commandes plusieurs fois si nécessaire.

Pourquoi ne pas citer?

Dans le premier exemple, nous prenons un raccourci en évitant complètement les problèmes de citation, en utilisant des valeurs NULL pour séparer les arguments. Quand xargsest donnée cette liste, il est chargé de se diviser sur les NULLs protégeant efficacement nos atomes de commande individuels.

Dans le deuxième exemple, nous gardons les résultats internes à find et il sait donc à quoi correspond chaque atome de fichier, et garantira de les traiter de manière appropriée, évitant ainsi le problème de les citer.

Taille maximale de la ligne de commande?

Cette question revient de temps en temps, donc en prime, je l'ajoute à cette réponse, principalement pour que je puisse la trouver à l'avenir. Vous pouvez utiliser xargspour voir à quoi ressemble la limite de l'environnement:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072
slm
la source
1
Merci mais j'ai besoin de passer tous les arguments à la même invocation de mon script. C'est dans la description du problème, mais je suppose que je n'ai pas précisé que ce n'est pas accessoire.
alexis
@alexis - relisez les réponses, ils passent tous les arguments à un seul appel de votre script.
slm
Que je sois damné! Je ne connaissais pas l' +argument find(et vous l'utilisez aussi +en prose, donc j'ai raté votre explication la première fois). Mais plus précisément, j'aurais mal compris ce que xargsfait par défaut !!! En trois décennies d'utilisation d'Unix, je n'en ai jamais utilisé jusqu'à présent, mais je pensais que je connaissais ma boîte à outils ...
alexis
@alexis - Je pensais que vous aviez raté ce que nous disions. Oui xargsest un diable d'une commande. Vous devez le lire et consultez findles pages de manuel plusieurs fois pour voir ce qu'ils peuvent faire. Les commutateurs peuvent être contre-positifs les uns des autres, ce qui ajoute à la confusion.
slm
@alexis - encore une chose à ajouter à la boîte à outils, n'utilisez pas les guillemets / backticks pour exécuter des commandes imbriquées, utilisez $(..)maintenant à la place. Il gère automatiquement l'imbrication des guillemets, etc. Les backticks sont obsolètes.
slm
3
find . -name something.txt -exec myscript {} +

Dans ce qui précède, findrecherche tous les noms de fichiers correspondants et les fournit comme arguments myscript. Cela fonctionne avec les noms de fichiers quels que soient les espaces ou tout autre caractère impair.

Si tous les noms de fichiers tiennent sur une seule ligne, alors myscript est exécuté une fois. Si la liste est trop longue pour être manipulée par le shell, alors find exécutera myscript plusieurs fois si nécessaire.

PLUS: Combien de fichiers tiennent sur une ligne de commande? man finddit que le findconstruit en ligne de commande "de la même manière que xargs le construit". Et man xargsque les limites dépendent du système et que vous pouvez les déterminer en exécutant xargs --show-limits. ( getconf ARG_MAXest également une possibilité). Sous Linux, la limite est généralement (mais pas toujours) d' environ 2 millions de caractères par ligne de commande.

John1024
la source
2

Quelques ajouts à la bonne réponse de @ slm.

La limitation de la taille des arguments se trouve sur l' execve(2)appel système (en fait, c'est sur la taille cumulée des chaînes d'argument et d'environnement et des pointeurs). Si myscriptest écrit dans un langage que votre shell peut interpréter, alors peut-être n'avez-vous pas besoin de l' exécuter , vous pourriez avoir votre shell simplement l'interpréter sans avoir à exécuter un autre interpréteur.

Si vous exécutez le script en tant que:

(. myscript x y)

C'est comme:

myscript x y

Sauf qu'il est interprété par un enfant du shell actuel, au lieu de l' exécuter (ce qui implique éventuellement l' exécution sh (ou ce que la ligne she-bang spécifie le cas échéant) avec encore plus d'arguments).

Maintenant, évidemment, vous ne pouvez pas utiliser find -exec {} +la .commande, car .étant une commande intégrée du shell, elle doit être exécutée par le shell, pas par find.

Avec zsh, c'est simple:

IFS=$'\0'
(. myscript $(find ... -print0))

Ou:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

Bien qu'avec zsh, vous n'auriez pas besoin finden premier lieu car la plupart de ses fonctionnalités sont intégrées dans le zshglobbing.

bashCependant, les variables ne peuvent pas contenir de caractères NUL, vous devez donc trouver une autre façon. Une façon pourrait être:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

Vous pouvez également utiliser le remplacement récursif de style zsh avec l' globstaroption bash4.0 et les versions ultérieures:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

Notez que les **liens symboliques suivis vers les répertoires jusqu'à ce qu'il soit corrigé dans bash4.3. Notez également que bashne pas mettre en œuvre des zshqualificatifs jokers afin que vous ne serez pas toutes les fonctionnalités de findlà.

Une autre alternative serait d'utiliser GNU ls:

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

Les méthodes ci-dessus peuvent également être utilisées si vous voulez vous assurer qu'il myscriptn'est exécuté qu'une seule fois (échec si la liste d'arguments est trop grande). Sur les versions récentes de Linux, vous pouvez augmenter et même lever cette limitation sur la liste des arguments avec:

ulimit -s 1048576

(Taille de pile 1GiB, dont un quart peut être utilisé pour la liste arg + env).

ulimit -s unlimited

(sans limites)

Stéphane Chazelas
la source
1

Dans la plupart des systèmes, il y a une limite à la longueur d'une ligne de commande passée à n'importe quel programme, en utilisant xargsou -exec command {} +. De man find:

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

Les invocations seront beaucoup moins, mais ne seront pas garanties d'être une. Ce que vous devez faire est de lire les noms de fichiers séparés par NUL dans le script depuis stdin, possible en fonction d'un argument de ligne de commande -o -. Je ferais quelque chose comme:

$ find . -name something.txt -print0 | myscript -0 -o -

et implémentez les arguments d'option en myscriptconséquence.

Timo
la source
Oui, le système d'exploitation impose une limite dans le nombre / taille d'arguments qui peuvent être passés. Sur les systèmes Linux modernes, c'est (gigantesque) ( linux.die.net/man/2/execve ) (1/4 de la taille de la pile, arguments 0x7FFFFFFF). AFAIK bash lui-même n'impose aucune limite. Mes listes sont beaucoup plus petites et mon problème est dû à un malentendu ou à un mauvais souvenir du xargsfonctionnement. Votre solution est en effet la plus robuste, mais elle est exagérée dans ce cas.
alexis
0

N'y a-t-il pas un moyen de protéger les espaces dans l'expansion backtick (ou $ (...))?

Non, il n'y en a pas. Pourquoi donc?

Bash n'a aucun moyen de savoir ce qui doit être protégé et ce qui ne devrait pas l'être.

Il n'y a pas de tableaux dans le fichier / pipe Unix. Ce n'est qu'un flux d'octets. La commande à l'intérieur de ``ou $()génère un flux, que bash avale et traite comme une chaîne unique. À ce stade, vous n'avez que deux choix: le mettre entre guillemets, pour le conserver sous forme de chaîne, ou le mettre nu, afin que bash le divise en fonction de son comportement configuré.

Donc, ce que vous devez faire si vous voulez un tableau est de définir un format d'octet qui a un tableau, et c'est ce que les outils aiment xargset findfont: si vous les exécutez avec l' -0argument, ils fonctionnent selon un format de tableau binaire qui termine les éléments avec l'octet nul, ajoutant la sémantique au flux d'octets autrement opaque.

Malheureusement, bashne peut pas être configuré pour fractionner les chaînes sur l'octet nul. Merci à /unix//a/110108/17980 de nous avoir montré quezsh .

xargs

Vous voulez que votre commande s'exécute une fois, et vous avez dit que cela xargs -0 -n 10000résout votre problème. Ce n'est pas le cas, cela garantit que si vous avez plus de 10000 paramètres, votre commande s'exécutera plus d'une fois.

Si vous voulez qu'il soit strictement exécuté une fois ou échoue, vous devez fournir l' -xargument et un -nargument plus grand que l' -sargument (vraiment: suffisamment grand pour qu'un tas d'arguments de longueur nulle plus le nom de la commande ne tiennent pas la -staille). ( man xargs , voir extrait ci-dessous)

Le système sur lequel je suis actuellement a une pile limitée à environ 8M, voici donc ma limite:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

frapper

Si vous ne voulez pas impliquer une commande externe, la boucle en lecture-lecture alimentant un tableau, comme indiqué dans /unix//a/110108/17980 , est le seul moyen pour bash de diviser les choses à l'octet nul.

L'idée de se procurer le script ( . ... "$@" )pour éviter la limite de taille de pile est cool (je l'ai essayé, ça marche!), Mais probablement pas important dans des situations normales.

L'utilisation d'un fd spécial pour le tube de processus est importante si vous voulez lire autre chose depuis stdin, mais sinon vous n'en aurez pas besoin.

Ainsi, la façon la plus simple "native", pour les besoins quotidiens des ménages:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

Si vous aimez que votre arbre de processus soit propre et agréable à regarder, cette méthode vous permet de le faire exec mynonscript "${files[@]}", ce qui supprime le processus bash de la mémoire, en le remplaçant par la commande appelée. xargsrestera toujours en mémoire pendant l'exécution de la commande appelée, même si la commande ne s'exécute qu'une seule fois.


Ce qui parle contre la méthode bash native est la suivante:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bash n'est pas optimisé pour la gestion des tableaux.


homme xargs :

-n max-args

Utilisez au maximum des arguments max-args par ligne de commande. Moins d'arguments que max-args seront utilisés si la taille (voir l'option -s) est dépassée, sauf si l'option -x est donnée, auquel cas xargs se fermera.

-s max-chars

Utilisez au maximum des caractères max-chars par ligne de commande, y compris la commande et les arguments initiaux et les valeurs nulles de fin aux extrémités des chaînes d'argument. La plus grande valeur autorisée dépend du système et est calculée comme la limite de longueur d'argument pour exec, moins la taille de votre environnement, moins 2048 octets de marge. Si cette valeur est supérieure à 128 Ko, 128 Ko sont utilisés comme valeur par défaut; sinon, la valeur par défaut est le maximum. 1 Ko est de 1024 octets.

-X

Quittez si la taille (voir l'option -s) est dépassée.

clacke
la source
Merci pour tous les ennuis, mais votre principe de base ignore le fait que bash utilise normalement un système élaboré de traitement des devis. Mais pas dans l'expansion des guillemets. Comparer les éléments suivants ( toutes deux erreurs de donner, mais montrer la différence): par ls "what is this"rapport ls `echo '"what is this"'` . Quelqu'un a négligé d'implémenter le traitement des devis pour le résultat des backquotes.
alexis
Je suis content que les backquotes ne traitent pas les devis. Le fait qu'ils séparent même les mots a causé suffisamment de regards confus, de rayures et de failles de sécurité dans l'histoire de l'informatique moderne.
clacke
La question est "N'y a-t-il pas un moyen de protéger les espaces dans le backtick (ou $(...)) l'expansion?", Il semble donc approprié d'ignorer le traitement qui n'est pas fait dans cette situation.
clacke
Le format de tableau d'éléments à terminaison nulle est le moyen le plus simple et donc le plus sûr d'exprimer un tableau. C'est juste une honte qui bashne le supporte pas nativement comme apparemment zsh.
clacke
En fait, juste cette semaine, j'ai utilisé printf "%s\0"et xargs -0pour contourner une situation de citation où un outil intermédiaire passerait des paramètres à travers une chaîne analysée par un shell. Citer revient toujours pour vous mordre.
clacke