¿Cómo puedo usar xargs para copiar archivos que tienen espacios y comillas en sus nombres?

232

Estoy tratando de copiar un montón de archivos debajo de un directorio y varios archivos tienen espacios y comillas simples en sus nombres. Cuando trato de encadenar findy grepcon xargs, me sale el siguiente error:

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

¿Alguna sugerencia para un uso más robusto de xargs?

Esto está en Mac OS X 10.5.3 (Leopard) con BSD xargs.

Drew Stephens
fuente
2
El mensaje de error xargs de GNU para esto con un nombre de archivo que contiene una comilla simple es bastante más útil: "xargs: comilla simple no coincidente; por defecto, las comillas son especiales para xargs a menos que use la opción -0".
Steve Jessop
3
GNU xargs también tiene la --delimiteropción ( -d). Pruébelo \ncomo delimitador, esto evita la xargsseparación de líneas con espacios en varias palabras / argumentos.
MattBianco

Respuestas:

199

Puedes combinar todo eso en un solo findcomando:

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

Esto manejará nombres de archivos y directorios con espacios en ellos. Puede usar -namepara obtener resultados sensibles a mayúsculas y minúsculas.

Nota: La --bandera pasada a cpevita que procese archivos que comienzan -como opciones.

Godbyk
fuente
70
La gente usa xargs porque generalmente es más rápido llamar a un ejecutable 5 veces con 200 argumentos cada vez que llamarlo 1000 veces con un argumento cada vez.
tzot
12
La respuesta de Chris Jester-Young debería ser la "buena respuesta" allí ... Por cierto, esta solución no funciona si un nombre de archivo comienza con "-". Al menos, necesita "-" después de cp.
Keltia el
11
Ejemplo de velocidad: más de 829 archivos, el método "find -exec" tardó 26 segundos, mientras que la herramienta de método "find -print0 | xargs --null" 0.7 segundos. Diferencia significativa.
Peter Porter
77
@tzot Un comentario tardío, pero de todos modos, xargsno es necesario para abordar el problema que está describiendo, findya lo admite con la -exec +puntuación.
jlliagre
3
no responde la pregunta de cómo lidiar con los espacios
Ben Glasser
117

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

No sé si los grepapoyos --null, ni si los xargsapoyos -0, en Leopard, pero en GNU, todo está bien.

Chris Jester-Young
fuente
1
Leopard admite "-Z" (es GNU grep) y, por supuesto, find (1) y xargs (1) admiten "-0".
Keltia el
1
En OS X 10.9 grep -{z|Z}significa "comportarse como zgrep" (descomprimir) y no el "imprimir un byte cero después de cada nombre de archivo". Úselo grep --nullpara lograr esto último.
bassim
44
¿Qué tiene de malo find . -name 'FooBar' -print0 | xargs -0 ...?
Quentin Pradet
1
@QuentinPradet Obviamente, para una cadena fija como "FooBar", -nameo -pathfunciona bien. El OP ha especificado el uso de grep, presumiblemente porque quieren filtrar la lista usando expresiones regulares.
Chris Jester-Young
1
@ Hola, Angel. Es exactamente por eso que lo uso xargs -0 junto con find -print0 . El último imprime los nombres de archivo con un terminador NUL y el primero recibe los archivos de esa manera. ¿Por qué? Los nombres de archivo en Unix pueden contener caracteres de nueva línea. Pero no pueden contener caracteres NUL.
Chris Jester-Young
92

La forma más fácil de hacer lo que quiere el póster original es cambiar el delimitador de cualquier espacio en blanco al carácter de final de línea como este:

find whatever ... | xargs -d "\n" cp -t /var/tmp
usuario87601
fuente
44
Esta respuesta es simple, efectiva y directa: el delimitador predeterminado establecido para xargs es demasiado amplio y debe reducirse para lo que OP quiere hacer. Sé esto de primera mano porque me encontré exactamente con el mismo problema hoy haciendo algo similar, excepto en Cygwin. Si hubiera leído la ayuda para el comando xargs, podría haber evitado algunos dolores de cabeza, pero su solución me lo arregló. Gracias ! (Sí, OP estaba en MacOS usando BSD xargs, que no uso, pero espero que el parámetro xargs "-d" exista en todas las versiones).
Etienne Delavennat
77
Buena respuesta pero no funciona en Mac. En su lugar, puede canalizar el hallazgo en sed -e 's_\(.*\)_"\1"_g'a las cotizaciones de la fuerza de todo el nombre del archivo
ishahak
10
Esta debería ser la respuesta aceptada. La pregunta era sobre el uso xargs.
Mohammad Alhashash
2
Obtengoxargs: illegal option -- d
nehem
1
Vale la pena señalar que los nombres de archivo pueden contener un carácter de nueva línea en muchos sistemas * nix. Es poco probable que se encuentre con esto en la naturaleza, pero si está ejecutando comandos de shell en una entrada no confiable, esto podría ser una preocupación.
Soren Bjornstad
71

