Qual é a diferença entre encontrar com -exec e xargs?

5

tentando aprender script Bash Eu quero executar algum comando em todos os arquivos abaixo do meu diretório atual que satisfaçam uma determinada condição. Usando

find -name *.flac

Especificamente, quero converter .flac em .mp3 . Eu posso encontrar todos os arquivos. No entanto, não vejo a diferença na execução de um comando usando a opção -exec para find e usando xargs . Por exemplo,

find -name *.flac | xargs -i ffmpeg -i {} {}.mp3

comparado a

find -name *.flac -exec ffmpeg -i {} {}.mp3 \;

Alguém pode apontar a diferença? O que é melhor praticar? Quais são as vantagens / desvantagens?

Além disso: se eu quisesse excluir simultaneamente o arquivo original, como adicionaria um segundo comando no código acima?

    
por Suppenkasper 24.09.2017 / 21:20

3 respostas

7

Resumo:

A menos que você esteja muito mais familiarizado com xargs do que -exec , provavelmente desejará usar -exec quando usar find .

Como xargs é um programa separado, chamá-lo provavelmente será um pouco menos eficiente do que usar -exec , que é um recurso do programa find . Geralmente, não queremos chamar um programa extra se ele não fornecer nenhum benefício adicional em termos de confiabilidade, desempenho ou legibilidade. Como find ... -exec ... fornece a capacidade de executar comandos com uma lista de argumentos (como xargs faz), se possível, não há realmente nenhuma vantagem de usar xargs com find over -exec . No caso de ffmpeg , temos que especificar arquivos de entrada e saída, portanto não podemos obter ganhos de desempenho usando qualquer método para construir uma lista de argumentos, e com xargs remover a extensão de nome de arquivo original ilógica é mais difícil. / p>

O que xargs faz

Observação: O sinalizador detalhado (que imprime o comando construído com seus argumentos) em xargs é -t e o sinalizador interativo (que faz com que o usuário seja solicitado confirmação para operar em cada argumento) é -p . Você pode achar ambos úteis para entender e testar seu comportamento.

xargs tenta transformar seu STDIN (normalmente o STDOUT do comando anterior que foi enviado para ele) em uma lista de argumentos para algum comando.

command1 | xargs command2 [output of command1 will be appended here]

Como STDOUT ou STDIN é apenas um fluxo de texto (também é por isso que você não deve analisar a saída de ls ), xargs é facilmente desativado. Ele lê argumentos como sendo delimitados por espaços ou novas linhas. Os nomes de arquivos podem conter espaços e podem até conter novas linhas, e esses nomes de arquivos causarão um comportamento inesperado. Digamos que você tenha um arquivo chamado foo bar . Quando uma lista contendo esse nome de arquivo é canalizada para xargs , ele tenta executar o comando especificado em foo e em bar .

O mesmo problema ocorre quando você digita command foo bar , e você sabe que pode evitá-lo citando o espaço ou o nome inteiro, por exemplo, command foo\ bar ou command "foo bar" , mas mesmo se pudermos citar a lista passada para xargs geralmente não queremos, porque não queremos que toda a lista seja tratada como um único argumento. A solução padrão para isso é usar o caractere nulo como delimitador, já que os nomes de arquivos não podem conter:

find path test(s) -print0 | xargs -0 command

Isso faz com que find anexe o caractere nulo a cada nome de arquivo em vez de um espaço e xargs para tratar apenas o caractere nulo como delimitador.

Ainda podem ocorrer problemas se o comando não aceitar vários argumentos ou se a lista de argumentos for extremamente longa.

Nesse caso, você está usando ffmpeg , que espera que os arquivos de entrada sejam especificados primeiro e que os arquivos de saída sejam especificados por último. Podemos dizer a ffmpeg quais arquivos (s) usar como entrada explicitamente com o -i flag, mas precisamos dar o nome do arquivo de saída (do qual o formato normalmente é adivinhado, embora também possamos especificá-lo). Portanto, para construir comandos adequados, você precisa usar a opção replace string ( -I ou -i ) de xargs para especificar os arquivos de entrada e saída:

... | xargs -I{} command {} {}.out

(a documentação diz que -i está obsoleto para essa finalidade e devemos usar -I , mas não tenho certeza do porquê. Ao usar -I , você deve especificar a substituição ( {} é normalmente usado ) imediatamente após a opção. Com -i você pode omitir para especificar a substituição, mas {} é entendido por padrão.)

A opção -I faz com que a lista de comandos seja dividida apenas em novas linhas, não em espaços, portanto, se você tiver certeza de que seus nomes não conterão novas linhas, não será necessário usar -print0 | xargs -0 quando usar -I . Se você não tiver certeza, ainda poderá usar a sintaxe mais segura:

find -name "*.flac" -print0 | xargs -0I{} ffmpeg -i {} {}.mp3

No entanto, o benefício de desempenho de xargs (que permite executar um comando uma vez com uma lista de argumentos) é perdido aqui, pois ffmpeg deve ser executado uma vez para cada par de arquivos de entrada e saída veja isso facilmente prefixando echo to ffmpeg para testar o comando acima). Isso também produz um nome de arquivo ilógico e não permite que você execute vários comandos. Para fazer o último, você pode chamar bash , como na resposta da sobremesa :

