Comment puis-je utiliser xargs pour copier des fichiers qui ont des espaces et des guillemets dans leurs noms?


232

J'essaie de copier un tas de fichiers sous un répertoire et un certain nombre de fichiers ont des espaces et des guillemets simples dans leurs noms. Lorsque j'essaie de chaîner ensemble findet grepavec xargs, j'obtiens l'erreur suivante:

find .|grep "FooBar"|xargs -I{} cp "{}" ~/foo/bar
xargs: unterminated quote

Des suggestions pour une utilisation plus robuste de xargs?

C'est sur Mac OS X 10.5.3 (Leopard) avec BSD xargs.


2
Le message d'erreur GNU xargs pour cela avec un nom de fichier contenant un guillemet simple est plutôt plus utile: "xargs: guillemet simple inégalé; par défaut, les guillemets sont spéciaux pour xargs sauf si vous utilisez l'option -0".
Steve Jessop

3
GNU xargs a également l' --delimiteroption ( -d). Essayez-le avec \ncomme délimiteur, cela empêche xargsde séparer les lignes avec des espaces en plusieurs mots / arguments.
MattBianco

Réponses:


199

Vous pouvez combiner tout cela en une seule findcommande:

find . -iname "*foobar*" -exec cp -- "{}" ~/foo/bar \;

Cela gérera les noms de fichiers et les répertoires contenant des espaces. Vous pouvez utiliser -namepour obtenir des résultats sensibles à la casse.

Remarque: l' --indicateur transmis à l' cpempêche de traiter les fichiers commençant par -comme options.


70
Les gens utilisent xargs car il est généralement plus rapide d'appeler un exécutable 5 fois avec 200 arguments à chaque fois que de l'appeler 1000 fois avec un argument à chaque fois.
tzot

12
La réponse de Chris Jester-Young devrait être la "bonne réponse" là ... BTW cette solution ne fonctionne pas si un nom de fichier commence par "-". Au moins, il a besoin de "-" après cp.
Keltia

11
Exemple de vitesse - sur 829 fichiers, la méthode "find -exec" a pris 26 secondes tandis que l'outil de méthode "find -print0 | xargs --null" 0,7 seconde. Différence significative.
Peter Porter

7
@tzot Un commentaire tardif mais de toute façon, xargsn'est pas nécessaire pour résoudre le problème que vous décrivez, le findsupporte déjà avec la -exec +ponctuation.
jlliagre

3
ne répond pas à la question de savoir comment gérer les espaces
Ben Glasser

117

find . -print0 | grep --null 'FooBar' | xargs -0 ...

Je ne sais pas si grepprend en charge --null, ni si xargsprend en charge -0, sur Leopard, mais sur GNU c'est tout bon.


