Entendendo a opção -exec de `find`

53

Encontro-me constantemente procurando a sintaxe de

find . -name "FILENAME"  -exec rm {} \;

principalmente porque não vejo exatamente como a -execpeça funciona. Qual é o significado das chaves, da barra invertida e do ponto e vírgula? Existem outros casos de uso para essa sintaxe?

Zsolt Szilagy
fonte
11
@ Philippos: Entendo o seu ponto. Por favor, lembre-se de que as páginas de manual são uma referência, ou seja, útil para aqueles que entendem o assunto e procuram a sintaxe. Para alguém novo no tópico, eles geralmente são enigmáticos e formais para serem úteis. Você descobrirá que a resposta aceita é cerca de 10 vezes maior que a entrada da página de manual, e isso é por um motivo.
Zsolt Szilagy 01/09/19
6
Até a manpágina antiga do POSIX lê Um nome ou argumento de utilitário que contém apenas os dois caracteres "{}" deve ser substituído pelo nome do caminho atual , que me parece suficiente. Além disso, tem um exemplo com -exec rm {} \;, assim como na sua pergunta. Nos meus dias, quase não havia outros recursos além da "grande parede cinza", livros de manpáginas impressas (o papel era mais grosso que o armazenamento). Então eu sei que isso é suficiente para alguém novo no tópico. Sua última pergunta, porém, é justa para perguntar aqui. Infelizmente, nem o @Kusalananda nem eu temos uma resposta para isso.
Philippos
11
Comeon @Philippos. Você está realmente dizendo a Kusalananda que ele não melhorou a página de manual? :-)
Zsolt Szilagy
11
@allo Embora xargsàs vezes seja útil, findpode passar vários argumentos de caminho para comandar sem ele. -exec command... {} +(com em +vez de \;) passa tantos caminhos depois command...quanto o necessário (cada sistema operacional tem seu próprio limite de quanto tempo uma linha de comando pode ter). E como xargs, a +forma terminadas em de find's -execação também será executado command...várias vezes no caso raro que há muitos caminhos para caber dentro do limite.
Eliah Kagan 01/09/19
2
@ZsoltSzilagy Eu não disse isso nem quis dizer isso. Ele te alimentou muito bem, só acho que você tem idade suficiente para comer sozinho. (-;
Philippos

Respostas:

90

Esta resposta vem nas seguintes partes:

  • Uso básico de -exec
  • Usando -execem combinação comsh -c
  • Usando -exec ... {} +
  • Usando -execdir

Uso básico de -exec

A -execopção pega um utilitário externo com argumentos opcionais como argumento e o executa.

Se a string {}estiver presente em qualquer lugar do comando, cada instância será substituída pelo nome do caminho atualmente sendo processado (por exemplo ./some/path/FILENAME). Na maioria dos shells, os dois caracteres {}não precisam ser citados.

O comando precisa ser finalizado com um ;para findsaber onde termina (pois pode haver outras opções posteriormente). Para proteger o ;shell, ele precisa ser citado como \;ou ';', caso contrário, o shell o verá como o final do findcomando.

Exemplo ( \no final das duas primeiras linhas são apenas para continuações de linha):

find . -type f -name '*.txt'      \
   -exec grep -q 'hello' {} ';'   \
   -exec cat {} ';'

Ele encontrará todos os arquivos regulares ( -type f) cujos nomes correspondem ao padrão *.txtno diretório atual ou abaixo dele. Ele testará se a sequência helloocorre em qualquer um dos arquivos encontrados usando grep -q(o que não produz nenhuma saída, apenas um status de saída). Para os arquivos que contêm a string, catserá executado para enviar o conteúdo do arquivo para o terminal.

Cada um -exectambém age como um "teste" nos nomes de caminhos encontrados por find, exatamente como -typee -namefaz. Se o comando retornar um status de saída zero (significando "sucesso"), a próxima parte do findcomando será considerada, caso contrário, o findcomando continuará com o próximo nome do caminho. Isso é usado no exemplo acima para encontrar arquivos que contêm a sequência hello, mas para ignorar todos os outros arquivos.

O exemplo acima ilustra os dois casos de uso mais comuns de -exec:

  1. Como um teste para restringir ainda mais a pesquisa.
  2. Executar algum tipo de ação no nome do caminho encontrado (geralmente, mas não necessariamente, no final do findcomando).

Usando -execem combinação comsh -c

O comando que -execpode ser executado é limitado a um utilitário externo com argumentos opcionais. -execNão é possível usar embutidos, funções, condicionais, pipelines, redirecionamentos etc. diretamente do sh -cshell , a menos que seja envolto em algo como um shell filho.

Se bashforem necessários recursos, use bash -cno lugar de sh -c.

sh -cé executado /bin/shcom um script fornecido na linha de comando, seguido por argumentos opcionais da linha de comando para esse script.

Um exemplo simples de uso sh -cpor si só, sem find:

sh -c 'echo  "You gave me $1, thanks!"' sh "apples"

Isso passa dois argumentos para o script do shell filho:

  1. A cadeia sh. Isso estará disponível como $0dentro do script e, se o shell interno emitir uma mensagem de erro, ele será prefixado com essa sequência.

  2. O argumento applesestá disponível como $1no script, e se houvesse mais argumentos, eles estariam disponíveis como $2, $3etc. Eles também estariam disponíveis na lista "$@"(exceto pelos $0quais não faria parte "$@").

Isso é útil em combinação com -exec, pois permite criar scripts arbitrariamente complexos que atuam nos nomes de caminho encontrados por find.

Exemplo: encontre todos os arquivos regulares que possuem um determinado sufixo de nome de arquivo e altere esse sufixo para outro sufixo, onde os sufixos são mantidos nas variáveis:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c 'mv "$3" "${3%.$1}.$2"' sh "$from" "$to" {} ';'

Dentro do script interno, $1seria a string text, $2seria a string txte $3seria o nome do caminho findencontrado para nós. A expansão do parâmetro ${3%.$1}pegaria o nome do caminho e removeria o sufixo .textdele.

Ou, usando dirname/ basename:

find . -type f -name "*.$from" -exec sh -c '
    mv "$3" "$(dirname "$3")/$(basename "$3" ".$1").$2"' sh "$from" "$to" {} ';'

ou, com variáveis ​​adicionadas no script interno:

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2; pathname=$3
    mv "$pathname" "$(dirname "$pathname")/$(basename "$pathname" ".$from").$to"' sh "$from" "$to" {} ';'

Observe que nesta última variação, as variáveis frome tono shell filho são distintas das variáveis ​​com os mesmos nomes no script externo.

A descrição acima é a maneira correta de chamar um script complexo arbitrário de -execwith find. Usando findem um loop como

for pathname in $( find ... ); do

é propenso a erros e deselegante (opinião pessoal). Ele está dividindo nomes de arquivos em espaços em branco, invocando o globbing do nome de arquivo e também força o shell a expandir o resultado completo findantes mesmo de executar a primeira iteração do loop.

Veja também:


Usando -exec ... {} +

O ;no final pode ser substituído por +. Isso faz findcom que o comando fornecido seja executado com o maior número possível de argumentos (nomes de caminhos encontrados), em vez de uma vez para cada nome de caminho encontrado. A string {} deve ocorrer imediatamente antes do +para que isso funcione .

find . -type f -name '*.txt' \
   -exec grep -q 'hello' {} ';' \
   -exec cat {} +

Aqui, findvocê coletará os nomes de caminho resultantes e executará o catmaior número possível de uma vez.

find . -type f -name "*.txt" \
   -exec grep -q "hello" {} ';' \
   -exec mv -t /tmp/files_with_hello/ {} +

Da mesma forma aqui, mvserá executado o menor número de vezes possível. Este último exemplo requer o GNU mvdo coreutils (que suporta a -topção).

O uso -exec sh -c ... {} +também é uma maneira eficiente de fazer um loop sobre um conjunto de nomes de caminho com um script arbitrariamente complexo.

O básico é o mesmo que quando usado -exec sh -c ... {} ';', mas o script agora leva uma lista muito maior de argumentos. Eles podem ser repetidos repetidamente "$@"dentro do script.

Nosso exemplo da última seção que altera os sufixos do nome do arquivo:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2
    shift 2  # remove the first two arguments from the list
             # because in this case these are *not* pathnames
             # given to us by find
    for pathname do  # or:  for pathname in "$@"; do
        mv "$pathname" "${pathname%.$from}.$to"
    done' sh "$from" "$to" {} +

Usando -execdir

Também existe -execdir(implementado pela maioria das findvariantes, mas não uma opção padrão).

Isso funciona -execcom a diferença de que o comando shell fornecido é executado com o diretório do nome do caminho encontrado como seu diretório de trabalho atual e que {}conterá o nome de base do nome de caminho encontrado sem o caminho (mas o GNU findainda prefixará o nome de base com ./BSD findnão fará isso).

Exemplo:

find . -type f -name '*.txt' \
    -execdir mv {} done-texts/{}.done \;

Isso moverá cada arquivo encontrado*.txt para um done-textssubdiretório pré-existente no mesmo diretório em que o arquivo foi encontrado . O arquivo também será renomeado adicionando o sufixo .donea ele.

Isso seria um pouco mais complicado -exec, já que precisaríamos obter o nome de base do arquivo encontrado {}para formar o novo nome do arquivo. Também precisamos do nome do diretório de {}para localizar o done-textsdiretório corretamente.

Com -execdir, algumas coisas como essas se tornam mais fáceis.

A operação correspondente usando em -execvez de -execdirteria que empregar um shell filho:

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "$( dirname "$name" )/done-texts/$( basename "$name" ).done"
    done' sh {} +

ou,

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +
Kusalananda
fonte
7
-execpega um programa e argumentos e o executa; alguns comandos do shell consistem apenas em um programa e argumentos, mas muitos não. Um comando shell pode incluir redirecionamento e tubulação; -execnão pode (embora o todo findpossa ser redirecionado). Um comando shell pode usar ; && ifetc; -execnão pode, embora -a -opossa fazer algumas. Um comando shell pode ser um alias ou função shell, ou embutido; -execnão podes. Um comando shell pode expandir vars; -execnão pode (embora o shell externo que executa a findlata). Um comando shell pode substituir de maneira $(command)diferente a cada vez; -execnão podes. ...
dave_thompson_085
... Um comando de shell pode glob, -execnão pode - embora findpossa iterar sobre arquivos da mesma forma que a maioria dos globs, então isso raramente é desejado.
Dave_thompson_085 01/09/19
@ dave_thompson_085 Claro, o comando shell pode ser shem si, que é totalmente competentemente de fazer todas essas coisas
Tavian Barnes
2
Dizer que é um comando do shell está errado aqui, find -exec cmd arg \;não invoca um shell para interpretar uma linha de comando do shell, ele é executado execlp("cmd", "arg")diretamente, não execlp("sh", "-c", "cmd arg")(para o qual o shell acabaria fazendo o equivalente a execlp("cmd", "arg")se cmdnão estivesse embutido).
Stéphane Chazelas 26/09
2
Você pode esclarecer que todos os findargumentos após -exece até ;ou +compõem o comando para executar junto com seus argumentos, com cada instância de um {}argumento substituída pelo arquivo atual (com ;) e {}como o último argumento antes +substituído por uma lista de arquivos como argumentos separados (no {} +caso). O IOW -execrecebe vários argumentos, finalizados por um ;ou {} +.
Stéphane Chazelas 26/09