Obter lista de subdiretórios que contêm um arquivo cujo nome contém uma sequência

45

Como posso obter uma lista dos subdiretórios que contêm um arquivo cujo nome corresponde a um padrão específico?

Mais especificamente, estou procurando diretórios que contenham um arquivo com a letra 'f' em algum lugar do nome do arquivo.

Idealmente, a lista não teria duplicatas e conteria apenas o caminho sem o nome do arquivo.

Muhd
fonte

Respostas:

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

O texto acima localiza todos os arquivos abaixo do diretório atual ( .) que são arquivos regulares ( -type f) e que têm falgum lugar em seu nome ( -name '*f*'). Em seguida, sedremove o nome do arquivo, deixando apenas o nome do diretório. Em seguida, a lista de diretórios é classificada ( sort) e as duplicatas removidas ( uniq).

O sedcomando consiste em um único substituto. Ele procura correspondências para a expressão regular /[^/]+$e substitui qualquer coisa que corresponda a isso por nada. O cifrão significa o fim da linha. [^/]+'significa um ou mais caracteres que não são barras. Assim, /[^/]+$significa todos os caracteres da barra final até o final da linha. Em outras palavras, isso corresponde ao nome do arquivo no final do caminho completo. Portanto, o comando sed remove o nome do arquivo, deixando inalterado o nome do diretório em que o arquivo estava.

Simplificações

Muitos sortcomandos modernos suportam uma -ubandeira que torna uniqdesnecessária. Para GNU sed:

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

E, para o MacOS sed:

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

Além disso, se o seu findcomando o suportar, é possível findimprimir diretamente os nomes dos diretórios. Isso evita a necessidade de sed:

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

Versão mais robusta (requer ferramentas GNU)

As versões acima serão confundidas com nomes de arquivos que incluem novas linhas. Uma solução mais robusta é fazer a classificação em cadeias terminadas em NUL:

find . -type f -name '*f*' -printf '%h\0' | sort -zu | sed -z 's/$/\n/'
John1024
fonte
Eu tenho muitos arquivos que tornam a classificação muito cara. Jogar uniqna mistura ajuda muito removendo as linhas repetidas que já estão próximas umas das outras. find . -type f -name '*f*' -printf '%h\0' | uniq -z | sort -zu | tr '\0' '\n'. Ou, se suas ferramentas forem um pouco mais antigas, o uniq pode não ter a opção -z. find . -type f -name '*f*' -printf '%h\n' | uniq | sort -u
jbo5112
1
Usuários de MacOS: o sinalizador sed não é -r. Por alguma razão a sua -E
David
@ David Muito verdade. Resposta atualizada para mostrar -Eno MacOS.
precisa saber é o seguinte
23

Por que não tentar isso:

find / -name '*f*' -printf "%h\n" | sort -u
Patrick Taylor
fonte
Melhor resposta. Inteiramente compatível com POSIX, ao contrário de algumas respostas acima, acima, e também ganha o prêmio especial The Shortest Pipeline :).
kkm
Eu adoraria ver alguém mostrar o momento disso em relação aos outros acima, porque sinto que esse é de longe o mais rápido.
dlamblin
4
@kkm Concordo que esta é a melhor solução, mas as especificações do POSIXfind são na verdade bastante esparsas - o -printfoperador não está especificado. Isso não funciona com o BSD find. Portanto, não é "totalmente compatível com POSIX". (Embora sort -u esteja em POSIX .)
Curinga
8

Existem essencialmente 2 métodos que você pode usar para fazer isso. Um analisará a string enquanto o outro operará em cada arquivo. Analisando a corda usar uma ferramenta como grep, sedou awké, obviamente, vai ser mais rápido, mas aqui está um exemplo mostrando tanto, bem como como você pode "perfil" dos 2 métodos.

Dados de amostra

Para os exemplos abaixo, usaremos os seguintes dados

$ 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}

Exclua alguns dos *f*arquivos de dir1/*:

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

Abordagem # 1 - Analisando via Strings

Aqui nós estamos indo para usar as seguintes ferramentas, find, grep, e sort.

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

Abordagem # 2 - Análise usando arquivos

A mesma cadeia de ferramentas de antes, exceto que desta vez usaremos em dirnamevez de grep.

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

NOTA: Os exemplos acima estão usando head -5apenas para limitar a quantidade de saída com a qual estamos lidando nesses exemplos. Eles normalmente seriam removidos para obter sua lista completa!

Comparando os resultados

Podemos usar timepara dar uma olhada nas 2 abordagens.

dirname

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

grep

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

Portanto, é sempre melhor lidar com as cordas, se possível.

Métodos alternativos de análise de string

grep & 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
fonte
+1 Porque ele funciona, mas curiosamente isso leva muitas vezes mais do que resposta da @ John1024
Muhd
@ Muhd ​​- sim, as chamadas para dirname são lentas. Estou trabalhando em uma alternativa.
Slm
2

Aqui está um que eu acho útil:

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

Esta resposta é descaradamente baseada na resposta slm. Foi uma abordagem interessante, mas tem uma limitação se os nomes de arquivos e / ou diretórios tiverem caracteres especiais (espaço, semi-coluna ...). Um bom hábito é usar find /somewhere -print0 | xargs -0 someprogam.

Dados de amostra

Para os exemplos abaixo, usaremos os seguintes dados

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}

Exclua alguns dos *f*arquivos de dir1/*/:

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

Abordagem # 1 - Análise usando arquivos

$ 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 : Os exemplos acima estão usando head -5apenas para limitar a quantidade de saída com a qual estamos lidando nesses exemplos. Eles normalmente seriam removidos para obter sua lista completa! Além disso, substitua o echocomando que você deseja usar.

Franklin Piat
fonte
1

Com zsh:

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

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