... | xargs -I{} bash -c 'ffmpeg -i {} {}.mp3 && rm {}'

mas renomear é complicado .

Como -exec é diferente

Quando você usa a opção -exec para find , os arquivos encontrados são transmitidos como argumentos para o comando após -exec . Eles não são transformados em texto. Com a sintaxe:

find ... -exec command {} \;

command é executado uma vez para cada arquivo encontrado. Com a sintaxe

find ... -exec command {} +

uma lista de argumentos é construída a partir dos arquivos encontrados para que possamos executar o comando apenas uma vez (ou somente quantas vezes forem necessárias) em vários arquivos, fornecendo o benefício de desempenho fornecido por xargs .No entanto, como os argumentos de nome de arquivo não são construídos a partir de um fluxo de texto, o uso de -exec não tem o problema xargs tem de quebrar espaços e outros caracteres especiais.

Com ffmpeg , não podemos usar + pela mesma razão que xargs não ofereceu nenhum benefício de desempenho; já que precisamos especificar entrada e saída, o comando deve ser executado em cada arquivo individualmente. Nós temos que usar alguma forma de

find -name "*.flac" -exec ffmpeg -i {} {}.out \;

Isso, mais uma vez, fornecerá a você um arquivo com nomes pouco ilógicos, como explica a resposta da sobremesa , então talvez você queira tira-lo, como resposta da sobremesa explica como fazer com manipulação de string (não é fácil feito em xargs ; outra razão para usar -exec ). Ele também explica como executar vários comandos no arquivo para que você possa remover com segurança o arquivo original após uma conversão bem-sucedida.

Em vez de repetir a recomendação de sobremesa, com a qual concordo, sugiro uma alternativa a find , que permite flexibilidade semelhante à execução de bash -c após -exec ; um loop bash for :

shopt -s globstar           # allow recursive globbing with **
for f in ./**/*.flac; do    # for all files ending with .flac
   # convert them, stripping the original extension from the new filename
   echo ffmpeg -i "$f" "${f%.flac}.mp3" &&
   echo rm -v "$f"          # if that succeeded, delete the original
done
shopt -u globstar           # turn recursive globbing off

Remova os echo es após o teste para operar nos arquivos.

ffmpeg não reconhece -- para marcar o fim das opções, portanto, para evitar nomes de arquivos que começam com - sendo interpretados como opções, usamos ./ para indicar o diretório atual em vez de começar com ** , para que todos os caminhos comecem com ./ em vez de nomes de arquivos arbitrários. Isso significa que não precisamos usar -- com rm (que também o reconhece).

Observação : você deve citar sua expressão -name test se ela contiver caracteres curinga, caso contrário, o shell os expandirá, se possível (ou seja, se eles corresponderem a qualquer arquivo no diretório atual) antes deles são passados para find , então, em primeiro lugar, use

find -name "*.flac"

para evitar comportamentos inesperados.

    
por Zanna 25.09.2017 / 12:45
4

Geralmente alguém tenta chamar o mínimo possível de comandos, mas no seu caso eu acho que é uma questão de gosto - eu usaria -exec , usando assim:

find . -name '*.flac' -exec bash -c 'ffmpeg -i "%pre%" "${0%flac}mp3" && rm "%pre%"' {} \;

O truque é chamar bash com a opção -c , assim você pode não apenas executar vários comandos, mas também usar Bash Parameter Substitution para remover o flac final dos seus nomes de arquivos - Suponho que você não queira realmente terminar com os arquivos nomeados filename.flac .mp3 , você?

Explicações

  • bash -c '…' {} - execute o (s) comando (s) in bash com o nome do arquivo como o primeiro argumento (acessível com ${0%flac} )
  • flac - tira && rm "%code%" do final do nome do arquivo
  • %code% - somente se o comando anterior foi bem-sucedido, remova o arquivo original
por dessert 24.09.2017 / 23:28
2

Como Zanna e sobremesa já responderam -exec deve ser preferido quando xargs não é necessário ( "Geralmente não queremos chamar um programa extra se ele não fornecer nenhum benefício adicional em termos de confiabilidade, desempenho ou legibilidade ". )

Embora isso esteja totalmente correto, quero acrescentar que xargs em combinação com o -P flag pode fornecer um benefício substancial em termos de desempenho .

xargs irá gerar os processos em paralelo, habilitando o multi-threading, similar ao mais flexível que o comando parallel .

-P max-procs, --max-procs=max-procs
              Run up to max-procs processes at a time; the default is 1.  If max-procs is 0, xargs will run as many processes as possible at a time.  Use the -n option or the -L option with -P; other‐
              wise chances are that only one exec will be done. 
              [...]

Isso ajuda especialmente com processos que não executam multi-thread por si mesmos. No seu caso, ffmpeg vai se preocupar com o multithreading, por isso não ajudará nem terá um efeito negativo no desempenho.

find . -name "*.ext" -print0 | xargs -0 -i -P 20 command -in {} -out {}.out
    
por RoVo 25.09.2017 / 14:26