Esto es más eficiente ya que no ejecuta "cp" varias veces:

find -name '*FooBar*' -print0 | xargs -0 cp -t ~/foo/bar
Tometzky
fuente
1
Esto no funcionó para mí. Trató de cp ~ / foo / bar en lo que sea que encuentre, pero no al contrario
Shervin Asgari
13
El indicador -t para cp es una extensión de GNU, AFAIK, y no está disponible en OS X. Pero si lo fuera, funcionaría como se muestra en esta respuesta.
metamatt
2
Estoy usando Linux Gracias por el interruptor '-t'. Eso era lo que me faltaba :-)
Vahid Pazirandeh
59

Tuve el mismo problema. Así es como lo resolví:

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

Solía sedsustituir cada línea de entrada con la misma línea, pero rodeado de comillas dobles. Desde la sedpágina del manual, " ... Un ampersand (` `& '') que aparece en el reemplazo se reemplaza por la cadena que coincide con el RE ... " - en este caso .*, la línea completa.

Esto resuelve el xargs: unterminated quoteerror.

oyouareatubeo
fuente
3
Estoy en Windows y estoy usando gnuwin32, así que tuve que usarlo sed s/.*/\"&\"/para que funcione.
Pat
Sí, pero presumiblemente esto no manejaría los nombres de archivo con "in, a menos que sed también cite comillas.
artfulrobot
¡Usar sedes genial y por ahora la solución correcta sin tener que volver a escribir el problema!
entonio
53

Este método funciona en Mac OS X v10.7.5 (Lion):

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

También probé la sintaxis exacta que publicaste. Eso también funcionó bien en 10.7.5.

the_minted
fuente
44
Esto funciona, pero -Iimplica -L 1(así lo dice el manual), lo que significa que el comando cp se ejecuta una vez por archivo = v lento.
artfulrobot
xargs -J% cp% <directorio de destino> Es posiblemente más eficiente en OSX.
Walker D
3
Lo siento, pero esto es INCORRECTO. Primero produce exactamente el error que el TO quería evitar. Debe usar find ... -print0y xargs -0para trabajar alrededor de xargs "por defecto las comillas son especiales". En segundo lugar, generalmente use '{}'no {}en comandos pasados ​​a xargs, para proteger contra espacios y caracteres especiales.
Andreas Spindler
3
Lo siento Andreas Spindler, no estoy familiarizado con xargs y encontré esta línea después de experimentar un poco. Parece funcionar para la mayoría de las personas que lo comentaron y lo votaron. ¿Te importaría entrar un poco más en detalles sobre qué tipo de error produce? Además, ¿le importaría publicar la entrada exacta que cree que sería más correcta? Gracias.
the_minted
12

Simplemente no lo uses xargs. Es un programa ordenado, pero no funciona bien findcuando se trata de casos no triviales.

Aquí hay una solución portátil (POSIX), es decir, una que no requiere find, xargso cpextensiones específicas de GNU:

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

Tenga en cuenta el final en +lugar de lo más habitual ;.

Esta solución:

  • maneja correctamente archivos y directorios con espacios incrustados, nuevas líneas o cualquier personaje exótico.

  • funciona en cualquier sistema Unix y Linux, incluso aquellos que no proporcionan el kit de herramientas GNU.

  • no utiliza xargscuál es un programa agradable y útil, pero requiere demasiados ajustes y características no estándar para manejar correctamente la findsalida.

  • También es más eficiente (leer más rápido ) que las respuestas aceptadas y la mayoría, si no todas, las demás.

Tenga en cuenta también que, a pesar de lo que se indica en algunas otras respuestas o comentarios, la cita {}es inútil (a menos que esté utilizando el fishshell exótico ).

jlliagre
fuente
1
@PeterMortensen Probablemente pases por alto el final más. findpuede hacer lo que xargshace sin gastos generales.
jlliagre
8

Considere el uso de la opción de línea de comando --null para xargs con la opción -print0 en find.

Shannon Nelson
fuente
8