1
Leopard prend en charge "-Z" (c'est GNU grep) et bien sûr find (1) et xargs (1) supportent "-0".
Keltia

1
Sous OS X 10.9 grep -{z|Z}signifie "se comporter comme zgrep" (décompresser) et non pas le "imprimer un octet zéro après chaque nom de fichier". Utilisez grep --nullpour atteindre ce dernier.
bassim

4
Qu'est-ce qui ne va pas find . -name 'FooBar' -print0 | xargs -0 ...?
Quentin Pradet

1
@QuentinPradet Évidemment, pour une chaîne fixe comme "FooBar", -nameou -pathfonctionne très bien. L'OP a spécifié l'utilisation de grep, probablement parce qu'ils veulent filtrer la liste à l'aide d'expressions régulières.
Chris Jester-Young

1
@ Hi-Angel C'est exactement pourquoi j'utilise xargs -0 en conjonction avec find -print0 . Ce dernier imprime les noms de fichiers avec un terminateur NUL et le premier reçoit les fichiers de cette façon. Pourquoi? Les noms de fichiers sous Unix peuvent contenir des caractères de nouvelle ligne. Mais ils ne peuvent pas contenir de caractères NUL.
Chris Jester-Young

92

La façon la plus simple de faire ce que l'affiche originale veut est de changer le délimiteur de n'importe quel espace en juste le caractère de fin de ligne comme ceci:

find whatever ... | xargs -d "\n" cp -t /var/tmp

4
Cette réponse est simple, efficace et directe: le délimiteur par défaut défini pour xargs est trop large et doit être restreint pour ce que OP veut faire. Je le sais de première main, car j'ai rencontré exactement le même problème aujourd'hui en faisant quelque chose de similaire, sauf dans cygwin. Si j'avais lu l'aide de la commande xargs, j'aurais peut-être évité quelques maux de tête, mais votre solution l'a corrigé pour moi. Merci ! (Oui, OP était sur MacOS utilisant des xargs BSD, que je n'utilise pas, mais j'espère que le paramètre xargs "-d" existe dans toutes les versions).
Etienne Delavennat

7
Belle réponse mais ne fonctionne pas sur Mac. Au lieu de cela , nous pouvons redirigez la trouver dans sed -e 's_\(.*\)_"\1"_g'de citations de force autour du nom de fichier
ishahak

10
Ce devrait être la réponse acceptée. La question portait sur l'utilisation xargs.
Mohammad Alhashash

2
Je reçoisxargs: illegal option -- d
nehem

1
Il convient de souligner que les noms de fichiers peuvent contenir un caractère de nouvelle ligne sur de nombreux systèmes * nix. Il est peu probable que vous rencontriez cela dans la nature, mais si vous exécutez des commandes shell sur des entrées non fiables, cela pourrait être un problème.
Soren Bjornstad

71

C'est plus efficace car il ne lance pas "cp" plusieurs fois:

find -name '*FooBar*' -print0 | xargs -0 cp -t ~/foo/bar

1
Ça n'a pas marché pour moi. Il a essayé de cp ~ / foo / bar dans tout ce que vous trouvez, mais pas le contraire
Shervin Asgari

13
L'indicateur -t de cp est une extension GNU, AFAIK, et n'est pas disponible sur OS X. Mais s'il l'était, cela fonctionnerait comme indiqué dans cette réponse.
metamatt

2
J'utilise Linux. Merci pour le commutateur '-t'. C'est ce qui me manquait :-)
Vahid Pazirandeh

59

J'ai rencontré le même problème. Voici comment je l'ai résolu:

find . -name '*FoooBar*' | sed 's/.*/"&"/' | xargs cp ~/foo/bar

J'avais l'habitude sedde remplacer chaque ligne d'entrée par la même ligne, mais entourée de guillemets doubles. À partir de la sedpage de manuel, " ... Une esperluette (` `& '') apparaissant dans le remplacement est remplacée par la chaîne correspondant à RE ... " - dans ce cas .*, la ligne entière.

Cela résout l' xargs: unterminated quoteerreur.


3
Je suis sur Windows et j'utilise gnuwin32, j'ai donc dû l'utiliser sed s/.*/\"&\"/pour le faire fonctionner.
Pat

Oui, mais sans doute cela ne traiterait-il pas les noms de fichiers avec "in - à moins que sed ne cite également des citations?
artfulrobot

L'utilisation sedest géniale et pour l'instant la bonne solution sans réécrire le problème!
entonio

53

Cette méthode fonctionne sur Mac OS X v10.7.5 (Lion):

find . | grep FooBar | xargs -I{} cp {} ~/foo/bar

J'ai également testé la syntaxe exacte que vous avez publiée. Cela a également bien fonctionné sur 10.7.5.


4
Cela fonctionne, mais -Iimplique -L 1(comme le dit le manuel), ce qui signifie que la commande cp est exécutée une fois par file = v slow.
artfulrobot

xargs -J% cp% <répertoire de destination> Est peut-être plus efficace sous OSX.
Walker D

3
Désolé, mais c'est faux. Tout d'abord, il produit exactement l'erreur que le TO voulait éviter. Vous devez utiliser find ... -print0et xargs -0pour contourner les xargs «par défaut, les guillemets sont spéciaux». Deuxièmement, utilisez généralement les commandes '{}'not {}in passées à xargs pour vous protéger contre les espaces et les caractères spéciaux.
Andreas Spindler

3
Désolé Andreas Spindler, je ne connais pas très bien les xargs et j'ai trouvé cette ligne après quelques expérimentations. Cela semble fonctionner pour la plupart des gens qui l'ont commenté et l'ont voté positivement. Pourriez-vous entrer un peu plus dans les détails sur le type d'erreur qu'elle génère? En outre, cela vous dérangerait-il de publier l'entrée exacte qui, selon vous, serait plus correcte? Je vous remercie.
the_minted

12

Ne l'utilisez pas xargs. C'est un programme soigné mais ça ne va pas bien avecfind cas non triviaux.

