Executando vários comandos com xargs

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

No exemplo acima, xargs assume echo %como argumento de comando. Mas, em alguns casos, preciso de vários comandos para processar o argumento em vez de um. Por exemplo:

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

Mas xargs não aceita este formulário. Uma solução que eu sei é que posso definir uma função para agrupar os comandos, mas não é um pipeline, não a prefiro. Existe outra solução?

Dagang
fonte
1
A maioria dessas respostas são vulnerabilidades de segurança . Veja aqui uma resposta potencialmente boa.
Mateen Ulhaq
2
Uso xargs para quase tudo, mas odeio colocar comandos dentro de strings e criar explicitamente subshells. Estou prestes a aprender a canalizar em um whileloop que pode conter vários comandos.
Sridhar Sarnobat 23/07/19

Respostas:

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

... ou, sem um uso inútil de gato :

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

Para explicar alguns dos pontos mais sutis:

  • O uso de em "$arg"vez de %(e a ausência de -Ina xargslinha de comando) é por motivos de segurança: A transmissão de dados na shlista de argumentos da linha de comando em vez de substituí-la no código impede o conteúdo que os dados possam conter (como $(rm -rf ~), por exemplo , exemplo malicioso) de ser executado como código.

  • Da mesma forma, o uso de -d $'\n'é uma extensão GNU que faz xargscom que cada linha do arquivo de entrada trate como um item de dados separado. Isso ou -0(que espera NULs em vez de novas linhas) é necessário para impedir que os xargs tentem aplicar uma análise semelhante ao shell (mas não totalmente compatível com o shell) ao fluxo que lê. (Se você não possui o GNU xargs, pode tr '\n' '\0' <a.txt | xargs -0 ...obter a leitura orientada a linhas sem -d).

  • O _é um espaço reservado para $0, de modo que outros valores de dados adicionados por xargsse tornem $1e avancem, o que passa a ser o conjunto de valores padrão que um forloop percorre.

Keith Thompson
fonte
58
Para aqueles que não estão familiarizados com sh -c- observe que o ponto-e-vírgula após cada comando não é opcional, mesmo que seja o último comando na lista.
Noah Sussman
6
Pelo menos na minha configuração, deve haver um espaço imediatamente após o inicial "{". Não é necessário espaço antes do fim da chave, mas, como observou o Sr. Sussman, você precisa de um ponto e vírgula final.
willdye
4
Esta resposta anteriormente tinha chaves em volta command1e command2; Mais tarde eu percebi que eles não são necessários.
Keith Thompson
24
Para esclarecer os comentários acima sobre ponto e vírgula, é necessário um ponto e vírgula antes do fechamento }: 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
Se você estiver incluindo o %caractere em algum lugar na sua cadeia de caracteres transmitida sh -c, isso é propenso a vulnerabilidades de segurança: um nome de arquivo contendo $(rm -rf ~)'$(rm -rf ~)'(e essa é uma substring perfeitamente legal para ter dentro de um nome de arquivo em sistemas de arquivos UNIX comuns!) Causará a alguém um dia muito ruim .
Charles Duffy
35

Com o GNU Parallel, você pode:

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

Assista aos vídeos de introdução para saber mais: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Por motivos de segurança, é recomendável usar o gerenciador de pacotes para instalar. Mas se você não puder fazer isso, poderá usar esta instalação de 10 segundos.

A instalação de 10 segundos tentará fazer uma instalação completa; se isso falhar, uma instalação pessoal; se isso falhar, uma instalação 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
fonte
56
Instalar ferramentas através da execução de scripts aleatórios de sites desconhecidos é uma prática horrível. Paralelo tem oficiall pacotes para distribuições populares, que podem ser confiáveis (em certa medida) muito mais do que aleatório wget | sh ...
mdrozdziel
4
Vamos ver qual é o vetor de ataque mais fácil: o Pi.dk é controlado pelo autor do GNU Parallel, portanto, para atacar, você precisará invadir o servidor ou assumir o DNS. Para assumir o pacote oficial de uma distribuição, muitas vezes você pode apenas se voluntariar para manter o pacote. Portanto, embora você possa estar certo em geral, parece que nesse caso em particular seu comentário não se justifica.
precisa
10
Na prática, não sei se o pi.dk pertence ao autor. Na verdade, verificar se esse é o caso, pensar em como usar o ssl no wget e verificar se esse comando faz o que é suposto fazer é um pouco de trabalho. Seu ponto de vista de que o pacote oficial pode conter código malicioso é verdadeiro, mas isso também vale para o pacote wget.
Fabian
3
Essa pode não ser a melhor solução se cada um dos comandos que o OP deseja executar precisar ser seqüencial, correto?
IcarianComplex
2
@IcarianComplex A adição de -j1 corrigirá isso.
precisa
26

Esta é apenas outra abordagem sem xargs nem cat:

while read stuff; do
  command1 "$stuff"
  command2 "$stuff"
  ...
done < a.txt
hmontoliu
fonte
2
Buggy, como dado. A menos que você decida IFS, ele ignorará os espaços em branco à esquerda e à esquerda nos nomes dos arquivos; a menos que você adicione -r, os nomes de arquivos com barras invertidas literais terão esses caracteres ignorados.
Charles Duffy
Não responde a pergunta. Perguntou especificamente sobre xargs. (Isso é difícil de expandir para fazer algo semelhante à opção GNU xargs' -P<n>)
Gert van den Berg
1
Isso funciona perfeitamente bem. Você também pode usá-lo como um comando canalizado como$ command | while read line; do c1 $line; c2 $line; done
Alexar
25

Você pode usar

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

{} = variável para cada linha no arquivo de texto

Ossama
fonte
8
Isso é inseguro. E se o seu file.txtcontiver um dado com $(rm -rf ~)como substring?
Charles Duffy
19

Uma coisa que faço é adicionar ao .bashrc / .profile esta função:

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

então você pode fazer coisas como

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

que é menos detalhado que xargs ou -exec. Você também pode modificar a função para inserir o valor da leitura em um local arbitrário nos comandos de cada um, se também precisar desse comportamento.

mwm
fonte
1
Resposta subestimada, isso é extremamente útil
charlesreid1
15

Eu prefiro o estilo que permite o modo de execução a seco (sem | sh):

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

Também funciona com tubos:

cat a.txt | xargs -I % echo "echo % | cat " | sh
brablc
fonte
2
Isso funciona, até que você quiser GNU xargs usar -Popção ... (Se não, eu uso principalmente -execem find, desde os meus entradas são principalmente os nomes de arquivo)
Gert van den Berg
13

Um pouco atrasado para a festa.

Eu uso o formato abaixo para compactar meus diretórios com milhares de pequenos arquivos antes de migrar. Se você não precisar de aspas simples dentro de comandos, deve funcionar.

Com algumas modificações, tenho certeza de que será útil para alguém. Testado em Cygwin(babun)

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

find .Encontre aqui
-maxdepth 1Não entre em diretórios filhos
! -path .Excluir. / O caminho do diretório atual
-type dcorresponde apenas aos diretórios
-print0Separe a saída por bytes nulos \ 0
| xargsCanalizar para xargs A
-0entrada é bytes separados por nulo O espaço
-I @@reservado é @@. Substitua @@ pela entrada. Comando
bash -c '...'Run Bash
{...}Agrupamento de comandos
&&Executar o próximo comando apenas se o comando anterior tiver sido encerrado com êxito (saída 0)

Final ;é importante, caso contrário, falhará.

Resultado:

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

Atualização de julho de 2018:

Se você gosta de hacks e brincadeiras, aqui está algo interessante:

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

Resultado:

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

Explicação:
- Crie um script de liner único e armazene-o em uma variável
- xargs leia a.txte execute-o como bashscript
- @@ garanta que toda vez que uma linha inteira seja passada
- Colocar @@após --garanta que @@seja tomado como entrada de parâmetro posicional para o bashcomando, não um bashinício OPTION, ou seja, como -cele, o que significarun command

--é mágico, funciona com muitas outras coisas, ou seja ssh, atékubectl

sdkks
fonte
1
Eu tenho usado uma configuração com este tipo de coisa: find . -type f -print0|xargs -r0 -n1 -P20 bash -c 'f="{}";ls -l "$f"; gzip -9 "$f"; ls -l "$f.gz"'(É um pouco mais fácil ao converter loops)
Gert van den Berg
1
Edição tardia para o meu comentário anterior: (Se os nomes de arquivos contiverem aspas duplas, há um problema de segurança ...) (Parece que usar "$@"é a única maneira de evitar isso ... (com -n1se você quiser limitar o número de parâmetros))
Gert van den Berg
Note-se que --é usado pelo shell para dizer que não há mais opções a serem aceitas. Isso permite que haja um -após o --também. Você pode obter uma saída muito interessante e confusa se não o fizer, por exemplo, grep -rquando incluir no padrão -! O modo como você o pronuncia não esclarece isso, na verdade não explica como isso funciona. Iirc é uma coisa POSIX, mas de qualquer maneira vale a pena apontar isso, eu acho. Apenas algo a considerar. E eu amo esse bônus btw!
Pryftan
10

Esta parece ser a versão mais segura.

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

( -0Pode ser removido e o trsubstituído por um redirecionamento (ou o arquivo pode ser substituído com um nulo separados arquivo em vez). É principalmente lá desde que eu uso principalmente xargscom finda -print0saída) (Isso também pode ser relevante em xargsversões sem a -0extensão)

É seguro, já que args passará os parâmetros para o shell como uma matriz ao executá-lo. O shell (pelo menos bash) os passaria como uma matriz inalterada para os outros processos quando todos forem obtidos usando["$@"][1]

Se você usar ...| xargs -r0 -I{} bash -c 'f="{}"; command "$f";' '', a atribuição falhará se a sequência contiver aspas duplas. Isso é verdade para todas as variantes que usam -ior -I. (Devido à sua substituição em uma sequência, você sempre pode injetar comandos inserindo caracteres inesperados (como aspas, reticulares ou cifrões) nos dados de entrada)

Se os comandos puderem usar apenas um parâmetro por vez:

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

Ou com processos um pouco menos:

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

Se você possui o GNU xargsou outro com a -Pextensão e deseja executar 32 processos em paralelo, cada um com no máximo 10 parâmetros para cada comando:

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

Isso deve ser robusto contra quaisquer caracteres especiais na entrada. (Se a entrada for nula separada.) A trversão obterá alguma entrada inválida se algumas das linhas contiverem novas linhas, mas isso é inevitável com um arquivo separado por nova linha.

O primeiro parâmetro em branco para bash -cé devido a isso: (Na bashpágina de manual ) (Obrigado @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
fonte
Isso deve funcionar mesmo com aspas duplas nos nomes de arquivos. Que requer um shell que adequadamente apoiar"$@"
Gert van den Berg
Está faltando o argumento argv [0] para bash. bash -c 'command1 "$@"; command2 "$@";' arbitrarytextgoeshere
Clacke
3
Não é sobre o que o xargs faz. bashcom -cobtém primeiro (após os comandos) um argumento que será o nome do processo, depois recebe os argumentos posicionais. Experimente bash -c 'echo "$@" ' 1 2 3 4e veja o que sai.
clacke
É bom ter uma versão segura que não seja ativada pelo Bobby.
Mateen Ulhaq
8

Outra solução possível que funciona para mim é algo como -

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

Observe o 'bash' no final - presumo que seja passado como argv [0] para o bash. Sem essa sintaxe, o primeiro parâmetro para cada comando é perdido. Pode ser qualquer palavra.

Exemplo:

cat a.txt | xargs -n 5 bash -c 'echo -n `date +%Y%m%d-%H%M%S:` ; echo " data: " $@; echo "data again: " $@' bash
tavvit
fonte
5
Se você não citar "$@", estará dividindo e expandindo a lista de argumentos.
Charles Duffy
2

Meu BKM atual para isso é

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

É lamentável que isso use perl, que é menos provável de ser instalado que o bash; mas lida com mais entradas do que a resposta aceita. (Congratulo-me com uma versão onipresente que não depende de perl.)

@ Sugestão de KeithThompson de

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

é ótimo - a menos que você tenha o caractere de comentário do shell # em sua entrada, nesse caso parte do primeiro comando e todo o segundo comando serão truncados.

Hashes # podem ser bastante comuns, se a entrada for derivada de uma lista de sistemas de arquivos, como ls ou find, e seu editor criar arquivos temporários com # em seu nome.

Exemplo do problema:

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

Opa, aqui está o 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, isso é melhor:

$ 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
fonte
5
# problema pode ser facilmente resolvido usando aspas:ls | xargs -I % sh -c 'echo 1 "%"; echo 2 "%"'
gpl