Para aquellos que dependen de comandos, además de encontrar, por ejemplo ls:

find . | grep "FooBar" | tr \\n \\0 | xargs -0 -I{} cp "{}" ~/foo/bar
Aleksandr Guidrevitch
fuente
1
Funciona pero lento porque -Iimplica-L 1
artfulrobot
6
find | perl -lne 'print quotemeta' | xargs ls -d

Creo que esto funcionará de manera confiable para cualquier carácter, excepto el avance de línea (y sospecho que si tiene avances de línea en sus nombres de archivo, entonces tiene problemas peores que este). No requiere GNU findutils, solo Perl, por lo que debería funcionar prácticamente en cualquier lugar.

mavit
fuente
¿Es posible tener un salto de línea en un nombre de archivo? Nunca lo oí.
mtk
2
De hecho, es. Prueba, por ejemplo,mkdir test && cd test && perl -e 'open $fh, ">", "this-file-contains-a-\n-here"' && ls | od -tx1
mavit
1
|perl -lne 'print quotemeta'es exactamente lo que he estado buscando. Otras publicaciones aquí no me ayudaron porque en lugar de lo findque necesitaba usar grep -rlpara reducir enormemente la cantidad de archivos PHP a solo los infectados por malware.
Marcos
perl y quotemeta son mucho más generales que print0 / -0 - gracias por la solución general para canalizar archivos con espacios
bmike
5

He descubierto que la siguiente sintaxis funciona bien para mí.

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

En este ejemplo, estoy buscando los 200 archivos más grandes de más de 1,000,000 de bytes en el sistema de archivos montado en "/ usr / pcapps".

El delineador de línea Perl entre "find" y "xargs" escapa / cita cada espacio en blanco para que "xargs" pase cualquier nombre de archivo con espacios en blanco incrustados a "ls" como argumento único.

Peter Mortensen
fuente
3

Reto de marco: estás preguntando cómo usar xargs La respuesta es: no usas xargs, porque no lo necesitas.

El comentario deuser80168 describe una forma de hacer esto directamente con cp, sin llamar a cp para cada archivo:

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

Esto funciona porque:

  • la cp -tbandera permite dar el directorio de destino cerca del principio cp, en lugar de cerca del final. De man cp:
   -t, --target-directory=DIRECTORY
         copy all SOURCE arguments into DIRECTORY
  • La --bandera le dice cpque interprete todo después como un nombre de archivo, no como una bandera, por lo que los archivos que comienzan con -o --no confunden cp; aún necesita esto porque los caracteres -/ --son interpretados por cp, mientras que cualquier otro carácter especial es interpretado por el shell.

  • La find -exec command {} +variante esencialmente hace lo mismo 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.

Al usar esto en find directamente, esto evita la necesidad de una tubería o una invocación de shell, por lo que no necesita preocuparse por los caracteres desagradables en los nombres de archivo.

gerrit
fuente
Increíble descubrimiento, no tenía idea! "-exec utility [argumento ...] {} + Igual que -exec, excepto que` `{} '' se reemplaza con la mayor cantidad de rutas posibles para cada invocación de la utilidad. Este comportamiento es similar al de xargs (1 ) ". en la implementación BSD.
conny
2

Tenga en cuenta que la mayoría de las opciones discutidas en otras respuestas no son estándar en plataformas que no utilizan las utilidades GNU (Solaris, AIX, HP-UX, por ejemplo). Ver el POSIX especificación para el comportamiento xargs 'estándar'.

También encuentro que el comportamiento de xargs por el cual ejecuta el comando al menos una vez, incluso sin entrada, es una molestia.