Voici une solution portable (POSIX), c'est-à-dire qui ne nécessite pas find, xargsou cpdes extensions spécifiques à GNU:

find . -name "*FooBar*" -exec sh -c 'cp -- "$@" ~/foo/bar' sh {} +

Notez la fin +au lieu de la plus habituelle; .

Cette solution:

  • gère correctement les fichiers et les répertoires avec des espaces intégrés, des retours à la ligne ou tout autre caractère exotique.

  • fonctionne sur n'importe quel système Unix et Linux, même ceux qui ne fournissent pas la boîte à outils GNU.

  • n'utilise pas xargsce qui est un programme agréable et utile, mais nécessite trop de réglages et de fonctionnalités non standard pour gérer correctement la findsortie.

  • est également plus efficace (lire plus rapidement ) que les autres réponses acceptées et la plupart sinon toutes les autres.

Notez également que malgré ce qui est indiqué dans certaines autres réponses ou commentaires, la citation {}est inutile (sauf si vous utilisez la fishcoquille exotique ).



1
@PeterMortensen Vous oubliez probablement la fin plus. findpeut faire ce qui se xargspasse sans frais généraux.
jlliagre

8

Envisagez d'utiliser l'option de ligne de commande --null pour xargs avec l'option -print0 dans find.


8

Pour ceux qui s'appuient sur des commandes, autres que trouver, par exemple ls:

find . | grep "FooBar" | tr \\n \\0 | xargs -0 -I{} cp "{}" ~/foo/bar

1
Fonctionne mais lent car -Iimplique-L 1
artfulrobot

6
find | perl -lne 'print quotemeta' | xargs ls -d

Je crois que cela fonctionnera de manière fiable pour n'importe quel personnage, sauf le saut de ligne (et je soupçonne que si vous avez des sauts de ligne dans vos noms de fichiers, vous avez des problèmes plus graves que cela). Il ne nécessite pas de découverte GNU, juste Perl, donc il devrait fonctionner à peu près n'importe où.


Est-il possible d'avoir un saut de ligne dans un nom de fichier? Jamais entendu parler.
mtk

2
En effet, ça l'est. Essayez, par exemple,mkdir test && cd test && perl -e 'open $fh, ">", "this-file-contains-a-\n-here"' && ls | od -tx1
mavit

1
|perl -lne 'print quotemeta'est exactement ce que je cherchais. D'autres articles ici ne m'ont pas aidé, car plutôt que findje devais utiliser grep -rlpour réduire considérablement le nombre de fichiers PHP uniquement ceux infectés par des logiciels malveillants.
Marcos

perl et quotemeta sont bien plus généraux que print0 / -0 - merci pour la solution générale de pipelining des fichiers avec des espaces
bmike

5

J'ai trouvé que la syntaxe suivante fonctionne bien pour moi.

find /usr/pcapps/ -mount -type f -size +1000000c | perl -lpe ' s{ }{\\ }g ' | xargs ls -l | sort +4nr | head -200

Dans cet exemple, je recherche les 200 fichiers les plus volumineux de plus de 1 000 000 octets dans le système de fichiers monté sur "/ usr / pcapps".

Le trait de ligne Perl entre "find" et "xargs" échappe / cite chaque blanc afin que "xargs" passe tout nom de fichier avec des blancs incorporés à "ls" comme un seul argument.


3

Défi du cadre - vous demandez comment utiliser xargs. La réponse est: vous n'utilisez pas xargs, car vous n'en avez pas besoin.

Le commentaire deuser80168 décrit un moyen de le faire directement avec cp, sans appeler cp pour chaque fichier:

find . -name '*FooBar*' -exec cp -t /tmp -- {} +

Cela fonctionne parce que:

  • l' cp -tindicateur permet de donner le répertoire cible vers le début cpplutôt que vers la fin. De man cp:
   -t, --target-directory=DIRECTORY
         copy all SOURCE arguments into DIRECTORY
  • Le --drapeau indique cpde tout interpréter après comme un nom de fichier, pas un drapeau, donc les fichiers commençant par- ou --ne confondent pas cp; vous en avez toujours besoin car les caractères -/ --sont interprétés parcp , tandis que tout autre caractère spécial est interprété par le shell.

  • La find -exec command {} +variante fait essentiellement la même chose que xargs. 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‐
         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, and (when find is being invoked
         from a shell) it should be quoted (for example, '{}') to protect
         it  from  interpretation  by shells.  The command is executed in
         the starting directory.  If any invocation  returns  a  non-zero
         value  as exit status, then find returns a non-zero exit status.
         If find encounters an error, this can sometimes cause an immedi‐
         ate  exit, so some pending commands may not be run at all.  This
         variant of -exec always returns true.

