¿Encontrar -exec una función de shell en Linux?

185

¿Hay alguna manera de findejecutar una función que defino en el shell? Por ejemplo:

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

El resultado de eso es:

find: dosomething: No such file or directory

¿Hay una manera de conseguir find's -execpara ver dosomething?

alxndr
fuente

Respuestas:

262

Dado que solo el shell sabe cómo ejecutar funciones de shell, debe ejecutar un shell para ejecutar una función. También debe marcar su función para exportar export -f, de lo contrario la subshell no las heredará:

export -f dosomething
find . -exec bash -c 'dosomething "$0"' {} \;
Adam Rosenfield
fuente
77
Tu me golpeaste. Por cierto, puedes poner las llaves dentro de las comillas en lugar de usarlas $0.
Pausado hasta nuevo aviso.
20
Si lo usa find . -exec bash -c 'dosomething "$0"' {} \;, manejará espacios (y otros caracteres extraños) en los nombres de archivo ...
Gordon Davisson
3
@alxndr: eso fallará en los nombres de archivo con comillas dobles, comillas inversas, signos de dólar, algunos combos de escape, etc ...
Gordon Davisson
44
Tenga en cuenta también que cualquier función que su función pueda estar llamando no estará disponible a menos que exporte -f también.
hraban
3
El export -ffuncionará sólo en algunas versiones de bash. No es posix, no crossplatforn, /bin/shtendrá un error con él
Роман Коптев
120
find . | while read file; do dosomething "$file"; done
Jac
fuente
10
Buena solución No requiere exportar la función o perder el tiempo para escapar de los argumentos y es presumiblemente más eficiente ya que no está generando subcapas para ejecutar cada función.
Tom
19
Sin embargo, tenga en cuenta que se romperá en los nombres de archivo que contengan nuevas líneas.
chepner
3
Esto es más "shell'ish" ya que sus variables y funciones globales estarán disponibles, sin crear un shell / entorno completamente nuevo cada vez. Aprendí esto de la manera difícil después de probar el método de Adam y encontrarse con todo tipo de problemas ambientales. Este método tampoco corrompe el shell de su usuario actual con todas las exportaciones y requiere menos diciplina.
Ray Foss
¡MUCHO MÁS RÁPIDO que la respuesta aceptada sin sub shell! ¡BONITO! ¡Gracias!
Bill Heller
2
También solucioné mi problema cambiando while readpara un bucle for; for item in $(find . ); do some_function "${item}"; done
user5359531
22

La respuesta de Jak anterior es excelente, pero tiene un par de dificultades que se pueden superar fácilmente:

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

Esto usa nulo como delimitador en lugar de un salto de línea, por lo que los nombres de archivo con saltos de línea funcionarán. También usa la -rbandera que deshabilita el escape de la barra invertida, sin que las barras invertidas en los nombres de archivo no funcionen. También se borra IFSpara que no se descarten los espacios en blanco finales potenciales en los nombres.

pajamian
fuente
1
Es bueno /bin/bashpero no funcionará /bin/sh. Lo que es una lástima.
Роман Коптев
@ РоманКоптев Qué suerte que al menos funcione en / bin / bash.
sdenham
21

Agregue citas {}como se muestra a continuación:

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

Esto corrige cualquier error debido a caracteres especiales devueltos por find, por ejemplo, archivos con paréntesis en su nombre.

Wagner
fuente
1
Esta no es la forma correcta de usar {}. Esto se romperá para un nombre de archivo que contenga comillas dobles. touch '"; rm -rf .; echo "I deleted all you files, haha. Ups
gniourf_gniourf
Si, esto es muy malo. Puede ser explotado por inyecciones. Muy inseguro ¡No uses esto!
Dominik
1
@kdubs: Use $ 0 (sin comillas) dentro de la cadena de comandos y pase el nombre del archivo como primer argumento: -exec bash -c 'echo $0' '{}' \;Tenga en cuenta que cuando usa bash -c, $ 0 es el primer argumento, no el nombre del script.
sdenham
10

Resultados de procesamiento a granel

Para una mayor eficiencia, muchas personas utilizan xargspara procesar resultados en masa, pero es muy peligroso. Debido a eso, se introdujo un método alternativo findque ejecuta resultados en masa.

Sin embargo, findtenga {}en cuenta que este método puede venir con algunas advertencias como, por ejemplo, un requisito en POSIX para tener al final del comando.

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

findpasará muchos resultados como argumentos a una sola llamada de bashy el forbucle iterará a través de esos argumentos, ejecutando la función dosomethingen cada uno de ellos.

La solución anterior inicia argumentos en $1, por lo que hay un _(que representa $0).

Procesando resultados uno por uno

Del mismo modo, creo que la respuesta superior aceptada debería corregirse para que sea

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

Esto no solo es más sensato, porque los argumentos siempre deben comenzar en $1, sino que también el uso $0podría conducir a un comportamiento inesperado si el nombre de archivo devuelto por findtiene un significado especial para el shell.

