Usar una lista generada de nombres de archivo como lista de argumentos, con espacios

16

Estoy tratando de invocar un script con una lista de nombres de archivos recopilados por find. Nada especial, solo algo así:

$ myscript `find . -name something.txt`

El problema es que algunos de los nombres de ruta contienen espacios, por lo que se dividen en dos nombres no válidos en la expansión de argumentos. Normalmente, rodearía los nombres con comillas, pero aquí están insertados por la expansión de la comilla trasera. Intenté filtrar la salida findy rodear cada nombre de archivo con comillas, pero para cuando bash los ve, es demasiado tarde para quitarlos y se tratan como parte del nombre de archivo:

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

Sí, esas son las reglas sobre cómo se procesa la línea de comando, pero ¿cómo puedo evitarla?

Esto es vergonzoso, pero no logro encontrar el enfoque correcto. Finalmente descubrí cómo hacerlo xargs -0 -n 10000... pero es un truco tan feo que todavía quiero preguntar: ¿cómo cito los resultados de la expansión de las comillas inversas o logro el mismo efecto de otra manera?

Editar: Yo estaba confundido por el hecho de que xargs hace recoger todos los argumentos en una sola lista de argumentos, a menos que se le dice lo contrario o los límites del sistema podría ser superado. ¡Gracias a todos por aclararme! Otros, tengan esto en cuenta al leer la respuesta aceptada porque no se señala muy directamente.

Acepté la respuesta, pero mi pregunta sigue siendo: ¿no hay alguna manera de proteger los espacios en la $(...)expansión de retroceso (o )? (Tenga en cuenta que la solución aceptada es una respuesta no bash).

alexis
fuente
Supongo que tendrías que cambiar qué usa el shell como separadores de nombre de archivo (por ejemplo, jugando con el valor de IFS, una forma posible es IFS=", nueva línea, "). Pero, ¿es necesario ejecutar el script sobre todos los nombres de archivo? De lo contrario, considere usar find para ejecutar el script para cada archivo.
njsg
Cambiar el IFS es una gran idea, ¡no lo había pensado! No es práctico para el uso de la línea de comandos, pero aún así. :-) Y sí, el objetivo es pasar todos los argumentos a la misma invocación de mi script.
alexis

Respuestas:

12

Puede hacer lo siguiente usando algunas implementaciones de findy de xargsesta manera.

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

o, simplemente, simplemente find:

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

Ejemplo

Digamos que tengo el siguiente directorio de muestra.

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

3 directories, 4 files

Ahora digamos que tengo esto para ./myscript.

#!/bin/bash

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

Ahora cuando ejecuto el siguiente comando.

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

O cuando uso la segunda forma así:

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

Detalles

encontrar + xargs

Los 2 métodos anteriores, aunque parecen diferentes, son esencialmente los mismos. El primero es tomar la salida de find, dividirla usando NULLs ( \0) a través del -print0interruptor para buscar. El xargs -0está diseñado específicamente para tomar entradas que se dividen usando NULL. GNU introdujo esa sintaxis no estándar findy xargshoy en día también se encuentra en algunos otros, como los BSD más recientes. Se -rrequiere la opción para evitar llamar myscriptsi findno encuentra nada con GNU findpero no con BSD.

NOTA: Todo este enfoque depende del hecho de que nunca pasará una cadena que sea extremadamente larga. Si es así, se ./myscriptiniciará una segunda invocación de con el resto de los resultados posteriores de find.

encontrar con +

Esa es la forma estándar (aunque solo se agregó relativamente recientemente (2005) a la implementación de GNU de find). La capacidad de hacer lo que estamos haciendo xargsestá literalmente integrada find. Por findlo tanto , encontrará una lista de archivos y luego pasará esa lista con tantos argumentos como pueda ajustarse al comando especificado después -exec(tenga en cuenta que {}solo puede durar justo antes +en este caso), ejecutando los comandos varias veces si es necesario.

¿Por qué no citar?

En el primer ejemplo, tomamos un atajo al evitar por completo los problemas con las citas, al usar NULL para separar los argumentos. Cuando xargsse le da esta lista, se le indica que se divida en los NULL que protegen eficazmente nuestros átomos de comando individuales.

En el segundo ejemplo, mantenemos los resultados internos para findque sepa qué es cada átomo de archivo y garantizamos que los manejemos adecuadamente, evitando así el negocio de citarlos.

Tamaño máximo de la línea de comando?

Esta pregunta surge de vez en cuando, así que como un bono la agrego a esta respuesta, principalmente para poder encontrarla en el futuro. Puede usar xargspara ver cuál es el límite del entorno:

$ 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
fuente
1
Gracias pero necesito pasar todos los argumentos a la misma invocación de mi script. Eso está en la descripción del problema, pero supongo que no dejé en claro que no es incidental.
alexis
@alexis: lea las respuestas nuevamente, están pasando todos los argumentos a una sola llamada de su script.
slm
¡Voy a ser condenado! No conocía el +argumento find(y también lo usas +en prosa, así que me perdí tu explicación la primera vez). Pero más aún, ¡no entendí lo que xargshace por defecto! En tres décadas de uso de Unix, nunca lo había usado hasta ahora, pero pensé que conocía mi caja de herramientas ...
alexis
@alexis: supuse que te habías perdido lo que decíamos. Sí xargses un demonio de una orden. Tienes que leerlo y findlas páginas de manual muchas veces para entender lo que pueden hacer. Mayo de los interruptores son contra-positivos entre sí, lo que se suma a la confusión.
slm
@alexis: también una cosa más para agregar a la caja de herramientas, no use las comillas inversas / comillas para ejecutar comandos anidados, use $(..)ahora en su lugar. Maneja automáticamente el anidamiento de comillas, etc. Los backticks están en desuso.
slm
3
find . -name something.txt -exec myscript {} +

En lo anterior, findencuentra todos los nombres de archivo coincidentes y los proporciona como argumentos para myscript. Esto funciona con nombres de archivos independientemente de los espacios o cualquier otro carácter extraño.

Si todos los nombres de archivo caben en una línea, entonces myscript se ejecuta una vez. Si la lista es demasiado larga para que la shell la maneje, find ejecutará myscript varias veces según sea necesario.

MÁS: ¿Cuántos archivos caben en una línea de comando? man finddice que lo findconstruye líneas de comando "de la misma manera que xargs construye su". Y man xargsque los límites dependen del sistema y que puede determinarlos ejecutando xargs --show-limits. ( getconf ARG_MAXTambién es una posibilidad). En Linux, el límite es típicamente (pero no siempre) alrededor de 2 millones de caracteres por línea de comando.

John1024
fuente
2

Algunas adiciones a la buena respuesta de @ slm.

La limitación en el tamaño de los argumentos está en la execve(2)llamada al sistema (en realidad, está en el tamaño acumulativo de los argumentos y las cadenas e indicadores del entorno). Si myscriptestá escrito en un lenguaje que su shell puede interpretar, entonces tal vez no necesite ejecutarlo , podría hacer que su shell lo interprete sin tener que ejecutar otro intérprete.

Si ejecuta el script como:

(. myscript x y)

Es como:

myscript x y

Excepto que está siendo interpretado por un hijo del shell actual, en lugar de ejecutarlo (lo que eventualmente implica ejecutar sh (o lo que la línea she-bang especifica, si corresponde) con aún más argumentos).

Ahora, obviamente, no se puede usar find -exec {} +con el .comando, ya que .es un comando incorporado del shell, tiene que ser ejecutado por el shell, no por find.

Con zsh, es fácil:

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

O:

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

Aunque con zsh, no necesitarías finden primer lugar ya que la mayoría de sus características están integradas en el zshglobbing.

bashSin embargo, las variables no pueden contener caracteres NUL, por lo que debe buscar otra forma. Una forma podría ser:

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

También puede usar el engrosamiento recursivo estilo zsh con globstaropción en bash4.0 y posterior:

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

Tenga en cuenta que **siguió enlaces simbólicos a directorios hasta que se corrigió en bash4.3. También tenga en cuenta que bashno implementa zshcalificadores globales, por lo que no obtendrá todas las características de findallí.

Otra alternativa sería usar GNU ls:

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

Los métodos anteriores también se pueden usar si desea asegurarse de que myscriptse ejecute solo una vez (si la lista de argumentos es demasiado grande, falla). En versiones recientes de Linux, puede aumentar e incluso eliminar esa limitación en la lista de argumentos con:

ulimit -s 1048576

(Tamaño de pila de 1GiB, un cuarto del cual se puede usar para la lista arg + env).

ulimit -s unlimited

(sin límite)

Stéphane Chazelas
fuente
1

En la mayoría de los sistemas, hay un límite en la longitud de una línea de comando que se pasa a cualquier programa, usando xargso -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.

Las invocaciones serán mucho menos, pero no se garantiza que sean una. Lo que debe hacer es leer los nombres de archivo separados por NUL en la secuencia de comandos de stdin, posible basado en un argumento de línea de comandos -o -. Haría algo como:

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

e implementar los argumentos de la opción en myscriptconsecuencia.

Timo
fuente
Sí, el sistema operativo impone un límite en el número / tamaño de los argumentos que se pueden pasar. En los sistemas Linux modernos, esto es (gigantesco) ( linux.die.net/man/2/execve ) (1/4 del tamaño de la pila, 0x7FFFFFFF argumentos). AFAIK bash en sí mismo no impone ningún límite. Mis listas son mucho más pequeñas, y mi problema fue causado por un malentendido o un mal recuerdo de cómo xargsfunciona. Su solución es de hecho la más robusta, pero en este caso es exagerada.
alexis
0

¿No hay alguna forma de proteger los espacios en la expansión de retroceso (o $ (...))?

