Comprender la opción -exec de `find`

53

Me encuentro constantemente buscando la sintaxis de

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

principalmente porque no veo exactamente cómo funciona la -execpieza. ¿Cuál es el significado de las llaves, la barra diagonal inversa y el punto y coma? ¿Hay otros casos de uso para esa sintaxis?

Zsolt Szilagy
fuente
11
@Philippos: veo tu punto. Tenga en cuenta que las páginas de manual son una referencia, es decir, útiles para quienes entienden el asunto para buscar la sintaxis. Para alguien nuevo en el tema, a menudo son crípticos y formales para ser útiles. Encontrará que la respuesta aceptada es aproximadamente 10 veces más larga que la entrada de la página de manual, y eso es por una razón.
Zsolt Szilagy
66
Incluso la manpágina POSIX anterior dice: Un nombre de utilidad o argumento que contiene solo los dos caracteres "{}" se reemplazará por el nombre de ruta actual , que me parece suficiente. Además, tiene un ejemplo con -exec rm {} \;, al igual que en su pregunta. En mis días, apenas había otros recursos que la "gran pared gris", libros de manpáginas impresas (el papel era más barato que el almacenamiento). Entonces sé que esto es suficiente para alguien nuevo en el tema. Sin embargo, su última pregunta es justa de hacer aquí. Desafortunadamente ni @Kusalananda ni yo tenemos una respuesta a eso.
Philippos
1
Comeon @Philippos. ¿Realmente le estás diciendo a Kusalananda que no mejoró la página del manual? :-)
Zsolt Szilagy
1
@allo Aunque a xargsveces es útil, findpuede pasar múltiples argumentos de ruta al comando sin él. -exec command... {} +(con en +lugar de \;) pasa tantas rutas después command...como quepan (cada sistema operativo tiene su propio límite en cuanto a la longitud de una línea de comandos). Y al igual que xargs, la +forma terminada en de find's -execacción también se ejecutará command...varias veces en el caso raro de que hay demasiados caminos para ajustarse al límite.
Eliah Kagan
2
@ZsoltSzilagy No dije eso ni quise decir eso. Te alimentó muy bien, solo creo que tienes la edad suficiente para comer solo. (-;
Philippos

Respuestas:

90

Esta respuesta viene en las siguientes partes:

  • Uso básico de -exec
  • Utilizando -execen combinación consh -c
  • Utilizando -exec ... {} +
  • Utilizando -execdir

Uso básico de -exec

La -execopción toma una utilidad externa con argumentos opcionales como argumento y la ejecuta.

Si la cadena {}está presente en cualquier parte del comando dado, cada instancia de la misma será reemplazada por el nombre de ruta que se está procesando actualmente (por ejemplo ./some/path/FILENAME). En la mayoría de los shells, {}no es necesario citar los dos caracteres .

El comando debe terminarse con un ;para findsaber dónde termina (ya que puede haber más opciones después). Para protegerlo ;del shell, debe citarse como \;o ';', de lo contrario, el shell lo verá como el final del findcomando.

Ejemplo ( \al final de las dos primeras líneas son solo para continuar con las líneas):

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

Esto encontrará todos los archivos normales ( -type f) cuyos nombres coincidan con el patrón *.txten o debajo del directorio actual. Luego probará si la cadena se helloproduce en alguno de los archivos encontrados grep -q(lo que no produce ningún resultado, solo un estado de salida). Para aquellos archivos que contienen la cadena, catse ejecutará para enviar el contenido del archivo al terminal.

Cada uno -exectambién actúa como una "prueba" en los nombres de ruta encontrados por find, al igual que -typey lo -namehace. Si el comando devuelve un estado de salida cero (que significa "éxito"), se considera la siguiente parte del findcomando; de lo contrario, el findcomando continúa con el siguiente nombre de ruta. Esto se usa en el ejemplo anterior para buscar archivos que contienen la cadena hello, pero para ignorar todos los demás archivos.

El ejemplo anterior ilustra los dos casos de uso más comunes de -exec:

  1. Como prueba para restringir aún más la búsqueda.
  2. Para realizar algún tipo de acción en el nombre de ruta encontrado (generalmente, pero no necesariamente, al final del findcomando).

Utilizando -execen combinación consh -c

El comando que -execpuede ejecutarse está limitado a una utilidad externa con argumentos opcionales. -execNo es posible utilizar las funciones integradas de shell, funciones, condicionales, canalizaciones, redirecciones, etc. directamente , a menos que esté envuelto en algo así como un sh -cshell secundario.

Si bashse requieren funciones, úselas bash -cen lugar de sh -c.

sh -cse ejecuta /bin/shcon un script dado en la línea de comando, seguido de argumentos opcionales de la línea de comando para ese script.

Un ejemplo simple de uso sh -cpor sí mismo, sin find:

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

Esto pasa dos argumentos al script de shell hijo:

  1. La cadena sh. Estará disponible $0dentro del script, y si el shell interno genera un mensaje de error, lo colocará como prefijo con esta cadena.

  2. El argumento applesestá disponible como $1en el script, y si hubiera habido más argumentos, entonces estos hubieran estado disponibles como $2, $3etc. También estarían disponibles en la lista "$@"(excepto de los $0cuales no serían parte de "$@").

Esto es útil en combinación con, -execya que nos permite crear scripts arbitrariamente complejos que actúan sobre los nombres de ruta encontrados por find.

Ejemplo: busque todos los archivos normales que tengan un sufijo de nombre de archivo determinado y cambie ese sufijo de nombre de archivo a otro sufijo, donde los sufijos se mantienen en variables:

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 de la secuencia de comandos interna, $1estaría la cadena text, $2sería la cadena txty $3sería cualquier ruta que findhaya encontrado para nosotros. La expansión del parámetro ${3%.$1}tomaría la ruta y eliminaría el sufijo .text.

O, usando dirname/ basename:

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

o, con variables agregadas en el 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" {} ';'

Tenga en cuenta que en esta última variación, las variables fromy toen el shell hijo son distintas de las variables con los mismos nombres en el script externo.

Lo anterior es la forma correcta de llamar a un script complejo arbitrario -execcon find. Usando finden un bucle como

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

es propenso a errores y poco elegante (opinión personal). Está dividiendo nombres de archivos en espacios en blanco, invocando el bloqueo de nombres de archivo, y también obliga al shell a expandir el resultado completo findincluso antes de ejecutar la primera iteración del bucle.

Ver también:


Utilizando -exec ... {} +

Al ;final puede ser reemplazado por +. Esto hace findque se ejecute el comando dado con tantos argumentos (nombres de ruta encontrados) como sea posible en lugar de una vez para cada nombre de ruta encontrado. La cadena {} debe aparecer justo antes +de que esto funcione .

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

Aquí, findrecopilará los nombres de ruta resultantes y los ejecutará caten tantos como sea posible a la vez.

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

Del mismo modo aquí, mvse ejecutará la menor cantidad de veces posible. Este último ejemplo requiere GNU mvde coreutils (que admite la -topción).

El uso -exec sh -c ... {} +también es una forma eficiente de recorrer un conjunto de nombres de ruta con un script arbitrariamente complejo.

Lo básico es lo mismo que cuando se usa -exec sh -c ... {} ';', pero el script ahora toma una lista de argumentos mucho más larga. Estos se pueden recorrer en bucle "$@"dentro del script.

Nuestro ejemplo de la última sección que cambia los sufijos de nombre de archivo:

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" {} +

Utilizando -execdir

También existe -execdir(implementado por la mayoría de las findvariantes, pero no es una opción estándar).

Esto funciona como -execla diferencia de que el comando de shell dado se ejecuta con el directorio del nombre de ruta encontrado como su directorio de trabajo actual y que {}contendrá el nombre de base del nombre de ruta encontrado sin su ruta (pero GNU findseguirá prefijando el nombre de base con ./BSD). findNo haré eso).

