Ejecutar múltiples comandos con xargs

310
cat a.txt | xargs -I % echo %

En el ejemplo anterior, xargs toma echo %como argumento de comando. Pero en algunos casos, necesito múltiples comandos para procesar el argumento en lugar de uno. Por ejemplo:

cat a.txt | xargs -I % {command1; command2; ... }

Pero xargs no acepta este formulario. Una solución que conozco es que puedo definir una función para ajustar los comandos, pero no es una canalización, no lo prefiero. ¿Hay otra solución?

Dagang
fuente
1
La mayoría de estas respuestas son vulnerabilidades de seguridad . Vea aquí para una respuesta potencialmente buena.
Mateen Ulhaq
2
Utilizo xargs para casi todo, pero odio poner comandos dentro de cadenas y crear explícitamente subcapas. Estoy a punto de aprender a conectar un whilebucle que puede contener múltiples comandos.
Sridhar Sarnobat

Respuestas:

443
cat a.txt | xargs -d $'\n' sh -c 'for arg do command1 "$arg"; command2 "$arg"; ...; done' _

... o, sin un uso inútil del gato :

<a.txt xargs -d $'\n' sh -c 'for arg do command1 "$arg"; command2 "$arg"; ...; done' _

Para explicar algunos de los puntos más finos:

  • El uso de en "$arg"lugar de %(y la ausencia de -Ien la xargslínea de comandos) es por razones de seguridad: pasar datos a shla lista de argumentos de la línea de comandos en lugar de sustituirlos en el código evita el contenido que los datos pueden contener (como $(rm -rf ~), por ejemplo , ejemplo malicioso) de ser ejecutado como código.

  • Del mismo modo, el uso de -d $'\n'es una extensión GNU que hace xargsque cada línea del archivo de entrada se trate como un elemento de datos separado. Esto o -0(que espera NUL en lugar de nuevas líneas) es necesario para evitar que los xargs intenten aplicar un análisis similar al shell (pero no del todo compatible) al flujo que lee. (Si no tiene GNU xargs, puede usar tr '\n' '\0' <a.txt | xargs -0 ...para obtener una lectura orientada a la línea sin -d).

  • El _es un marcador de posición para $0, de modo que otros valores de datos agregados por xargsconvertido $1y adelante, que resulta ser el conjunto predeterminado de valores sobre los que un forciclo itera.

Keith Thompson
fuente
58
Para aquellos que no estén familiarizados sh -c, tenga en cuenta que el punto y coma después de cada comando no es opcional, incluso si es el último comando de la lista.
Noah Sussman
66
Al menos en mi configuración, debe haber un espacio inmediatamente después de la inicial "{". No se requiere espacio antes de la llave final, pero como señaló el Sr. Sussman, necesita un punto y coma de cierre.
willdye
44
Esta respuesta previamente tenía llaves command1y command2; Más tarde me di cuenta de que no son necesarios.
Keith Thompson el
24
Para aclarar los comentarios anteriores sobre punto y coma, se requiere un punto y coma antes del cierre }: sh -c '{ command1; command2; }' -- but it's not required at the end of a command sequence that doesn't use braces: sh -c 'command1; comando2 ''
Keith Thompson
8
Si está incluido el %algún lugar personaje en su cadena pasada a sh -c, entonces este es propenso a las vulnerabilidades de seguridad: Un nombre de fichero que contiene $(rm -rf ~)'$(rm -rf ~)'(y eso es una subcadena perfectamente legal tener dentro de un nombre de archivo en sistemas de ficheros UNIX comunes!) Hará que alguien un muy mal día .
Charles Duffy
35

Con GNU Parallel puedes hacer:

cat a.txt | parallel 'command1 {}; command2 {}; ...; '

Mire los videos de introducción para obtener más información: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Por razones de seguridad, se recomienda que use su administrador de paquetes para instalar. Pero si no puede hacerlo, puede usar esta instalación de 10 segundos.

La instalación de 10 segundos intentará hacer una instalación completa; si eso falla, una instalación personal; si eso falla, una instalación mínima.

