Cualquiera que haya pasado un rato en una terminal ha visto xargs en una respuesta de Stack Overflow, lo ha copiado y ha seguido con su vida. Pocos lo escriben de memoria. Y sin embargo es la pieza que permite conectar comandos que producen listas con comandos que esperan argumentos, que es justo donde las tuberías se quedan cortas.
El problema que resuelve
La tubería | conecta la salida estándar de un comando con la entrada estándar del siguiente. Funciona bien con programas pensados para leer de stdin, como grep:
cat files.txt | grep "error"
Pero muchos programas (rm, mv, git checkout, kill) no leen stdin: esperan los datos como argumentos. Esto no hace nada útil:
echo "file.txt" | rm
Ahí entra xargs, cuyo trabajo se resume en una frase: lee stdin y lo convierte en argumentos de otro comando.
echo "file.txt" | xargs rm
Por defecto trocea la entrada por espacios y saltos de línea y lanza el comando con todos los elementos de una vez (o en varias tandas, si no caben; más abajo se explica). Si no se indica comando, usa echo.
Casos básicos
Borrar los ficheros listados en un archivo o los que devuelva find:
cat to_delete.txt | xargs rm find . -name "*.log" | xargs rm
La alternativa sin xargs sería un bucle for f in $(find ...), que además tiene los mismos problemas con los espacios y es más largo.
-I: colocar el argumento donde haga falta
Normalmente los argumentos se añaden al final del comando. Con -I se define un marcador que xargs sustituye por cada elemento:
cat branches.txt | xargs -I {} git checkout {}
Dos detalles que conviene conocer. Primero, -I ejecuta el comando una vez por cada línea (implica -L 1) y solo separa por saltos de línea, de modo que una línea como un nombre llega entera, con su espacio. Segundo, el marcador puede ser cualquier cadena; {} es solo convención.
Un ejemplo muy repetido es mover los .bak a una carpeta de copias:
find . -name "*.bak" | xargs -I {} mv {} ./backup/{}
Cuidado: solo funciona si todos los ficheros están en el directorio actual. Con subdirectorios, {} vale ./sub/b.bak y el destino queda ./backup/./sub/b.bak, una ruta que no existe, y mv falla con «No such file or directory» (comprobado con GNU findutils 4.8). Lo habitual es quedarse solo con el nombre y excluir la propia carpeta de destino:
find . -name "*.bak" -not -path "./backup/*" -print0 | xargs -0 mv -t ./backup/
La opción -t (destino primero) es de GNU mv y evita necesitar el marcador. Si dos ficheros de subcarpetas distintas se llaman igual, el segundo sobrescribirá al primero, así que añadir -n o revisar los nombres antes no sobra.
-n: limitar los argumentos por ejecución
Sin opciones, todos los elementos van en una sola llamada. Con -n se fija el máximo de argumentos por llamada:
cat servers.txt | xargs -n 1 ping -c 1
Con cinco servidores se lanzan cinco ping, uno por cada nombre. -n 2 pasaría dos por llamada, y así sucesivamente.
-P: ejecución en paralelo
Es la opción que convierte a xargs en una herramienta seria para trabajos lentos e independientes:
cat urls.txt | xargs -n 1 -P 4 curl -O find . -name "*.csv" -print0 | xargs -0 -n 1 -P 8 python process.py
Hasta cuatro (u ocho) procesos a la vez, sin colas de trabajo ni herramientas adicionales. Matices útiles:
-P 0lanza tantos procesos como pueda; en la práctica, mejor-P "$(nproc)"para trabajos que usan CPU.- La salida de los procesos puede salir entremezclada y desordenada. En una prueba con cuatro tareas que terminan en orden inverso, la salida llegó 4, 3, 2, 1. Si el orden importa, cada proceso debería escribir a su propio fichero.
- Si algún comando falla,
xargsdevuelve el código 123; si termina por una señal, 125, y si el comando no se puede ejecutar, 126 o 127. Con-Plos fallos individuales no detienen al resto.
El patrón -print0 y -0
Hay un fallo silencioso en la mayoría de ejemplos que circulan: los nombres de fichero pueden contener espacios, y xargs separa por espacios. Un fichero llamado my report.csv se interpreta como dos argumentos, my y report.csv:
find . -name "*.csv" | xargs ls ls: cannot access './my': No such file or directory ls: cannot access 'report.csv': No such file or directory
La solución es que find separe los resultados con el carácter nulo (-print0) y que xargs lo interprete así (-0), ya que un nombre de fichero nunca puede contener ese byte:
find . -name "*.csv" -print0 | xargs -0 rm
Conviene convertirlo en hábito cada vez que se combinen ambos comandos. Es más importante aún con rm, donde un nombre mal partido puede apuntar a otra ruta distinta de la deseada.
Entrada vacía: -r
GNU xargs ejecuta el comando una vez aunque no llegue nada por stdin. Con rm -f es inocuo, pero con comandos que sin argumentos hacen otra cosa (por ejemplo, grep leyendo del teclado o un ls que lista el directorio actual) puede sorprender. La opción -r (--no-run-if-empty) lo evita:
: | xargs echo hola # imprime "hola" : | xargs -r echo hola # no imprime nada
En las versiones BSD y macOS el comportamiento por defecto ya es no ejecutar con entrada vacía.
Patrones que conviene tener a mano
Buscar en qué ficheros Java aparece una cadena (-l muestra solo los nombres):
find . -name "*.java" -print0 | xargs -0 grep -l "NullPointerException"
Reiniciar una lista de servicios, uno por uno:
printf '%sn' nginx postgresql redis | xargs -n 1 systemctl restart
Se usa printf en lugar de echo -e, cuyo comportamiento varía entre shells.
Renombrar de .jpeg a .jpg con una subshell para manipular la cadena:
find . -name "*.jpeg" -print0 | xargs -0 -I {} bash -c 'mv "$1" "${1%.jpeg}.jpg"' _ {}
El _ ocupa la posición de $0 y hace que el fichero llegue como $1; así se evita interpolar el nombre dentro del texto del script, que sería un problema si contiene comillas o símbolos. Probado con un fichero con espacios, funciona correctamente.
Contar líneas de varios ficheros:
find . -name "*.log" -print0 | xargs -0 wc -l
Si hay muchos ficheros, xargs parte la lista en varias ejecuciones para no superar el límite de longitud de argumentos del sistema (xargs --show-limits lo muestra; en la máquina de pruebas, unos 2 MB). En ese caso wc -l imprimirá más de una línea «total», una por tanda.
Alternativas: find -exec y -delete
Para casos simples, find no necesita xargs. La forma -exec ... {} + agrupa argumentos igual que xargs y gestiona bien los espacios en los nombres:
find . -name "*.log" -exec rm {} +
find . -name "*.log" -delete
xargs aporta valor cuando la lista no viene de find (un fichero, git ls-files, grep -l
), cuando se quiere paralelismo con -P o cuando se necesita controlar el reparto con -n. Para git ls-files también existe -z, que emite la lista separada por nulos y casa con xargs -0.
xargs o un bucle for
xargs encaja cuando hay una lista en stdin que alimentar a un comando, cuando se busca paralelismo o cuando se quiere una línea breve y componible. Un bucle for es mejor si hace falta lógica condicional por elemento, gestionar errores de cada caso por separado o si la operación es lo bastante compleja como para que la legibilidad pese más que la brevedad. Se podría resumir así: xargs está optimizado para el rendimiento y la composición, y el bucle para el control.
Resumen de opciones
Opción | Qué hace |
| Sustituye el marcador por cada línea; una ejecución por línea |
| Máximo de N argumentos por ejecución |
| Hasta N procesos en paralelo |
| Separa la entrada por el carácter nulo (pareja de |
| No ejecuta nada si la entrada está vacía (GNU) |
| Muestra cada comando antes de ejecutarlo |
| Pide confirmación antes de cada ejecución |
Una buena costumbre antes de lanzar algo destructivo es anteponer echo al comando (xargs -0 echo rm) o usar -t/-p para ver qué se va a ejecutar.
Imagen: Pexels / Rafael Minguet Delgado
