Wykonywanie funkcji zdefiniowanej przez użytkownika w wywołaniu find -exec

25

Korzystam z systemu Solaris 10 i przetestowałem następujące opcje z ksh (88), bash (3.00) i zsh (4.2.1).

Poniższy kod nie daje żadnego wyniku:

function foo {
    echo "Hello World"
}

find somedir -exec foo \;

Znalezisko pasuje kilka plików (jak pokazano poprzez zastąpienie -exec ...z -print), a funkcja działa idealnie, kiedy nazywa zewnątrz od findpołączenia.

Oto, co man findmówi strona -exec:

 -exec polecenie Prawda, jeśli wykonane polecenie zwraca a
                     zero wartości jako status wyjścia. Koniec
                     polecenie musi być przerywane znakiem ucieczki
                     średnik (;). Argumentem polecenia {} jest
                     zastąpiony bieżącą nazwą ścieżki. Jeśli…
                     ostatnim argumentem dla -exec jest {} a ty
                     podaj + zamiast średnika (;),
                     polecenie jest wywoływane mniej razy, przy pomocy
                     {} zastąpiono grupami ścieżek. Jeśli
                     każde wywołanie polecenia zwraca a
                     wartość niezerowa jako status wyjścia, znajdź
                     zwraca niezerowy status wyjścia.

Prawdopodobnie mógłbym uciec, robiąc coś takiego:

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

Ale boję się rozwiązywać problemy z separatorem pól.

Czy można wywołać funkcję powłoki (zdefiniowaną w tym samym skrypcie, nie zawracajmy sobie głowy problemami z zakresu) z find ... -exec ...wywołania?

Próbowałem zarówno /usr/bin/findi /bin/findotrzymał ten sam wynik.

rahmu
źródło
próbowałeś wyeksportować funkcję po jej zadeklarowaniu? export -f foo
h3rrmiller
2
Musisz uczynić tę funkcję zewnętrznym skryptem i wstawić ją PATH. Alternatywnie użyj sh -c '...'i zdefiniuj ORAZ uruchom funkcję w ...bicie. Może pomóc zrozumieć różnice między funkcjami a skryptami .
jw013,

Odpowiedzi:

27

A functionjest lokalne dla powłoki, więc musisz find -execmóc spawnować powłokę i mieć zdefiniowaną funkcję w tej powłoce, zanim będziesz mógł z niej korzystać. Coś jak:

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

bashpozwala eksportować funkcje za pomocą środowiska export -f, dzięki czemu możesz (bash):

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

ksh88musi typeset -fxwyeksportować funkcję (nie przez środowisko), ale może być używana tylko przez skrypty wykonujące mniej she-bang ksh, więc nie z ksh -c.

Inną opcją jest wykonanie:

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

To znaczy, użyj, typeset -faby zrzucić definicję foofunkcji w skrypcie wbudowanym. Pamiętaj, że jeśli fooużywasz innych funkcji, musisz je również zrzucić.

Stéphane Chazelas
źródło
2
Czy możesz wyjaśnić, dlaczego występują dwa polecenia kshlub bashw -execpoleceniu? Rozumiem pierwsze, ale nie drugie wystąpienie.
Daniel Kullmann
6
@danielkullmann In bash -c 'some-code' a b c, $0is a, $1is b..., więc jeśli chcesz $@być a, b, c, musisz coś wcześniej wstawić. Ponieważ $0jest również używany podczas wyświetlania komunikatów o błędach, dobrym pomysłem jest użycie nazwy powłoki lub czegoś, co ma sens w tym kontekście.
Stéphane Chazelas
@ StéphaneChazelas, dziękuję za tak miłą odpowiedź.
User9102d82,
5

Nie zawsze ma to zastosowanie, ale kiedy jest, jest to proste rozwiązanie. Ustaw globstaropcję ( set -o globstarw ksh93, shopt -s globstarw bash ≥4; jest domyślnie włączony w zsh). Następnie użyj, **/aby rekurencyjnie dopasować bieżący katalog i jego podkatalogi.

Na przykład zamiast find . -name '*.txt' -exec somecommand {} \;można uruchomić

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

Zamiast tego find . -type d -exec somecommand {} \;możesz biegać

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

Zamiast tego find . -newer somefile -exec somecommand {} \;możesz biegać

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

Kiedy **/nie działa dla ciebie (ponieważ twoja powłoka go nie ma lub potrzebujesz findopcji, która nie ma analogu powłoki), zdefiniuj funkcję w find -execargumencie .

Gilles „SO- przestań być zły”
źródło
Nie mogę znaleźć globstaropcji w używanej przeze ksh88mnie wersji ksh ( ).
rahmu
@rahmu Rzeczywiście, jest nowy w ksh93. Czy Solaris 10 nie ma gdzieś ksh93?
Gilles „SO- przestań być zły”
Zgodnie z tym blogu ksh93 została wprowadzona w systemie Solaris 11, aby zastąpić zarówno Bourne Shell i ksh88 ...
rahmu
@rahmu. Solaris ma już ksh93 jako dtksh(ksh93 z niektórymi rozszerzeniami X11), ale starą wersję i być może część opcjonalnego pakietu, w którym CDE jest opcjonalny.
Stéphane Chazelas
Należy zauważyć, że globowanie rekurencyjne różni się findtym, że wyklucza pliki kropkowe i nie schodzi na kropki i że sortuje listę plików (oba mogą być adresami w zsh poprzez kwalifikatory globowania). Również **/*w przeciwieństwie do ./**/*, nazwy plików mogą zaczynać się od -.
Stéphane Chazelas
4

Użyj \ 0 jako separatora i odczytaj nazwy plików w bieżącym procesie z polecenia spawnowanego, tak jak poniżej:

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

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

Co tu się dzieje:

  • read -d '' czyta do następnego bajtu \ 0, więc nie musisz się martwić o dziwne znaki w nazwach plików.
  • podobnie -print0używa \ 0 do zakończenia każdej wygenerowanej nazwy pliku zamiast \ n.
  • cmd2 < <(cmd1)jest taki sam, cmd1 | cmd2z wyjątkiem tego, że cmd2 jest uruchamiany w głównej powłoce, a nie w podpowłoce.
  • wywołanie foo jest przekierowywane z, /dev/nullaby upewnić się, że nie odczytuje przypadkowo z potoku.
  • $filename jest cytowany, aby powłoka nie próbowała podzielić nazwy pliku zawierającej spacje.

Teraz, read -di <(...)są w zsh, bash i ksh 93u, ale nie jestem pewien co do wcześniejszych wersji ksh.

Aecolley
źródło
4

jeśli chcesz, aby proces potomny, spawnowany ze skryptu, używał wstępnie zdefiniowanej funkcji powłoki, z którą musisz ją wyeksportować export -f <function>

UWAGA: export -fjest specyficzny dla bash

ponieważ tylko powłoka może uruchamiać funkcje powłoki :

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

EDYCJA: zasadniczo twój skrypt powinien przypominać to:

#!/bin/bash
function foo() { do something; }
export -f foo
find somedir -exec /bin/bash -c 'foo "$0"' {} \;
h3rrmiller
źródło
1
export -fjest błędem składni w kshi drukuje definicję funkcji na ekranie w zsh. We wszystkich ksh, zshi bashto nie rozwiązuje problemu.
rahmu
1
find / -exec /bin/bash -c 'function' \;
h3rrmiller
Teraz działa, ale tylko z bash. Dziękuję Ci!
rahmu
4
Nigdy nie osadzaj {}w kodzie powłoki! Oznacza to, że nazwa pliku jest interpretowana jako kod powłoki, więc jest bardzo niebezpieczny
Stéphane Chazelas