encontrar -exec uma função shell no Linux?


185

Existe uma maneira de conseguir findexecutar uma função que eu defino no shell? Por exemplo:

dosomething () {
  echo "doing something with $1"
}
find . -exec dosomething {} \;

O resultado disso é:

find: dosomething: No such file or directory

Existe uma maneira de obter find's -execpara ver dosomething?

Respostas:


262

Como apenas o shell sabe como executar funções do shell, você deve executar um shell para executar uma função. Você também precisa marcar sua função para exportação export -f, caso contrário, o subshell não as herdará:

export -f dosomething
find . -exec bash -c 'dosomething "$0"' {} \;

7
Você me venceu. A propósito, você pode colocar o aparelho dentro das aspas em vez de usá-lo $0.
Pausado até novo aviso.

20
Se você usar find . -exec bash -c 'dosomething "$0"' {} \;ele vai lidar com espaços (e outros caracteres estranhos) em nomes de arquivos ...
Gordon Davisson

3
@alxndr: isso falhará nos nomes de arquivos com aspas duplas, aspas, cifrões, alguns combos de escape, etc. ...
Gordon Davisson

4
Observe também que quaisquer funções que sua função possa estar chamando não estarão disponíveis, a menos que você exporte -f também.
Hraban

3
O export -ffuncionará apenas em algumas versões do bash. Não é posix, nem crossplatforn, /bin/shterá um erro com ele
#

120
find . | while read file; do dosomething "$file"; done

10
Ótima solução. Não requer exportar a função ou mexer nos argumentos de escape e é presumivelmente mais eficiente, pois não é necessário gerar subshells para executar cada função.
Tom

19
Lembre-se, no entanto, de que os nomes de arquivos contêm novas linhas.
chepner

3
Isso é mais "shell'ish", pois suas variáveis ​​e funções globais estarão disponíveis, sem a criação de um shell / ambiente totalmente novo a cada vez. Aprendi isso da maneira mais difícil, depois de tentar o método de Adam e encontrar todos os tipos de problemas ambientais. Esse método também não corrompe o shell do usuário atual com todas as exportações e requer menos diciplina.
Raio Foss

MUITO MAIS RÁPIDO do que a resposta aceita sem sub shell! LEGAIS! Obrigado!
Bill Heller

2
também corrigi meu problema alterando while readpara um loop for; for item in $(find . ); do some_function "${item}"; done
user5359531

22

A resposta de Jak acima é ótima, mas tem algumas armadilhas que são facilmente superadas:

find . -print0 | while IFS= read -r -d '' file; do dosomething "$file"; done

Isso usa null como um delimitador em vez de um avanço de linha; portanto, os nomes de arquivos com avanço de linha funcionarão. Ele também usa o -rsinalizador que desativa a barra invertida, sem que as barras invertidas nos nomes de arquivos não funcionem. Ele também é limpo IFSpara que os espaços em branco à direita nos nomes não sejam descartados.


1
É bom, /bin/bashmas não funciona /bin/sh. Que pena.
Роман Коптев

@ РоманКоптев Que sorte que pelo menos ele funciona em / bin / bash.
sdenham

21

Adicione aspas {}como mostrado abaixo:

export -f dosomething
find . -exec bash -c 'dosomething "{}"' \;

Isso corrige qualquer erro devido a caracteres especiais retornados por find, por exemplo, arquivos com parênteses no nome.


1
Esta não é a maneira correta de usar {}. Isso quebrará para um nome de arquivo contendo aspas duplas. touch '"; rm -rf .; echo "I deleted all you files, haha. Opa
gniourf_gniourf

Sim, isso é muito ruim. Pode ser explorado por injeções. Muito inseguro. Não use isso!
Dominik

1
@kdubs: use $ 0 (sem aspas) na cadeia de comandos e passe o nome do arquivo como o primeiro argumento: -exec bash -c 'echo $0' '{}' \;Observe que, ao usar bash -c, $ 0 é o primeiro argumento, não o nome do script.
sdenham

10

Processando resultados em massa

Para aumentar a eficiência, muitas pessoas usam xargspara processar resultados em massa, mas é muito perigoso. Por isso, foi introduzido um método alternativo findque executa os resultados em massa.

Observe, porém, que esse método pode vir com algumas advertências, como por exemplo, um requisito no POSIX findpara ter {}no final do comando.

export -f dosomething
find . -exec bash -c 'for f; do dosomething "$f"; done' _ {} +

findpassará muitos resultados como argumentos para uma única chamada de bashe o forloop-itera esses argumentos, executando a função dosomethingem cada um deles.

A solução acima inicia argumentos em $1, e é por isso que existe um _(que representa $0).

Processando resultados um por um

Da mesma forma, acho que a resposta principal aceita deve ser corrigida para ser

export -f dosomething
find . -exec bash -c 'dosomething "$1"' _ {} \;

Isso não é apenas mais sensato, porque os argumentos devem sempre começar em $1, mas também o uso $0pode levar a um comportamento inesperado se o nome do arquivo retornado por findtiver um significado especial para o shell.


9

Faça com que o script se chame, passando cada item encontrado como argumento:

#!/bin/bash

if [ ! $1 == "" ] ; then
   echo "doing something with $1"
   exit 0