En l'utilisant directement dans find, cela évite d'avoir besoin d'un tuyau ou d'une invocation de shell, de sorte que vous n'avez pas à vous soucier des caractères désagréables dans les noms de fichiers.


Recherche incroyable, je n'en avais aucune idée !!! "-exec utility [argument ...] {} + Identique à -exec, sauf que` `{} '' est remplacé par autant de chemins que possible pour chaque appel d'utilitaire. Ce comportement est similaire à celui de xargs (1 ). " dans la mise en œuvre de BSD.
conny

2

Sachez que la plupart des options abordées dans d'autres réponses ne sont pas standard sur les plates-formes qui n'utilisent pas les utilitaires GNU (Solaris, AIX, HP-UX, par exemple). Voir le POSIX spécification pour le comportement xargs «standard».

Je trouve également que le comportement de xargs par lequel il exécute la commande au moins une fois, même sans entrée, est une nuisance.

J'ai écrit ma propre version privée de xargs (xargl) pour traiter les problèmes d'espaces dans les noms (seuls les retours à la ligne se séparent - bien que la combinaison 'find ... -print0' et 'xargs -0' soit assez nette étant donné que les noms de fichiers ne peuvent pas contiennent des caractères ASCII NUL '\ 0'. Mon xargl n'est pas aussi complet qu'il devrait en valoir la peine d'être publié - d'autant plus que GNU possède des fonctionnalités au moins aussi bonnes.


2
GitHub ou ce n'est pas arrivé
Corey Goldberg

@CoreyGoldberg: Je suppose que cela ne s'est pas produit alors.
Jonathan Leffler

POSIX findn'a pas besoin xargsen premier lieu (et c'était déjà vrai il y a 11 ans).
jlliagre

2

Avec Bash (pas POSIX), vous pouvez utiliser la substitution de processus pour obtenir la ligne actuelle dans une variable. Cela vous permet d'utiliser des guillemets pour échapper les caractères spéciaux:

while read line ; do cp "$line" ~/bar ; done < <(find . | grep foo)

2

Pour moi, j'essayais de faire quelque chose d'un peu différent. Je voulais copier mes fichiers .txt dans mon dossier tmp. Les noms de fichiers .txt contiennent des espaces et des caractères d'apostrophe. Cela a fonctionné sur mon Mac.

$ find . -type f -name '*.txt' | sed 's/'"'"'/\'"'"'/g' | sed 's/.*/"&"/'  | xargs -I{} cp -v {} ./tmp/

1

Trouvez et si les versions xarg sur votre système ne prend pas en charge -print0et les -0commutateurs (par exemple trouver AIX et xargs) vous pouvez utiliser ce code à la recherche terriblement:

 find . -name "*foo*" | sed -e "s/'/\\\'/g" -e 's/"/\\"/g' -e 's/ /\\ /g' | xargs cp /your/dest

Ici sed prendra soin d'échapper aux espaces et aux guillemets pour xargs.

Testé sur AIX 5.3


1

J'ai créé un petit script wrapper portable appelé "xargsL" autour de "xargs" qui résout la plupart des problèmes.

Contrairement à xargs, xargsL accepte un chemin d'accès par ligne. Les chemins d'accès peuvent contenir n'importe quel caractère sauf (évidemment) les sauts de ligne ou NUL.

Aucune citation n'est autorisée ou prise en charge dans la liste des fichiers - vos noms de fichiers peuvent contenir toutes sortes d'espaces blancs, de barres obliques inverses, de barres obliques inverses, de caractères génériques de shell et autres - xargsL les traitera comme des caractères littéraux, sans aucun dommage.

Comme bonus supplémentaire, xargsL n'exécutera pas la commande une fois s'il n'y a pas d'entrée!

Notez la différence:

$ true | xargs echo no data
no data

$ true | xargsL echo no data # No output

Tous les arguments donnés à xargsL seront transmis à xargs.

Voici le script shell POSIX "xargsL":

