Executando função definida pelo usuário em uma chamada find -exec

25

Estou no Solaris 10 e testei o seguinte com ksh (88), bash (3.00) e zsh (4.2.1).

O código a seguir não produz nenhum resultado:

function foo {
    echo "Hello World"
}

find somedir -exec foo \;

A descoberta corresponde a vários arquivos (como mostrado substituindo -exec ...por-print ), e a função funciona perfeitamente quando chamada fora da findchamada.

Aqui está o que a man findpágina diz sobre-exec :

 comando -exec True se o comando executado retornar um
                     valor zero como status de saída. O fim de
                     comando deve ser pontuado por um escape
                     ponto e vírgula (;). Um argumento de comando {} é
                     substituído pelo nome do caminho atual. Se o
                     o último argumento para -exec é {} e você
                     especifique + em vez do ponto e vírgula (;),
                     o comando é chamado menos vezes, com
                     {} substituído por grupos de nomes de caminho. E se
                     qualquer invocação do comando retorna um
                     valor diferente de zero como status de saída, localize
                     retorna um status de saída diferente de zero.

Eu provavelmente poderia sair fazendo algo assim:

for f in $(find somedir); do
    foo
done

Mas tenho medo de lidar com problemas de separador de campos.

É possível chamar uma função shell (definida no mesmo script, não vamos nos preocupar com problemas de escopo) de uma find ... -exec ...chamada?

Eu tentei com ambos /usr/bin/finde /bin/finde obteve o mesmo resultado.

rahmu
fonte
você tentou exportar a função depois de declara-la? export -f foo
H3rrmiller 12/12/12
2
Você precisará transformar a função em um script externo e inseri-la PATH. Como alternativa, use sh -c '...'e ambos definem AND executam a função no ...bit. Pode ajudar a entender as diferenças entre funções e scripts .
jw013

Respostas:

27

A functioné local para um shell, então você precisa find -execgerar um shell e ter essa função definida nesse shell antes de poder usá-lo. Algo como:

find ... -exec ksh -c '
  function foo {
    echo blan: "$@"
  }
  foo "$@"' ksh {} +

bashpermite exportar funções através do ambiente export -f, para que você possa fazer (no bash):

foo() { ...; }
export -f foo
find ... -exec bash -c 'foo "$@"' bash {} +

ksh88tem typeset -fxque exportar a função (não pelo ambiente), mas isso só pode ser usado por menos scripts executados pelo she-bang ksh, por isso não com ksh -c.

Outra opção é fazer:

find ... -exec ksh -c "
  $(typeset -f foo)"'
  foo "$@"' ksh {} +

Ou seja, use typeset -fpara despejar a definição da foofunção dentro do script embutido. Observe que, se foousar outras funções, você também precisará despejá-las.

Stéphane Chazelas
fonte
2
Você pode explicar por que existem duas ocorrências kshou bashno -execcomando? Eu entendo a primeira, mas não a segunda ocorrência.
Daniel kullmann 15/10/12
6
@danielkullmann Em bash -c 'some-code' a b c, $0é a, $1é b..., então se você quiser $@ser, a, b, cprecisa inserir algo antes. Como $0também é usado ao exibir mensagens de erro, é uma boa ideia usar o nome do shell ou algo que faça sentido nesse contexto.
Stéphane Chazelas 15/10/12
@ StéphaneChazelas, obrigado por uma resposta tão agradável.
User9102d82
5

Isso nem sempre é aplicável, mas quando é, é uma solução simples. Defina a globstaropção ( set -o globstarno ksh93, shopt -s globstarno bash ≥4; está ativado por padrão no zsh). Em seguida, use **/para corresponder o diretório atual e seus subdiretórios recursivamente.

Por exemplo, em vez de find . -name '*.txt' -exec somecommand {} \;, você pode executar

for x in **/*.txt; do somecommand "$x"; done

Em vez de find . -type d -exec somecommand {} \;, você pode executar

for d in **/*/; do somecommand "$d"; done

Em vez de find . -newer somefile -exec somecommand {} \;, você pode executar

for x in **/*; do
  [[ $x -nt somefile ]] || continue
  somecommand "$x"
done

Quando **/não funciona para você (porque o seu shell não tê-lo, ou porque você precisa de uma findopção que não tem um análogo shell), definir a função no find -execargumento .

Gilles 'SO- parar de ser mau'
fonte
Não consigo encontrar uma globstaropção na versão do ksh ( ksh88) que estou usando.
rahmu
@rahmu De fato, é novo no ksh93. O Solaris 10 não tem um ksh93 em algum lugar?
Gilles 'SO- stop be evil'
De acordo com este post ksh93 foi introduzido no Solaris 11 para substituir tanto a Shell Bourne e ksh88 ...
rahmu
@rahmu. O Solaris possui o ksh93 há algum tempo como dtksh(ksh93 com algumas extensões X11), mas uma versão antiga e possivelmente parte de um pacote opcional em que o CDE é opcional.
Stéphane Chazelas
Deve-se notar que o globbing recursivo difere do findque exclui arquivos de ponto e não desce para dotdirs e classifica a lista de arquivos (os quais podem ser endereços no zsh por meio de qualificadores de globbing). Também com **/*, ao contrário ./**/*, nomes de arquivos podem começar com -.
Stéphane Chazelas
4

Use \ 0 como delimitador e leia os nomes de arquivos no processo atual a partir de um comando gerado, da seguinte maneira:

foo() {
  printf "Hello {%s}\n" "$1"
}

while read -d '' filename; do
  foo "${filename}" </dev/null
done < <(find . -maxdepth 2 -type f -print0)

Oque esta acontecendo aqui:

  • read -d '' lê até o próximo \ 0 byte, para que você não precise se preocupar com caracteres estranhos nos nomes de arquivos.
  • da mesma forma, -print0usa \ 0 para finalizar cada nome de arquivo gerado em vez de \ n.
  • cmd2 < <(cmd1)é o mesmo que cmd1 | cmd2exceto que o cmd2 é executado no shell principal e não em um subshell.
  • a chamada para foo é redirecionada de /dev/nullpara garantir que ela não seja lida acidentalmente no canal.
  • $filename é citado para que o shell não tente dividir um nome de arquivo que contenha espaço em branco.

Agora, read -de <(...)estão no zsh, bash e ksh 93u, mas não tenho certeza das versões anteriores do ksh.

aecolley
fonte
4

se você deseja que um processo filho, gerado em seu script, use uma função shell predefinida, é necessário exportá-lo com export -f <function>

NOTA: export -fé específico do bash

pois apenas um shell pode executar funções do shell :

find / -exec /bin/bash -c 'function "$1"' bash {} \;

EDIT: essencialmente o seu script deve se parecer com isso:

#!/bin/bash
function foo() { do something; }
export -f foo
find somedir -exec /bin/bash -c 'foo "$0"' {} \;
h3rrmiller
fonte
11
export -fé um erro de sintaxe em kshe imprime a definição da função na tela em zsh. Em todos os ksh, zshe bash, não resolve o problema.
rahmu
11
find / -exec /bin/bash -c 'function' \;
H3rrmiller
Agora funciona, mas apenas com bash. Obrigado!
rahmu
4
Nunca incorpore {}o código do shell! Isso significa que o nome do arquivo é interpretado como código shell por isso é muito perigoso
Stéphane Chazelas