Exécution de plusieurs commandes avec xargs

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

Dans l'exemple ci-dessus, xargs prend echo %comme argument de commande. Mais dans certains cas, j'ai besoin de plusieurs commandes pour traiter l'argument au lieu d'une. Par exemple:

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

Mais xargs n'accepte pas ce formulaire. Une solution que je connais est que je peux définir une fonction pour encapsuler les commandes, mais ce n'est pas un pipeline, je ne le préfère pas. Y a-t-il une autre solution?

Dagang
la source
1
La plupart de ces réponses sont des failles de sécurité . Voir ici pour une réponse potentiellement bonne.
Mateen Ulhaq
2
J'utilise xargs pour presque tout, mais je déteste mettre des commandes à l'intérieur de chaînes et créer explicitement des sous-coquilles. Je suis sur le point d'apprendre à diriger une whileboucle qui peut contenir plusieurs commandes.
Sridhar Sarnobat

Réponses:

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

... ou, sans utilisation inutile de chat :

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

Pour expliquer certains des points les plus fins:

  • L'utilisation de "$arg"au lieu de %(et l'absence de -Idans la xargsligne de commande) est pour des raisons de sécurité: la transmission de données sur shla liste d'arguments de ligne de commande au lieu de la remplacer dans le code empêche le contenu que les données peuvent contenir (par exemple $(rm -rf ~), pour prendre un exemple malveillant) d'être exécuté en tant que code.

  • De même, l'utilisation de -d $'\n'est une extension GNU qui fait que xargschaque ligne du fichier d'entrée est traitée comme une donnée distincte. Soit ceci, soit -0(qui attend des NUL au lieu de sauts de ligne) est nécessaire pour empêcher les xargs d'essayer d'appliquer une analyse de type shell (mais pas tout à fait compatible shell) au flux qu'il lit. (Si vous n'avez pas de xargs GNU, vous pouvez utiliser tr '\n' '\0' <a.txt | xargs -0 ...pour obtenir une lecture orientée ligne sans -d).

  • Le _est un espace réservé pour $0, de sorte que les autres valeurs de données ajoutées par xargsdeviennent $1et à partir de, ce qui s'avère être l'ensemble de valeurs par défaut sur lequel une forboucle itère.

Keith Thompson
la source
58
Pour ceux qui ne connaissent pas sh -c- notez que le point-virgule après chaque commande n'est pas facultatif, même s'il s'agit de la dernière commande de la liste.
Noah Sussman
6
Au moins sur ma configuration, il doit y avoir un espace immédiatement après le "{" initial. Aucun espace n'est requis avant l'accolade terminale, mais comme l'a noté M. Sussman, vous avez besoin d'un point-virgule de fermeture.
willdye
4
Cette réponse avait précédemment des accolades bouclées autour command1et command2; J'ai réalisé plus tard qu'ils n'étaient pas nécessaires.
Keith Thompson
24
Pour clarifier les commentaires ci-dessus sur les points-virgules, un point-virgule est requis avant la fermeture }: sh -c '{ command1; command2; }' -- but it's not required at the end of a command sequence that doesn't use braces: sh -c 'command1; command2'`
Keith Thompson
8
Si vous incluez le %caractère quelque part dans votre chaîne passée sh -c, cela est sujet à des failles de sécurité: un nom de fichier contenant $(rm -rf ~)'$(rm -rf ~)'(et c'est une sous-chaîne parfaitement légale à avoir dans un nom de fichier sur des systèmes de fichiers UNIX courants!) Causera une très mauvaise journée à quelqu'un .
Charles Duffy
35

Avec GNU Parallel, vous pouvez faire:

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

Regardez les vidéos d'introduction pour en savoir plus: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Pour des raisons de sécurité, il est recommandé d'utiliser votre gestionnaire de packages pour l'installation. Mais si vous ne pouvez pas le faire, vous pouvez utiliser cette installation de 10 secondes.

L'installation de 10 secondes tentera de faire une installation complète; si cela échoue, une installation personnelle; si cela échoue, une installation minimale.

$ (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
la source
56
L'installation d'outils via l'exécution de scripts aléatoires à partir de sites inconnus est une pratique horrible. Parallel a des packages oficiall pour les distributions populaires, qui peuvent être faites confiance (dans une certaine mesure) bien plus que aléatoire wget | sh ...
mdrozdziel
4
Voyons quel est le vecteur d'attaque le plus simple: Pi.dk est contrôlé par l'auteur de GNU Parallel, donc pour attaquer, il vous faudrait pénétrer dans le serveur ou reprendre DNS. Pour reprendre le package officiel d'une distribution, vous pouvez souvent simplement vous porter volontaire pour maintenir le package. Donc, bien que vous ayez raison en général, il semble que dans ce cas particulier, votre commentaire ne soit pas justifié.
Ole Tange
10
En pratique, je ne sais pas que pi.dk appartient à l'auteur. Vérifier réellement que c'est le cas, penser à comment utiliser ssl dans wget et vérifier que cette commande fait ce qu'elle est censée faire est un peu de travail. Votre argument selon lequel le package officiel peut contenir du code malveillant est vrai, mais cela vaut également pour le package wget.
Fabian
3
Ce n'est peut-être pas la meilleure solution si chacune des commandes que OP souhaite exécuter doit être séquentielle, n'est-ce pas?
IcarianComplex
2
@IcarianComplex L'ajout de -j1 corrigera cela.
Ole Tange
26

Ceci est juste une autre approche sans xargs ni chat:

while read stuff; do
  command1 "$stuff"
  command2 "$stuff"
  ...
done < a.txt
hmontoliu
la source
2
Buggy, comme indiqué. Sauf si vous effacez IFS, il ignorera les espaces de début et de fin dans les noms de fichiers; sauf si vous ajoutez -r, les noms de fichiers avec des barres obliques inverses auront ces caractères ignorés.
Charles Duffy
Ne répond pas à la question. Il a spécifiquement demandé xargs. (Ceci est difficile à développer pour faire quelque chose de similaire à xargsl' -P<n>option GNU )
Gert van den Berg
1
Cela fonctionne parfaitement bien. Vous pouvez également l'utiliser comme une commande canalisée comme$ command | while read line; do c1 $line; c2 $line; done
Alexar
25

Vous pouvez utiliser

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

{} = variable pour chaque ligne du fichier texte

Ossama
la source
8
Ce n'est pas sûr. Et si votre file.txtcontient une donnée avec $(rm -rf ~)comme sous-chaîne?
Charles Duffy
19

Une chose que je fais est d'ajouter à .bashrc / .profile cette fonction:

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

alors vous pouvez faire des choses comme

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

qui est moins verbeux que xargs ou -exec. Vous pouvez également modifier la fonction pour insérer la valeur de la lecture à un emplacement arbitraire dans les commandes de chacun, si vous avez également besoin de ce comportement.

mwm
la source
1
Réponse sous
estimée
15

Je préfère le style qui permet le mode marche à sec (sans | sh):

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

Fonctionne également avec les tuyaux:

cat a.txt | xargs -I % echo "echo % | cat " | sh
brablc
la source
2
Cela fonctionne, jusqu'à ce que vous vouliez utiliser l' -Poption GNU xargs ... (Sinon, j'utilise principalement -execon find, car mes entrées sont principalement des noms de fichiers)
Gert van den Berg
13

Un peu tard pour la fête.

J'utilise le format ci-dessous pour compresser mes répertoires avec des milliers de petits fichiers avant de migrer. Si vous n'avez pas besoin de guillemets simples dans les commandes, cela devrait fonctionner.

Avec quelques modifications, je suis sûr que cela sera utile pour quelqu'un. Testé à Cygwin(babun)

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

find .Trouver ici
-maxdepth 1Ne pas aller dans les répertoires enfants
! -path .Exclure. / Le chemin du répertoire actuel
-type dcorrespond uniquement aux répertoires
-print0Sortie séparée par des octets nuls \ 0
| xargsTuyau vers xargs L'
-0entrée est octets séparés par des caractères nuls L'espace
-I @@réservé est @@. Remplacez @@ par une entrée.
bash -c '...'Exécuter la commande Bash
{...}Groupement de commandes
&&Exécuter la commande suivante uniquement si la commande précédente s'est terminée avec succès (sortie 0)

La finale ;est importante, sinon elle échouera.

Production:

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

Mise à jour de juillet 2018:

Si vous aimez les hacks et jouer, voici quelque chose d'intéressant:

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" -- @@

Production:

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

Explication:
- Créez un seul script de liner et stockez-le dans une variable
- le xargs lit a.txtet l'exécute en tant que bashscript
- @@ s'assure à chaque fois qu'une ligne entière est passée
- Le fait de mettre @@après --s'assure @@est pris comme entrée de paramètre positionnel pour la bashcommande, pas un bashdébut OPTION, c'est-à-dire comme -clui ce qui signifierun command

--est magique, il fonctionne avec beaucoup d'autres choses, c'est ssh-à- dire mêmekubectl

sdkks
la source
1
J'ai utilisé une configuration avec ce type de chose: find . -type f -print0|xargs -r0 -n1 -P20 bash -c 'f="{}";ls -l "$f"; gzip -9 "$f"; ls -l "$f.gz"'(C'est un peu plus facile lors de la conversion de boucles)
Gert van den Berg
1
Modification tardive pour mon commentaire précédent: (Si les noms de fichiers contiennent des guillemets, il y a un problème de sécurité ...) (Il semble que l'utilisation "$@"soit le seul moyen d'éviter cela ... (avec -n1si vous voulez limiter le nombre de paramètres))
Gert van den Berg
Il convient de noter que --le shell est utilisé pour dire qu'aucune autre option ne doit être acceptée. Cela permet qu'il y ait aussi un -après --. Vous pouvez obtenir une sortie très intéressante et déroutante si vous ne le faites pas, par exemple grep -rlorsque vous incluez dans le motif -! La façon dont vous le dites ne clarifie pas cela n'explique pas vraiment comment cela fonctionne. Iirc c'est une chose POSIX mais de toute façon cela vaut la peine de le souligner, je pense. Juste quelque chose à considérer. Et j'adore ce bonus btw!
Pryftan
10

Cela semble être la version la plus sûre.

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

( -0peut être supprimé et trremplacé par une redirection (ou le fichier peut être remplacé par un fichier séparé par null à la place). Il est principalement là car j'utilise principalement xargsavec findavec -print0sortie) (Cela peut également être pertinent sur les xargsversions sans l' -0extension)

C'est sûr, car args transmettra les paramètres au shell sous forme de tableau lors de son exécution. Le shell (au moins bash) les transmettrait ensuite sous la forme d'un tableau non modifié aux autres processus lorsque tous sont obtenus à l'aide de["$@"][1]

Si vous utilisez ...| xargs -r0 -I{} bash -c 'f="{}"; command "$f";' '', l'affectation échouera si la chaîne contient des guillemets doubles. Cela est vrai pour chaque variante utilisant -iou -I. (En raison de son remplacement dans une chaîne, vous pouvez toujours injecter des commandes en insérant des caractères inattendus (comme des guillemets, des guillemets ou des signes dollar) dans les données d'entrée)

Si les commandes ne peuvent prendre qu'un paramètre à la fois:

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

Ou avec un peu moins de processus:

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

Si vous avez GNU xargsou un autre avec l' -Pextension et que vous souhaitez exécuter 32 processus en parallèle, chacun avec pas plus de 10 paramètres pour chaque commande:

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

Cela devrait être robuste contre tous les caractères spéciaux dans l'entrée. (Si l'entrée est séparée par des valeurs nulles.) La trversion obtiendra une entrée non valide si certaines des lignes contiennent des retours à la ligne, mais cela est inévitable avec un fichier séparé par des retours à la ligne.

Le premier paramètre vide pour bash -cest dû à ceci: (À partir de la bashpage de manuel ) (Merci @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
la source
Cela devrait fonctionner même avec des guillemets doubles dans les noms de fichiers. Cela nécessite un shell qui supporte correctement"$@"
Gert van den Berg
Il vous manque l'argument argv [0] pour bash. bash -c 'command1 "$@"; command2 "$@";' arbitrarytextgoeshere
clacke
3
Il ne s'agit pas de ce que fait xargs. bashavec -cprend d'abord (après les commandes) un argument qui sera le nom du processus, puis il prend les arguments positionnels. Essayez de bash -c 'echo "$@" ' 1 2 3 4voir ce qui sort.
clacke
C'est agréable d'avoir une version sûre qui ne soit pas déposée sur Bobby.
Mateen Ulhaq
8

Une autre solution possible qui fonctionne pour moi est quelque chose comme -

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

Notez le 'bash' à la fin - je suppose qu'il est passé en argv [0] à bash. Sans lui dans cette syntaxe, le premier paramètre de chaque commande est perdu. Ce peut être n'importe quel mot.

Exemple:

cat a.txt | xargs -n 5 bash -c 'echo -n `date +%Y%m%d-%H%M%S:` ; echo " data: " $@; echo "data again: " $@' bash
tavvit
la source
5
Si vous ne citez pas "$@", vous divisez les chaînes et développez la liste d'arguments de manière globale.
Charles Duffy
2

Mon BKM actuel pour cela est

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

Il est regrettable que cela utilise perl, qui est moins susceptible d'être installé que bash; mais il gère plus d'entrée que la réponse acceptée. (Je salue une version omniprésente qui ne repose pas sur perl.)

@ Suggestion de KeithThompson

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

est génial - à moins que vous n'ayez le caractère de commentaire du shell # dans votre entrée, auquel cas une partie de la première commande et toute la deuxième commande seront tronquées.

Les hachages # peuvent être assez courants, si l'entrée est dérivée d'une liste de systèmes de fichiers, telle que ls ou find, et que votre éditeur crée des fichiers temporaires avec # dans leur nom.

Exemple du problème:

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

Oups, voici le problème:

$ 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, c'est mieux:

$ 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
la source
5
# problème peut être facilement résolu en utilisant des guillemets:ls | xargs -I % sh -c 'echo 1 "%"; echo 2 "%"'
gpl