No, no hay ¿Porqué es eso?

Bash no tiene forma de saber qué debe protegerse y qué no.

No hay matrices en el archivo / tubería de Unix. Es solo un flujo de bytes. El comando dentro de ``o $()genera una secuencia, que bash traga y trata como una sola cadena. En ese punto, solo tiene dos opciones: ponerlo entre comillas, para mantenerlo como una cadena, o ponerlo desnudo, para que bash lo divida de acuerdo con su comportamiento configurado.

Entonces, lo que debe hacer si desea una matriz es definir un formato de bytes que tenga una matriz, y eso es lo que les gusta xargsy findhacen las herramientas : si las ejecuta con el -0argumento, funcionan de acuerdo con un formato de matriz binaria que termina los elementos con el byte nulo, agregando semántica a la corriente de byte opaco.

Desafortunadamente, bashno se puede configurar para dividir cadenas en el byte nulo. Gracias a /unix//a/110108/17980 por mostrarnos que zshpuede.

xargs

Desea que su comando se ejecute una vez y dijo que eso xargs -0 -n 10000resuelve su problema. No lo hace, asegura que si tiene más de 10000 parámetros, su comando se ejecutará más de una vez.

Si desea que se ejecute estrictamente una vez o falle, debe proporcionar el -xargumento y un -nargumento más grande que el -sargumento (realmente: lo suficientemente grande como para que un montón completo de argumentos de longitud cero más el nombre del comando no encajen) El -stamaño). ( hombre xargs , ver extracto más abajo)

El sistema en el que estoy actualmente tiene una pila limitada a aproximadamente 8M, así que aquí está mi límite:

$ 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)

golpetazo

Si no desea involucrar un comando externo, el ciclo while-read que alimenta una matriz, como se muestra en /unix//a/110108/17980 , es la única forma en que bash divide las cosas en El byte nulo.

La idea de obtener el script ( . ... "$@" )para evitar el límite de tamaño de la pila es genial (lo intenté, ¡funciona!), Pero probablemente no sea importante para situaciones normales.

Usar un fd especial para la tubería de proceso es importante si desea leer algo más de stdin, pero de lo contrario no lo necesitará.

Entonces, la forma "nativa" más simple, para las necesidades cotidianas del hogar:

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

myscriptornonscript "${files[@]}"

Si desea que su árbol de procesos sea limpio y agradable de ver, este método le permite hacerlo exec mynonscript "${files[@]}", lo que elimina el proceso bash de la memoria y lo reemplaza con el comando llamado. xargssiempre permanecerá en la memoria mientras se ejecuta el comando llamado, incluso si el comando solo se ejecutará una vez.


Lo que habla en contra del método bash nativo es esto:

$ 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 no está optimizado para el manejo de matrices.


hombre xargs :

-n max-args

Utilice como máximo argumentos de max-args por línea de comando. Se usarán menos argumentos de max-args si se excede el tamaño (vea la opción -s), a menos que se proporcione la opción -x, en cuyo caso se cerrará xargs.

-s caracteres máximos

Utilice como máximo caracteres de caracteres máximos por línea de comando, incluidos el comando y los argumentos iniciales y los nulos de terminación en los extremos de las cadenas de argumentos. El valor permitido más grande depende del sistema y se calcula como el límite de longitud del argumento para exec, menos el tamaño de su entorno, menos 2048 bytes de margen. Si este valor es superior a 128 KB, se utiliza 128 KB como valor predeterminado; de lo contrario, el valor predeterminado es el máximo. 1 KB es de 1024 bytes.

-X

Salga si se excede el tamaño (vea la opción -s).

clacke
fuente
Gracias por todos los problemas, pero su premisa básica ignora el hecho de que bash normalmente utiliza un elaborado sistema de procesamiento de cotizaciones. Pero no en expansión entre comillas. Compare los siguientes (errores que ambos dan, pero muestran la diferencia): ls "what is this"vs ls `echo '"what is this"'` . Alguien descuidó implementar el procesamiento de cotizaciones para el resultado de las cotizaciones atrasadas.
alexis
Me alegra que las comillas inversas no procesen las cotizaciones. El hecho de que incluso separen palabras ha causado suficientes miradas confusas, rasguños de cabeza y fallas de seguridad en la historia de la informática moderna.
clacke
La pregunta es "¿No hay alguna forma de proteger los espacios en la $(...)expansión de retroceso (o )?", Por lo que parece apropiado ignorar el procesamiento que no se realiza en esa situación.
clacke
El formato de matriz de elementos con terminación nula es la forma más simple y, por lo tanto, más segura de expresar una matriz. Es una pena que bashno lo soporte de forma nativa como aparentemente lo zshhace.
clacke
De hecho, solo esta semana utilicé printf "%s\0"y xargs -0para enrutar una situación de cita donde una herramienta intermedia pasaría parámetros a través de una cadena analizada por un shell. Las citas siempre vuelven a morderte.
clacke