Ejemplo:

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

Esto moverá cada *.txtarchivo encontrado a un done-textssubdirectorio preexistente en el mismo directorio donde se encontró el archivo . El archivo también será renombrado añadiéndole el sufijo .done.

Esto sería un poco más complicado de hacer, -execya que tendríamos que obtener el nombre base del archivo encontrado {}para formar el nuevo nombre del archivo. También necesitamos el nombre del directorio de {}para localizar el done-textsdirectorio correctamente.

Con -execdir, algunas cosas como estas se vuelven más fáciles.

La operación correspondiente usando en -execlugar de -execdirtendría que emplear un shell hijo:

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

o,

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +
Kusalananda
fuente
77
-exectoma un programa y argumentos y lo ejecuta; algunos comandos de shell consisten solo en un programa y argumentos, pero muchos no. Un comando de shell puede incluir redirección y canalización; -execno puede (aunque todo findse puede redirigir). Un comando de shell puede usar, ; && ifetc; -execno puede, aunque -a -opuede hacer algo. Un comando de shell puede ser un alias o una función de shell, o integrado; -execno puedo. Un comando de shell puede expandir vars; -execno puede (aunque la capa externa que ejecuta la findlata). Un comando de shell puede sustituir de manera $(command)diferente cada vez; -execno puedo. ...
dave_thompson_085
... Un comando de shell puede glob, -execno puede, aunque findpuede iterar sobre los archivos de la misma manera que la mayoría de los globs, por lo que rara vez se desea.
dave_thompson_085
@ dave_thompson_085 Por supuesto, el comando de shell puede ser él shmismo, que es capaz de hacer todas esas cosas
Tavian Barnes
2
Decir que es un comando de shell está mal aquí, find -exec cmd arg \;no invoca un shell para interpretar una línea de comando de shell, se ejecuta execlp("cmd", "arg")directamente, no execlp("sh", "-c", "cmd arg")(para lo cual el shell terminaría haciendo el equivalente de execlp("cmd", "arg")si cmdno estuviera integrado).
Stéphane Chazelas
2
Se podría aclarar que todos los findargumentos después -execy hasta ;o +compensar el comando a ejecutar junto con sus argumentos, con cada instancia de un {}argumento reemplaza con el archivo actual (con ;), y {}como último argumento antes +reemplazado con una lista de archivos como argumentos separados (en el {} +caso). IOW -exectoma varios argumentos, terminados en a ;o {} +.
Stéphane Chazelas