Dominik
fuente
9

Haga que el script se llame a sí mismo, pasando cada elemento encontrado como argumento:

#!/bin/bash

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

find . -exec $0 {} \;

exit 0

Cuando ejecuta el script por sí mismo, encuentra lo que está buscando y se llama a sí mismo pasando cada resultado de búsqueda como argumento. Cuando el script se ejecuta con un argumento, ejecuta los comandos en el argumento y luego sale.

Mike Maready
fuente
idea genial pero mal estilo: usa el mismo script para dos propósitos. si desea reducir la cantidad de archivos en su bin / entonces podría fusionar todos sus scripts en uno solo que tenga una cláusula de mayúsculas y minúsculas al inicio. solución muy limpia, ¿no?
user829755
sin mencionar que esto fallará find: ‘myscript.sh’: No such file or directorysi se inicia como bash myscript.sh...
Camusensei
5

Para aquellos de ustedes que buscan una función bash que ejecute un comando dado en todos los archivos en el directorio actual, he compilado uno de las respuestas anteriores:

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

Tenga en cuenta que se rompe con nombres de archivos que contienen espacios (ver más abajo).

Como ejemplo, tome esta función:

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

Digamos que quería cambiar todas las instancias de hello a world en todos los archivos en el directorio actual. Yo lo haría:

toall world

Para estar seguro con cualquier símbolo en los nombres de archivo, use:

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

(pero necesita un findque maneje -print0, por ejemplo, GNU find).

Jason Basanese
fuente
3

Encuentro la manera más fácil de la siguiente manera, repitiendo dos comandos en un solo do

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
edib
fuente
3

Como referencia, evito este escenario usando:

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

Obtenga la salida de find en el archivo de secuencia de comandos actual e itere sobre la salida como desee. Estoy de acuerdo con la respuesta aceptada, pero no quiero exponer la función fuera de mi archivo de script.

dinesh saini
fuente
esto tiene límites de tamaño
Richard
2

No es posible ejecutar una función de esa manera.

Para superar esto, puede colocar su función en un script de shell y llamar a eso desde find

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

Ahora úsalo en buscar como:

find . -exec dosomething.sh {} \;
codictorio
fuente
Intentaba evitar más archivos en mi ~ / bin. Gracias sin embargo!
alxndr
Consideré el voto negativo, pero la solución en sí misma no es mala. Simplemente utilice la cita correcta: dosomething $1=> dosomething "$1"e inicie su archivo correctamente confind . -exec bash dosomething.sh {} \;
Camusensei
2

Coloque la función en un archivo separado y findejecute eso.

Las funciones de shell son internas al shell en el que están definidas; findnunca podrá verlos.

Angus
fuente
Gotcha tiene sentido. Sin embargo, intentaba evitar más archivos en mi ~ / bin.
alxndr
2

Para proporcionar adiciones y aclaraciones a algunas de las otras respuestas, si está utilizando la opción masiva para execo execdir( -exec command {} +), y desea recuperar todos los argumentos posicionales, debe considerar el manejo de $0con bash -c. Más concretamente, considere el siguiente comando, que utiliza bash -ccomo se sugirió anteriormente, y simplemente repite las rutas de archivo que terminan con '.wav' de cada directorio que encuentra:

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

El manual de bash dice:

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.

Aquí, 'check $@'está la cadena de comando, y _ {}son los argumentos después de la cadena de comando. Tenga en cuenta que $@es un parámetro posicional especial en bash que se expande a todos los parámetros posicionales a partir de 1 . También tenga en cuenta que con la -copción, el primer argumento se asigna al parámetro posicional $0. Esto significa que si intenta acceder a todos los parámetros posicionales con $@, solo obtendrá parámetros a partir de $1y hacia arriba. Esa es la razón por la cual la respuesta de Dominik tiene el _, que es un argumento ficticio para completar el parámetro $0, por lo que todos los argumentos que queremos están disponibles más adelante si usamos la $@expansión de parámetros, por ejemplo, o el bucle for como en esa respuesta.

Por supuesto, similar a la respuesta aceptada, bash -c 'shell_function $0 $@'también funcionaría al pasar explícitamente $0, pero nuevamente, debe tener en cuenta que $@no funcionará como se esperaba.

sargento
fuente
0

No directamente, no. Find se está ejecutando en un proceso separado, no en su shell.

Cree un script de shell que haga el mismo trabajo que su función y encuentre -execeso.

Laurence Gonsalves
fuente
Intentaba evitar más archivos en mi ~ / bin. Gracias sin embargo!
alxndr
-2

Evitaría usar por -execcompleto. ¿Por qué no usar xargs?

find . -name <script/command you're searching for> | xargs bash -c
Barry
fuente
En ese momento, el IIRC intentaba reducir la cantidad de recursos utilizados. Piense en encontrar millones de archivos vacíos y eliminarlos.
alxndr