#! /bin/sh
# Line-based version of "xargs" (one pathname per line which may contain any
# amount of whitespace except for newlines) with the added bonus feature that
# it will not execute the command if the input file is empty.
#
# Version 2018.76.3
#
# Copyright (c) 2018 Guenther Brunthaler. All rights reserved.
#
# This script is free software.
# Distribution is permitted under the terms of the GPLv3.

set -e
trap 'test $? = 0 || echo "$0 failed!" >& 2' 0

if IFS= read -r first
then
        {
                printf '%s\n' "$first"
                cat
        } | sed 's/./\\&/g' | xargs ${1+"$@"}
fi

Mettez le script dans un répertoire de votre $ PATH et n'oubliez pas de

$ chmod +x xargsL

le script là pour le rendre exécutable.


1

La version Perl de bill_starr ne fonctionnera pas bien pour les sauts de ligne intégrés (ne fait face qu'aux espaces). Pour ceux sur Solaris par exemple où vous n'avez pas les outils GNU, une version plus complète pourrait être (en utilisant sed) ...

find -type f | sed 's/./\\&/g' | xargs grep string_to_find

ajustez les arguments find et grep ou d'autres commandes selon vos besoins, mais le sed corrigera vos nouvelles lignes / espaces / tabulations intégrés.


1

J'ai utilisé la réponse de Bill Star légèrement modifiée sur Solaris:

find . -mtime +2 | perl -pe 's{^}{\"};s{$}{\"}' > ~/output.file

Cela mettra des guillemets autour de chaque ligne. Je n'ai pas utilisé l'option '-l' bien que cela aiderait probablement.

La liste de fichiers que j'allais peut-être pourrait avoir «-», mais pas de nouvelles lignes. Je n'ai pas utilisé le fichier de sortie avec d'autres commandes car je veux revoir ce qui a été trouvé avant de commencer à les supprimer massivement via xargs.


1

J'ai joué un peu avec cela, j'ai commencé à envisager de modifier les xargs et j'ai réalisé que pour le type de cas d'utilisation dont nous parlons ici, une simple réimplémentation en Python est une meilleure idée.

D'une part, avoir ~ 80 lignes de code pour le tout signifie qu'il est facile de comprendre ce qui se passe, et si un comportement différent est requis, vous pouvez simplement le pirater dans un nouveau script en moins de temps qu'il n'en faut pour obtenir une réponse quelque part comme Stack Overflow.

Voir https://github.com/johnallsup/jda-misc-scripts/blob/master/yargs et https://github.com/johnallsup/jda-misc-scripts/blob/master/zargs.py .

Avec yargs tel qu'écrit (et Python 3 installé), vous pouvez taper:

find .|grep "FooBar"|yargs -l 203 cp --after ~/foo/bar

pour copier 203 fichiers à la fois. (Ici, 203 n'est qu'un espace réservé, bien sûr, et l'utilisation d'un nombre étrange comme 203 indique clairement que ce nombre n'a pas d'autre signification.)

Si vous voulez vraiment quelque chose de plus rapide et sans avoir besoin de Python, prenez des zargs et des yargs comme prototypes et réécrivez en C ++ ou C.


0

Vous devrez peut-être grep répertoire Foobar comme:

find . -name "file.ext"| grep "FooBar" | xargs -i cp -p "{}" .

1
Par la page de manuel, -iest obsolète et -Idoit être utilisé à la place.
Acumenus

-1

Si vous utilisez Bash, vous pouvez convertir stdout en un tableau de lignes en mapfile:

find . | grep "FooBar" | (mapfile -t; cp "${MAPFILE[@]}" ~/foobar)

Les avantages sont les suivants:

  • Il est intégré, donc c'est plus rapide.
  • Exécutez la commande avec tous les noms de fichiers en une seule fois, donc c'est plus rapide.
  • Vous pouvez ajouter d'autres arguments aux noms de fichiers. Pour cp, vous pouvez également:

    find . -name '*FooBar*' -exec cp -t ~/foobar -- {} +
    

    cependant, certaines commandes n'ont pas une telle fonctionnalité.

Les désavantages:

  • Peut-être pas bien évolué s'il y a trop de noms de fichiers. (La limite? Je ne sais pas, mais j'avais testé avec un fichier de liste de 10 Mo qui comprend plus de 10000 noms de fichiers sans problème, sous Debian)

Eh bien ... qui sait si Bash est disponible sur OS X?

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.