Obtenga una lista de subdirectorios que contienen un archivo cuyo nombre contiene una cadena

45

¿Cómo puedo obtener una lista de los subdirectorios que contienen un archivo cuyo nombre coincide con un patrón en particular?

Más específicamente, estoy buscando directorios que contengan un archivo con la letra 'f' en algún lugar del nombre del archivo.

Idealmente, la lista no tendría duplicados y solo contendría la ruta sin el nombre del archivo.

Muhd
fuente

Respuestas:

43
find . -type f -name '*f*' | sed -r 's|/[^/]+$||' |sort |uniq

Lo anterior encuentra todos los archivos debajo del directorio actual ( .) que son archivos normales ( -type f) y tienen falgún lugar en su nombre ( -name '*f*'). A continuación, sedelimina el nombre del archivo, dejando solo el nombre del directorio. Luego, la lista de directorios se ordena ( sort) y se eliminan los duplicados ( uniq).

El sedcomando consiste en un solo sustituto. Busca coincidencias con la expresión regular /[^/]+$y reemplaza todo lo que coincida con nada. El signo de dólar significa el final de la línea. [^/]+'significa uno o más caracteres que no son barras. Por lo tanto, /[^/]+$significa todos los caracteres desde la barra final hasta el final de la línea. En otras palabras, esto coincide con el nombre del archivo al final de la ruta completa. Por lo tanto, el comando sed elimina el nombre del archivo, dejando sin cambios el nombre del directorio en el que estaba el archivo.

Simplificaciones

Muchos sortcomandos modernos admiten una -ubandera que hace uniqinnecesaria. Para GNU sed:

find . -type f -name '*f*' | sed -r 's|/[^/]+$||' |sort -u

Y, para MacOS sed:

find . -type f -name '*f*' | sed -E 's|/[^/]+$||' |sort -u

Además, si su findcomando lo admite, es posible findimprimir directamente los nombres de directorio. Esto evita la necesidad de sed:

find . -type f -name '*f*' -printf '%h\n' | sort -u

Versión más robusta (requiere herramientas GNU)

Las versiones anteriores se confundirán con los nombres de archivo que incluyen nuevas líneas. Una solución más robusta es hacer la clasificación en cadenas terminadas en NUL:

find . -type f -name '*f*' -printf '%h\0' | sort -zu | sed -z 's/$/\n/'
John1024
fuente
Tengo muchos archivos que hacen que ordenarlos sea demasiado costoso. Lanzar uniqen la mezcla ayuda mucho al eliminar las líneas repetidas que ya están una al lado de la otra. find . -type f -name '*f*' -printf '%h\0' | uniq -z | sort -zu | tr '\0' '\n'. O si sus herramientas son un poco más antiguas, entonces uniq puede no tener la opción -z. find . -type f -name '*f*' -printf '%h\n' | uniq | sort -u
jbo5112
1
Usuarios de MacOS: el indicador sed no es -r. Por alguna razón es -E
David
@David Muy cierto. Respuesta actualizada para mostrar -Epara MacOS.
John1024
23

¿Por qué no probar esto?

find / -name '*f*' -printf "%h\n" | sort -u
Patrick Taylor
fuente
La mejor respuesta. Totalmente compatible con POSIX, a diferencia de algunas respuestas anteriores, anteriores, y también gana el premio especial The Shortest Pipeline :).
kkm
Me encantaría ver a alguien mostrar el momento de esto frente a los otros anteriores, porque tengo la sensación de que este es, con mucho, el más rápido.
dlamblin
44
@kkm Estoy de acuerdo con que esta es la mejor solución, pero las especificaciones POSIXfind son bastante escasas: el -printfoperador no está especificado. Esto no funciona con BSD find. Por lo tanto, no es "totalmente compatible con POSIX". (Aunque sort -u está en POSIX .)
Comodín el
8

Básicamente, existen 2 métodos que puede usar para hacer esto. Uno analizará la cadena mientras que el otro operará en cada archivo. El análisis de la cadena usa una herramienta como grep, sedo awkobviamente será más rápido, pero aquí hay un ejemplo que muestra ambos, así como también cómo puedes "perfilar" los 2 métodos.