Escribí mi propia versión privada de xargs (xargl) para tratar los problemas de espacios en los nombres (solo las nuevas líneas se separan, aunque la combinación 'find ... -print0' y 'xargs -0' es bastante clara dado que los nombres de archivos no pueden contienen caracteres ASCII NUL '\ 0'. Mi xargl no está tan completo como debería valer la pena publicar, especialmente porque GNU tiene instalaciones que son al menos tan buenas.

Jonathan Leffler
fuente
2
GitHub o no sucedió
Corey Goldberg
@ CoreyGoldberg: Supongo que no sucedió entonces.
Jonathan Leffler
POSIX findno necesita xargsen primer lugar (y eso ya era cierto hace 11 años).
jlliagre
2

Con Bash (no POSIX) puede usar la sustitución de procesos para obtener la línea actual dentro de una variable. Esto le permite usar comillas para escapar de caracteres especiales:

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

Para mí, estaba tratando de hacer algo un poco diferente. Quería copiar mis archivos .txt en mi carpeta tmp. Los nombres de archivo .txt contienen espacios y caracteres de apóstrofe. Esto funcionó en mi Mac.

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

Si las versiones find y xarg en su sistema no son compatibles -print0y -0cambian (por ejemplo, AIX find y xargs), puede usar este código de aspecto terrible:

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

Aquí sed se encargará de escapar de los espacios y las comillas para xargs.

Probado en AIX 5.3

Jan Ptáčník
fuente
1

Creé un pequeño script de contenedor portátil llamado "xargsL" alrededor de "xargs" que aborda la mayoría de los problemas.

Al contrario de xargs, xargsL acepta un nombre de ruta por línea. Los nombres de ruta pueden contener cualquier carácter excepto (obviamente) nueva línea o bytes NUL.

No se permite ni admite el uso de comillas en la lista de archivos: sus nombres de archivo pueden contener todo tipo de espacios en blanco, barras diagonales inversas, comillas invertidas, caracteres comodín de shell y similares: xargsL los procesará como caracteres literales, sin causar daño.

Como una característica adicional, ¡xargsL no ejecutará el comando una vez si no hay entrada!

Tenga en cuenta la diferencia:

$ true | xargs echo no data
no data

$ true | xargsL echo no data # No output

Cualquier argumento dado a xargsL se pasará a xargs.

Aquí está el script de 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

Ponga el script en algún directorio en su $ PATH y no olvide

$ chmod +x xargsL

el script allí para hacerlo ejecutable.

Guenther Brunthaler
fuente
1

La versión Perl de bill_starr no funcionará bien para líneas nuevas incrustadas (solo hace frente a espacios). Para aquellos en, por ejemplo, Solaris donde no tiene las herramientas GNU, una versión más completa podría ser (usando sed) ...

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

ajuste los argumentos find y grep u otros comandos según lo requiera, pero sed arreglará sus nuevas líneas / espacios / pestañas incrustadas.

Peter Mortensen
fuente
1

Solía respuesta de Bill estrella ligeramente modificada en Solaris:

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

Esto pondrá comillas alrededor de cada línea. No utilicé la opción '-l' aunque probablemente ayudaría.

Sin embargo, la lista de archivos a la que iba podría tener '-', pero no nuevas líneas. No he usado el archivo de salida con ningún otro comando, ya que quiero revisar lo que se encontró antes de comenzar a eliminarlos masivamente a través de xargs.

Carl Yamamoto-Furst
fuente
1

Jugué un poco con esto, comencé a contemplar la modificación de xargs y me di cuenta de que para el tipo de caso de uso del que estamos hablando aquí, una simple reimplementación en Python es una mejor idea.

Por un lado, tener ~ 80 líneas de código para todo significa que es fácil darse cuenta de lo que está sucediendo, y si se requiere un comportamiento diferente, puede piratearlo en un nuevo script en menos tiempo del necesario para obtenerlo. una respuesta en algún lugar como Stack Overflow.

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

Con los hilos como están escritos (y Python 3 instalado) puede escribir:

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

hacer la copia de 203 archivos a la vez. (Aquí 203 es solo un marcador de posición, por supuesto, y el uso de un número extraño como 203 deja en claro que este número no tiene otra importancia).

Si realmente quieres algo más rápido y sin la necesidad de Python, toma zargs e hilos como prototipos y reescribe en C ++ o C.

John Allsup
fuente
0

Puede que necesite grep directorio de Foobar como:

find . -name "file.ext"| grep "FooBar" | xargs -i cp -p "{}" .
Fred
fuente
1
Según la página de manual, -iestá en desuso y -Idebe usarse en su lugar.
Acumenus
-1

Si está utilizando Bash, puede convertir stdout en una matriz de líneas de la siguiente manera mapfile:

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

Los beneficios son:

  • Está integrado, por lo que es más rápido.
  • Ejecute el comando con todos los nombres de archivo al mismo tiempo, por lo que es más rápido.
  • Puede agregar otros argumentos a los nombres de archivo. Para cp, usted también puede:

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

    sin embargo, algunos comandos no tienen esa característica.

Las desventajas:

  • Tal vez no escale bien si hay demasiados nombres de archivo. (¿El límite? No lo sé, pero lo probé con un archivo de lista de 10 MB que incluye más de 10000 nombres de archivos sin problemas, en Debian)

Bueno ... ¿quién sabe si Bash está disponible en OS X?

Xiè Jìléi
fuente