fi

find . -exec $0 {} \;

exit 0

Quando você executa o script por si só, ele encontra o que está procurando e chama a si próprio passando cada resultado da busca como argumento. Quando o script é executado com um argumento, ele executa os comandos no argumento e sai.


idéia legal, mas estilo ruim: usa o mesmo script para dois propósitos. se você quiser reduzir o número de arquivos em sua lixeira /, poderá mesclar todos os seus scripts em um único que possua uma grande cláusula case no início. solução muito limpa, não é?
user829755

para não mencionar que isso falhará find: ‘myscript.sh’: No such file or directoryse iniciado como bash myscript.sh...
Camusensei

5

Para aqueles que procuram uma função bash que execute um determinado comando em todos os arquivos no diretório atual, compilei um das respostas acima:

toall(){
    find . -type f | while read file; do "$1" "$file"; done
}

Observe que ele quebra com nomes de arquivos contendo espaços (veja abaixo).

Como exemplo, considere esta função:

world(){
    sed -i 's_hello_world_g' "$1"
}

Digamos que eu queira alterar todas as instâncias do hello para world em todos os arquivos no diretório atual. Eu faria:

toall world

Para estar seguro com qualquer símbolo nos nomes de arquivos, use:

toall(){
    find . -type f -print0 | while IFS= read -r -d '' file; do "$1" "$file"; done
}

(mas você precisa de um findque manipule -print0, por exemplo, GNUfind ).


3

Acho a maneira mais fácil a seguir, repetindo dois comandos em um único

func_one () {
  echo "first thing with $1"
}

func_two () {
  echo "second thing with $1"
}

find . -type f | while read file; do func_one $file; func_two $file; done

3

Para referência, evito esse cenário usando:

for i in $(find $dir -type f -name "$name" -exec ls {} \;); do
  _script_function_call $i;
done;

Obtenha Saída de localização no arquivo de script atual e itere sobre a saída conforme desejar. Concordo com a resposta aceita, mas não quero expor a função fora do meu arquivo de script.


isso tem limites de tamanho
Richard

2

Não é possível executar uma função dessa maneira.

Para superar isso, você pode colocar sua função em um shell script e chamar isso de find

# dosomething.sh
dosomething () {
  echo "doing something with $1"
}
dosomething $1

Agora use-o em find como:

find . -exec dosomething.sh {} \;

Estava tentando evitar mais arquivos no meu ~ / bin. Obrigado embora!
Alxndr

Eu considerei voto negativo, mas a solução em si não é ruim. Por favor, é só usar correta citando: dosomething $1=> dosomething "$1"e iniciar o seu arquivo corretamente comfind . -exec bash dosomething.sh {} \;
Camusensei

2

Coloque a função em um arquivo separado e findexecute-o.

As funções do shell são internas ao shell em que estão definidas; findnunca será capaz de vê-los.


Peguei vocês; faz sentido. Estava tentando evitar mais arquivos no meu ~ / bin.
Alxndr

2

Para fornecer adições e esclarecimentos a algumas das outras respostas, se você estiver usando a opção em massa para execou execdir( -exec command {} +) e quiser recuperar todos os argumentos posicionais, é necessário considerar o tratamento $0com bash -c. Mais concretamente, considere o comando abaixo, que é usado bash -ccomo sugerido acima, e simplesmente reproduz os caminhos de arquivo que terminam com '.wav' em cada diretório encontrado:

find "$1" -name '*.wav' -execdir bash -c 'echo $@' _ {} +

O manual do bash diz:

If the -c option is present, then commands are read from the first non-option argument command_string.  If there are arguments after the command_string, they  are  assigned  to  the
                 positional parameters, starting with $0.

Aqui 'check $@'está a sequência de comandos e _ {}os argumentos após a sequência de comandos. Observe que $@é um parâmetro posicional especial no bash que se expande para todos os parâmetros posicionais a partir de 1 . Observe também que, com a -copção, o primeiro argumento é atribuído ao parâmetro posicional $0. Isso significa que, se você tentar acessar todos os parâmetros posicionais $@, obterá apenas parâmetros iniciando $1e subindo. Essa é a razão pela qual a resposta de Dominik possui o _parâmetro $0, que é um argumento fictício para preencher o parâmetro , para que todos os argumentos que desejamos estejam disponíveis mais tarde, se usarmos$@ expansão de parâmetros, por exemplo, ou o loop for, como nessa resposta.

Obviamente, semelhante à resposta aceita, bash -c 'shell_function $0 $@'também funcionaria passando explicitamente $0, mas novamente, você deve ter em mente que $@não funcionará conforme o esperado.


0

Não diretamente, não. O Find está sendo executado em um processo separado, não no seu shell.

Crie um shell script que faça o mesmo trabalho que sua função e encontre -execisso.


Estava tentando evitar mais arquivos no meu ~ / bin. Obrigado embora!
Alxndr

-2

Eu evitaria usar -execcompletamente. Por que não usar xargs?

find . -name <script/command you're searching for> | xargs bash -c

Na época, o IIRC estava tentando reduzir a quantidade de recursos utilizados. Pense em encontrar milhões de arquivos vazios e excluí-los.
alxndr
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.