Data de muestra

Para los ejemplos a continuación usaremos los siguientes datos

$ touch dir{1..3}/dir{100..112}/file{1..5}
$ touch dir{1..3}/dir{100..112}/nile{1..5}
$ touch dir{1..3}/dir{100..112}/knife{1..5}

Eliminar algunos de los *f*archivos de dir1/*:

$ rm dir1/dir10{0..2}/*f*

Enfoque n. ° 1: análisis a través de cadenas

Aquí vamos a utilizar las siguientes herramientas, find, grep, y sort.

$ find . -type f -name '*f*' | grep -o "\(.*\)/" | sort -u | head -5
./dir1/dir103/
./dir1/dir104/
./dir1/dir105/
./dir1/dir106/
./dir1/dir107/

Enfoque n. ° 2: análisis mediante archivos

La misma cadena de herramientas que antes, excepto que esta vez usaremos en dirnamelugar de grep.

$ find . -type f -name '*f*' -exec dirname {} \; | sort -u | head -5
./dir1/dir103
./dir1/dir104
./dir1/dir105
./dir1/dir106
./dir1/dir107

NOTA: Los ejemplos anteriores se utilizan head -5para limitar simplemente la cantidad de salida que estamos tratando para estos ejemplos. ¡Normalmente se eliminarían para obtener su listado completo!

Comparando los resultados

Podemos usar timepara echar un vistazo a los 2 enfoques.

dirname

real        0m0.372s
user        0m0.028s
sys         0m0.106s

grep

real        0m0.012s
user        0m0.009s
sys         0m0.007s

Por lo tanto, siempre es mejor lidiar con las cadenas si es posible.

Métodos alternativos de análisis de cadenas

grep y PCRE

$ find . -type f -name '*f*' | grep  -oP '^.*(?=/)' | sort -u

sed

$ find . -type f -name '*f*' | sed 's#/[^/]*$##' | sort -u

awk

$ find . -type f -name '*f*' | awk -F'/[^/]*$' '{print $1}' | sort -u
slm
fuente
+1 Porque funciona, pero curiosamente, esto lleva muchas veces más tiempo que la respuesta de @ John1024
Muhd
@ Muhd: sí, las llamadas a dirname son lentas. Estoy trabajando en una alternativa.
slm
2

Aquí hay uno que me parece útil:

find . -type f -name "*somefile*" | xargs dirname | sort | uniq
Martin Tapp
fuente
1

Esta respuesta se basa descaradamente en la respuesta slm. Fue un enfoque interesante, pero tiene una limitación si los nombres de archivo y / o directorio tienen caracteres especiales (espacio, semicolumna ...). Un buen hábito es usar find /somewhere -print0 | xargs -0 someprogam.

Data de muestra

Para los ejemplos a continuación usaremos los siguientes datos

mkdir -p dir{1..3}/dir\ {100..112}
touch dir{1..3}/dir\ {100..112}/nile{1..5}
touch dir{1..3}/dir\ {100..112}/file{1..5}
touch dir{1..3}/dir\ {100..112}/kni\ fe{1..5}

Eliminar algunos de los *f*archivos de dir1/*/:

rm dir1/dir\ 10{0..2}/*f*

Enfoque n. ° 1: análisis mediante archivos

$ find -type f -name '*f*' -print0 | sed -e 's#/[^/]*\x00#\x00#g' | sort -zu | xargs -0 -n1 echo | head -n5
./dir1/dir 103
./dir1/dir 104
./dir1/dir 105
./dir1/dir 106
./dir1/dir 107

NOTA : Los ejemplos anteriores se utilizan head -5para limitar simplemente la cantidad de salida que estamos tratando para estos ejemplos. ¡Normalmente se eliminarían para obtener su listado completo! Además, reemplace el echocomando que quiera usar.

Franklin Piat
fuente
1

Con zsh:

typeset -aU dirs # array with unique values
dirs=(**/*f*(D:h))

printf '%s\n' $dirs
Stéphane Chazelas
fuente