$ (wget -O - pi.dk/3 || lynx -source pi.dk/3 || curl pi.dk/3/ || \
   fetch -o - http://pi.dk/3 ) > install.sh
$ sha1sum install.sh | grep 3374ec53bacb199b245af2dda86df6c9
12345678 3374ec53 bacb199b 245af2dd a86df6c9
$ md5sum install.sh | grep 029a9ac06e8b5bc6052eac57b2c3c9ca
029a9ac0 6e8b5bc6 052eac57 b2c3c9ca
$ sha512sum install.sh | grep f517006d9897747bed8a4694b1acba1b
40f53af6 9e20dae5 713ba06c f517006d 9897747b ed8a4694 b1acba1b 1464beb4
60055629 3f2356f3 3e9c4e3c 76e3f3af a9db4b32 bd33322b 975696fc e6b23cfb
$ bash install.sh
Ole Tange
fuente
56
Instalar herramientas mediante la ejecución de scripts aleatorios desde sitios desconocidos es una práctica horrible. Oficiall paralelo tiene paquetes para las distribuciones populares, que se puede confiar (en cierta medida) de manera más que al azar wget | sh ...
mdrozdziel
44
Veamos cuál es el vector de ataque más fácil: Pi.dk está controlado por el autor de GNU Parallel, por lo que para atacar tendrías que entrar en el servidor o tomar DNS. Para hacerse cargo del paquete oficial de una distribución, a menudo puede ser voluntario para mantener el paquete. Entonces, si bien puede tener razón en general, parece que en este caso particular su comentario no está justificado.
Ole Tange
10
En la práctica, no sé si pi.dk pertenece al autor. En realidad, verificar que este sea el caso, pensar en cómo usar ssl en wget y verificar que este comando haga lo que se supone que debe hacer es un poco de trabajo. Su argumento de que el paquete oficial puede contener código malicioso es cierto, pero eso también es válido para el paquete wget.
Fabian
3
Esta podría no ser la mejor solución si cada uno de los comandos que OP desea ejecutar debe ser secuencial, ¿correcto?
IcarianComplex
2
@IcarianComplex Agregar -j1 arreglará eso.
Ole Tange
26

Este es solo otro enfoque sin xargs ni cat:

while read stuff; do
  command1 "$stuff"
  command2 "$stuff"
  ...
done < a.txt
hmontoliu
fuente
2
Buggy, según lo dado. A menos que lo borre IFS, ignorará los espacios en blanco iniciales y finales en los nombres de archivo; a menos que agregue -r, los nombres de archivo con barras diagonales invertidas tendrán esos caracteres ignorados.
Charles Duffy
No responde la pregunta. Se pregunta específicamente sobre xargs. (Esto es difícil de ampliar para hacer algo similar a xargsla -P<n>opción GNU ' )
Gert van den Berg
1
Esto funciona perfectamente bien. También puede usarlo como un comando canalizado como$ command | while read line; do c1 $line; c2 $line; done
Alexar
25

Puedes usar

cat file.txt | xargs -i  sh -c 'command {} | command2 {} && command3 {}'

{} = variable para cada línea en el archivo de texto

Ossama
fuente
8
Esto es inseguro. ¿Qué pasa si tu file.txtcontiene un dato con $(rm -rf ~)una subcadena?
Charles Duffy
19

Una cosa que hago es agregar a .bashrc / .profile esta función:

function each() {
    while read line; do
        for f in "$@"; do
            $f $line
        done
    done
}

entonces puedes hacer cosas como

... | each command1 command2 "command3 has spaces"

que es menos detallado que xargs o -exec. También puede modificar la función para insertar el valor de la lectura en una ubicación arbitraria en los comandos de cada uno, si también necesita ese comportamiento.

mwm
fuente
1
Respuesta subestimada, esto es extremadamente útil
charlesreid1
15

Prefiero el estilo que permite el modo de ejecución en seco (sin | sh):

cat a.txt | xargs -I % echo "command1; command2; ... " | sh

Funciona también con tuberías:

cat a.txt | xargs -I % echo "echo % | cat " | sh
brablc
fuente
2
Estos trabajos, hasta que desee xargs de GNU utilizar -Pla opción ... (Si no es así, que en su mayoría utilizan -execen find, ya que mis entradas son en su mayoría nombres de archivos)
Gert van den Berg
13

Un poco tarde para la fiesta.

Utilizo el siguiente formato para comprimir mis directorios con miles de archivos pequeños antes de migrar. Si no necesita comillas simples dentro de los comandos, debería funcionar.

Con alguna modificación, estoy seguro de que será útil para alguien. Probado en Cygwin(babun)

find . -maxdepth 1 ! -path . -type d -print0 | xargs -0 -I @@ bash -c '{ tar caf "@@.tar.lzop" "@@" && echo Completed compressing directory "@@" ; }'

find .Encuentre aquí
-maxdepth 1No entre en directorios secundarios
! -path .Excluir. / La ruta actual del directorio
-type dcoincide solo con los directorios
-print0Salida separada por bytes nulos \ 0
| xargsCanalizar a xargs La
-0entrada es bytes separados por nulos El
-I @@marcador de posición es @@. Reemplace @@ con entrada.
bash -c '...'Ejecutar comando Bash
{...}Agrupación de comandos
&&Ejecute el siguiente comando solo si el comando anterior salió con éxito (salida 0)

Final ;es importante, de lo contrario fallará.

Salida:

Completed compressing directory ./Directory1 with meta characters in it
Completed compressing directory ./Directory2 with meta characters in it
Completed compressing directory ./Directory3 with meta characters in it

Actualización de julio de 2018:

Si te encantan los hacks y jugar, aquí hay algo interesante:

echo "a b c" > a.txt
echo "123" >> a.txt
echo "###this is a comment" >> a.txt
cat a.txt
myCommandWithDifferentQuotes=$(cat <<'EOF'                                     
echo "command 1: $@"; echo 'will you do the fandango?'; echo "command 2: $@"; echo
EOF
)
< a.txt xargs -I @@ bash -c "$myCommandWithDifferentQuotes" -- @@

Salida:

command 1: a b c
will you do the fandango?
command 2: a b c

command 1: 123
will you do the fandango?
command 2: 123

command 1: ###this is a comment
will you do the fandango?
command 2: ###this is a comment

Explicación:
- Crear una única secuencia de comandos de línea y almacenarla en una variable
- xargs lee a.txty lo ejecuta como bashguión
- @@ asegura que cada vez que se pasa una línea completa
- Poner @@después --se asegura @@se toma como entrada posicional parámetro en bashorden, no una bashde inicio OPTION, es decir, como -csí mismo que significarun command

--es mágico, funciona con muchas otras cosas, es decir ssh, inclusokubectl

sdkks
fuente
1
He usado una configuración con este tipo de cosas: find . -type f -print0|xargs -r0 -n1 -P20 bash -c 'f="{}";ls -l "$f"; gzip -9 "$f"; ls -l "$f.gz"'(Es un poco más fácil al convertir bucles)
Gert van den Berg
1
Edición tardía para mi comentario anterior: (Si los nombres de archivo contienen comillas dobles, hay un problema de seguridad ...) (Parece que usar "$@"es la única forma de evitar eso ... (con -n1si desea limitar el número de parámetros))
Gert van den Berg
Cabe señalar que --el shell lo utiliza para decir que no se aceptarán más opciones. Esto permite que haya un -after the --too. Puede obtener un resultado muy interesante y confuso si no lo hace, por ejemplo, grep -rcuando lo incluye en el patrón- ! Sin embargo, la forma en que lo dices no aclara esto no explica realmente cómo funciona esto. Iirc es una cosa POSIX, pero de todos modos vale la pena señalar esto, creo. Solo algo a considerar. Y me encanta ese bono por cierto!
Pryftan
10

Esta parece ser la versión más segura.

tr '[\n]' '[\0]' < a.txt | xargs -r0 /bin/bash -c 'command1 "$@"; command2 "$@";' ''

( -0se puede eliminar y trreemplazar con una redirección (o el archivo se puede reemplazar con un archivo separado por nulos). Está principalmente allí ya que principalmente uso xargscon findcon -print0salida) (Esto también podría ser relevante en xargsversiones sin la -0extensión)

Es seguro, ya que args pasará los parámetros al shell como una matriz cuando lo ejecute. El shell (al menos bash) los pasaría como una matriz inalterada a los otros procesos cuando todos se obtengan utilizando["$@"][1]

Si usa ...| xargs -r0 -I{} bash -c 'f="{}"; command "$f";' '', la asignación fallará si la cadena contiene comillas dobles. Esto es cierto para cada variante que usa -io -I. (Debido a que se reemplaza en una cadena, siempre puede inyectar comandos insertando caracteres inesperados (como comillas, comillas o signos de dólar) en los datos de entrada)

Si los comandos solo pueden tomar un parámetro a la vez:

tr '[\n]' '[\0]' < a.txt | xargs -r0 -n1 /bin/bash -c 'command1 "$@"; command2 "$@";' ''

O con algo menos de procesos:

tr '[\n]' '[\0]' < a.txt | xargs -r0 /bin/bash -c 'for f in "$@"; do command1 "$f"; command2 "$f"; done;' ''

Si tienes GNU xargs u otro con la -Pextensión y desea ejecutar 32 procesos en paralelo, cada uno con no más de 10 parámetros para cada comando:

tr '[\n]' '[\0]' < a.txt | xargs -r0 -n10 -P32 /bin/bash -c 'command1 "$@"; command2 "$@";' ''

Esto debería ser robusto contra cualquier carácter especial en la entrada. (Si la entrada está separada por un valor nulo). La trversión obtendrá alguna entrada no válida si algunas de las líneas contienen líneas nuevas, pero eso es inevitable con un archivo separado por líneas nuevas.

El primer parámetro en blanco para bash -cse debe a esto: (Desde la bashpágina del manual ) (Gracias @clacke)

-c   If the -c option is present, then  commands  are  read  from  the  first  non-option  argument  com
     mand_string.   If there are arguments after the command_string, the first argument is assigned to $0
     and any remaining arguments are assigned to the positional parameters.  The assignment  to  $0  sets
     the name of the shell, which is used in warning and error messages.
Gert van den Berg
fuente
Esto debería funcionar incluso con comillas dobles en los nombres de archivo. Eso requiere un caparazón que soporte correctamente"$@"
Gert van den Berg
Te falta el argumento argv [0] para bash. bash -c 'command1 "$@"; command2 "$@";' arbitrarytextgoeshere
clacke 05 de
3
No se trata de lo que hace xargs. bashwith -ctoma primero (después de los comandos) un argumento que será el nombre del proceso, luego toma los argumentos posicionales. Intenta bash -c 'echo "$@" ' 1 2 3 4ver qué sale.
clacke 05 de
Es bueno tener una versión segura que no tenga Bobby-Tabled.
Mateen Ulhaq
8

Otra posible solución que funciona para mí es algo como:

cat a.txt | xargs bash -c 'command1 $@; command2 $@' bash

Tenga en cuenta el 'bash' al final: supongo que se pasa como argv [0] a bash. Sin esta sintaxis, se pierde el primer parámetro de cada comando. Puede ser cualquier palabra.

Ejemplo:

cat a.txt | xargs -n 5 bash -c 'echo -n `date +%Y%m%d-%H%M%S:` ; echo " data: " $@; echo "data again: " $@' bash
tavvit
fuente
55
Si no cita "$@", entonces está dividiendo cadenas y expandiendo globalmente la lista de argumentos.
Charles Duffy
2

Mi BKM actual para esto es

... | xargs -n1 -I % perl -e 'system("echo 1 %"); system("echo 2 %");'

Es lamentable que esto use perl, que es menos probable que se instale que bash; pero maneja más información que la respuesta aceptada. (Doy la bienvenida a una versión omnipresente que no depende de Perl).

La sugerencia de @ KeithThompson de

 ... | xargs -I % sh -c 'command1; command2; ...'

es genial, a menos que tenga el carácter de comentario de shell # en su entrada, en cuyo caso parte del primer comando y todo el segundo comando se truncarán.

Los hashes # pueden ser bastante comunes, si la entrada se deriva de una lista del sistema de archivos, como ls o find, y su editor crea archivos temporales con # en su nombre.

Ejemplo del problema:

$ bash 1366 $>  /bin/ls | cat
#Makefile#
#README#
Makefile
README

Vaya, aquí está el problema:

$ bash 1367 $>  ls | xargs -n1 -I % sh -i -c 'echo 1 %; echo 2 %'
1
1
1
1 Makefile
2 Makefile
1 README
2 README

Ahh, eso está mejor:

$ bash 1368 $>  ls | xargs -n1 -I % perl -e 'system("echo 1 %"); system("echo 2 %");'
1 #Makefile#
2 #Makefile#
1 #README#
2 #README#
1 Makefile
2 Makefile
1 README
2 README
$ bash 1369 $>  
Krazy Glew
fuente
55
# el problema se puede resolver fácilmente con comillas:ls | xargs -I % sh -c 'echo 1 "%"; echo 2 "%"'
gpl