13 de Septiembre, 2007

Otra sobre expansión de argumentos en BASH

Hace ya algún tiempo comentaba diferencias entre SH (pdksh) y BASH, y hoy me he encontrado algo habitual cuando manejas directorios con muchos ficheros, o como me ha pasado a mi: liberando espacio de /tmp/.

Tenía gran cantidad de ficheros temporales, y otras cosas que no quería borrar, bueno... para eso están los patrones:

# rm /tmp/loquesea.*
-bash: /bin/rm: La lista de argumentos es demasiado larga

¡Anda! Y, ¿ahora qué hago? Bueno, lo más fácil es find(1) al rescate:

# find /tmp/ -name "loquesea.*" -exec rm {} \;

Esta solución hace que se ejecute el rm sobre cualquier fichero encontrado en /tmp/ o subdirectorios que cumpla el patrón indicado. Ojo que el patrón está entre comillas (no lo interpreta el shell, sino find), y veamos porqué la orden original tenía problemas.

Cuando indicamos en el shell un patrón de sustitución, BASH en este caso, reemplaza el patrón por una lista de ficheros que se ajustan a la descripción.

Eso quiere decir que si tenemos:

$ ls
fichero1  fichero2  fichero3  ficha

Al hace un rm fichero*, lo que realmente se ejecuta es: rm fichero1 fichero2 fichero3.

El comando rm(1) puede procesar listas de elementos a eliminar, pero el tamañano de la lista es limitado, y con los ficheros temporales que tenía esta mañana... he superado el límite.

Apuesto a que hay otras formas de solucionar la papeleta, si alguien se anima, que use los comentarios ;).

Anotación por Juan J. Martínez, clasificada en: scripting, unix, bash.

Hay 10 comentarios

Gravatar

Me resulta muy curiosa esta entrada en el blog porque hace muy poco tiempo hubo una discusión en la lista de correo del kernel de linux en el que un troll famoso ya por esos lugares comentaba este problema

http://lkml.org/lkml/2007/8/19/19

El encabronamiento general era que el tío en cuestión no es capaz de distinguir entre el sistema operativo y un elemento del mismo, como es la shell. Finalmente se implementó una extensión del gestor de memoria que permite tener listas de argumentos arbitrariamente largas (al menos hasta donde la memoria del sistema permita), que me figuro veremos en el kernel 2.6.23

http://lkml.org/lkml/2007/8/19/44

por plácido, en 2007-09-13 23:30:52

Gravatar

Aunque no sé si es positivo que se implementen cosas quizás innecesarias para darle en toda la boca al troll de turno :D

Por cierto que el primer enlace que apuntas tiene una alternativa a mi -exec que emplea xargs (aunque siendo puntillosos es menos eficiente, crea un proceso más por fichero).

por Juanjo, en 2007-09-14 08:10:12

Gravatar

De hecho siento contradecirte pero no, tu versión es bastante menos eficiente.

find . -name "*.whatever" -exec rm {} \;

eso lanza un rm por cada fichero que cumple el patrón.

find . -name "*.whatever" | xargs rm

eso lanza un find, un xargs y un solo rm para todos los ficheros (bueno, tantos como hagan falta para no exceder el tamaño de la línea de comandos)

En versiones actuales de find (al menos desde Etch para adelante) es posible sustituir el ; terminador de argumentos a -exec en find por un +, que tiene un efecto similar al de redirigir a xargs, es decir, concatena la salida y la pasa como argumentos al comando.

De todas formas, cuando se usa xargs es conveniente usar -print0 en find y -0 en xargs, para no tener problemas con los nombres de ficheros que tengan espacios.

por plácido, en 2007-09-14 22:28:57

Gravatar

A simple vista he pensado que xargs hacía un fork por cada linea que recogía de la entrada estándard (salida de find en este caso), pero realiza una sola ejecución pasando todo como argumentos (o varias ejecuciones, si son demasiados argumentos).

Al agrupar argumentos, es más eficiente que mi propuesta. Ya decía yo lo de puntilloso, creía que suponía un proceso más al menos (es decir, uno por rm más el del xargs).

Pero vamos, otra vez: a simple vista. A ver si alguien se anima a intentar mejorarlo (pero recurrir a la lista del kernel está feo :D).

por Juanjo, en 2007-09-15 02:33:41

Gravatar

Holas!
He visto tu entrada en http://www.planetabsd.es, y venía a comentarte lo mismo que ya veo te ha dicho Plácido; usando "-exec" no te libras del límite de ficheros máximo que puede manejar el shell. Para ello debes usar el comando "xargs" (y también por experiencia, pero con más de 500.000 ficheros que me las he tenido que ver).
Saludos y felicidades por tu weblog.

Have a nice day ;-)
TooManySecrets

por TooManySecrets, en 2007-09-15 19:00:00

Gravatar

ls -1 /tmp/loqusea.* | xargs -L 100 rm

xargs ejecutara el comando rm pasandole como parametro 100 nombres de fichero.

--
SegFault

por SegFault, en 2007-09-18 12:43:22

Gravatar

TooManySecrets:

con "find -exec" sí que te libras de la expansión de la shell, lo que se discutía era si era más eficiente que usar "xargs" en este caso concreto. Es decir, la línea que comenta Juanjo funciona perfectamente, simplemente no es eficiente.

SegFault:

si "rm /tmp/loquesea.*" falla porque la lista de argumentos es mayor que el tamaño máximo, entonces "ls /tmp/loquesea.*" también fallará ¿no? Simplemente te has llevado el problema a otro comando ;-)

por placido, en 2007-09-18 22:14:33

Gravatar

En el próximo kernel 2.6.23 este asunto quedará solucionado gracias la lista variable de argumentos.

Podeís verlo en el Changelog de kernelnewbies:
http://kernelnewbies.org/Linux_2_6_23

por funky, en 2007-09-23 02:00:50

Gravatar

Pues otra mas para el cuaderno de tips :

for i in `ls loquesea.*`; do rm $i; done :)

Cuando quieres eliminar mas de 4.000 ficheros no esta mal :P.

Saludos y felicitaciones por el blog :)

por lazaro, en 2007-09-24 14:12:35

Gravatar

Has cometido el mismo error que SegFault, trasladando el problema a otro comando (ls).

Lo explico otra vez: el shell reemplaza los patrones (un * en este caso) por los ficheros que encajan, expandiendo la linea de comandos con tantos parámetros como ocurrencias.

Una vez se tiene la linea de ejecución definitiva, se pasa a la llamada al sistema para que ejecute y... falla, porque hay más argumentos que el tope soportado por esa llamada.

Las soluciones válidas (por ahora :D) pasan por usar find y que éste ejecute n veces rm o (como apunta Plácido), usar xargs para que pase el número máximo de parámetros permitidos por ejecución de rm, minimizando así el número de procesos necesarios.

por Juanjo, en 2007-09-24 15:44:50

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: