Como relatar o número de arquivos em todos os subdiretórios?


24

Preciso inspecionar todos os subdiretórios e relatar quantos arquivos (sem mais recursões) eles contêm:

directoryName1 numberOfFiles
directoryName2 numberOfFiles

Por que você deseja usar findquando o Bash fará? (shopt -s dotglob; for dir in */; do all=("$dir"/*); echo "$dir: ${#all[@]}"; done): Para todos os diretórios, contar o número de entradas no diretório (incluindo arquivos de pontos ocultos, excluindo .e ..)
janmoesen

@janmoesen Por que você não respondeu isso? Eu sou novo no shell script, mas não consigo ver nenhuma dica com seu método. Para mim, parece o melhor caminho. Ninguém votou positivamente no seu comentário, mas ninguém comentou por que ele pode ser ruim também. As respostas votadas têm muito mais reputação do que você, por isso me faz pensar se estou perdendo alguma coisa.
toxalot 10/03/14

@toxalot: eu não me incomodei em adicioná-lo como resposta, porque era muito curto (e possivelmente um tom condescendente). Sinta-se livre para votar o comentário. :-) Além disso, a pergunta é um tanto vaga quanto ao significado de "quantos arquivos". Minha solução conta com arquivos e diretórios "regulares" ; talvez o pôster realmente significasse "arquivos, não diretórios". Outra coisa a ter em mente é que esse globbing não leva em consideração os arquivos de pontos "ocultos". Existem maneiras de contornar esses dois truques, no entanto. Mais uma vez: não tenho certeza dos requisitos exatos do pôster original.
Janmoesen 23/05

Respostas:


30

Isso é feito de maneira segura e portátil. Não ficará confuso com nomes de arquivos estranhos.

for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1 -exec echo \; | wc -l && echo $f; done

Observe que ele imprimirá o número de arquivos primeiro, depois o nome do diretório em uma linha separada. Se você deseja manter o formato do OP, precisará de mais formatação, por exemplo

for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1 -exec echo \;|wc -l|tr '\n' ' ' && echo $f; done|awk '{print $2"\t"$1}'

Se você tiver um conjunto específico de subdiretórios de seu interesse, poderá substituí *-lo por eles.

Por que isso é seguro? (e, portanto, digno de script)

Os nomes de arquivos podem conter qualquer caractere, exceto /. Existem alguns caracteres que são tratados especialmente pelo shell ou pelos comandos. Isso inclui espaços, novas linhas e traços.

Usar a for f in *construção é uma maneira segura de obter cada nome de arquivo, não importa o que ele contenha.

Depois de ter o nome do arquivo em uma variável, você ainda precisa evitar coisas como find $f. Se $fcontinha o nome do arquivo -test, findreclamaria da opção que você acabou de dar. A maneira de evitar isso é usando ./na frente do nome; dessa forma, ele tem o mesmo significado, mas não começa mais com um traço.

Novas linhas e espaços também são um problema. Se $fcontiver "olá, amigo" como um nome de arquivo,, find ./$fé find ./hello, buddy. Você está dizendo findpara olhar ./hello,e buddy. Se esses não existirem, ele irá reclamar e nunca irá investigar ./hello, buddy. Isso é fácil de evitar - use aspas em torno de suas variáveis.

Por fim, os nomes de arquivos podem conter novas linhas, portanto, contar novas linhas em uma lista de nomes de arquivos não funcionará; você receberá uma contagem extra para cada nome de arquivo com uma nova linha. Para evitar isso, não conte novas linhas em uma lista de arquivos; em vez disso, conte novas linhas (ou qualquer outro caractere) que representem um único arquivo. É por isso que o findcomando tem simplesmente -exec echo \;e não -exec echo {} \;. Eu só quero imprimir uma única nova linha com o objetivo de calcular os arquivos.


1
Por que existe uma pessoa no mundo que usa novas linhas no nome do arquivo? Obrigado pela resposta.
ShyBoy 23/10

1
Os nomes de arquivos podem conter qualquer caractere, exceto / e o caractere nulo, acredito. dwheeler.com/essays/fixing-unix-linux-filenames.html
Flimm

2
A contagem incluirá o próprio diretório. Se você quiser excluir isso da contagem, use-mindepth 1
toxalot

Você também pode usar em -printf '\n'vez de -exec echo.
toxalot

1
@toxalot é possível se você tiver uma descoberta que suporte -printf, mas não se quiser que ele funcione no FreeBSD, por exemplo.
Shawn J. Goff

6

Supondo que você esteja procurando uma solução Linux padrão, uma maneira relativamente direta de conseguir isso é find:

find dir1/ dir2/ -maxdepth 1 -type f | wc -l

Onde findatravessa os dois subdiretórios especificados, para um -maxdepthde 1, o que evita recursões adicionais e apenas relata arquivos ( -type f) separados por novas linhas. O resultado é então canalizado wcpara contar o número dessas linhas.


Eu tenho mais de 2 dirs ... Como posso combinar seu comando com a find . -maxdepth 1 -type dsaída?
ShyBoy 23/10

Você poderia (a) incluem os diretórios necessários em uma variável e find $dirs ...ou, (b) se forem exclusivamente no diretório um nível mais elevado, glob a partir desse diretório,find */ ...
jasonwryan

1
Isso informará resultados incorretos se algum nome de arquivo tiver um caractere de nova linha.
Shawn J. Goff

@ Shawn: obrigado. Eu pensei que tinha nomes de arquivos com espaços cobertos, mas não havia considerado novas linhas: alguma sugestão para uma correção?
jasonwryan

Adicione -exec echoao seu comando find - dessa forma, não ecoará o nome do arquivo, apenas uma nova linha.
Shawn J. Goff

4

Por "sem recursão", você quer dizer que se directoryName1possui subdiretórios, não deseja contar os arquivos nos subdiretórios? Nesse caso, aqui está uma maneira de contar todos os arquivos regulares nos diretórios indicados:

count=0
for d in directoryName1 directoryName2; do
  for f in "$d"/* "$d"/.[!.]* "$d"/..?*; do
    if [ -f "$f" ]; then count=$((count+1)); fi
  done
done

Observe que o -fteste executa duas funções: testa se a entrada correspondente a um dos globs acima é um arquivo regular e testa se a entrada foi uma correspondência (se um dos globs não corresponder a nada, o padrão permanecerá como ¹). Se você deseja contar todas as entradas nos diretórios fornecidos, independentemente do tipo, substitua -fpor -e.

O Ksh tem uma maneira de fazer com que os padrões correspondam aos arquivos de ponto e produzir uma lista vazia, caso nenhum arquivo corresponda a um padrão. Portanto, no ksh, você pode contar arquivos regulares como este:

FIGNORE='.?(.)'
count=0
for x in ~(N)directoryName1/* ~(N)directoryName2/*; do
  if [ -f "$x" ]; then ((++count)); fi
done

ou todos os arquivos simplesmente assim:

FIGNORE='.?(.)'
files=(~(N)directoryName1/* ~(N)directoryName2/*)
count=${#files}

O Bash tem maneiras diferentes de simplificar isso. Para contar arquivos regulares:

shopt -s dotglob nullglob
count=0
for x in directoryName1/* directoryName2/*; do
  if [ -f "$x" ]; then ((++count)); fi
done

Para contar todos os arquivos:

shopt -s dotglob nullglob
files=(directoryName1/* directoryName2/*)
count=${#files}

Como de costume, é ainda mais simples no zsh. Para contar arquivos regulares:

files=({directoryName1,directoryName2}/*(DN.))
count=$#files

Mude (DN.)para (DN)para contar todos os arquivos.

¹ Observe que cada padrão corresponde a si próprio; caso contrário, os resultados podem estar desativados (por exemplo, se você estiver contando arquivos que começam com um dígito, você não pode simplesmente fazer isso for x in [0-9]*; do if [ -f "$x" ]; then …porque pode haver um arquivo chamado [0-9]foo).


2

Com base em um script de contagem , a resposta de Shawn e um truque do Bash para garantir que mesmo nomes de arquivos com novas linhas sejam impressos em um formulário utilizável em uma única linha:

for f in *
do
    if [ -d "./$f" ]
    then
        printf %q "$f"
        printf %s ' '
        find "$f" -maxdepth 1 -printf x | wc -c
    fi
done

printf %qé imprimir uma versão entre aspas de uma string, ou seja, uma string de linha única que você pode colocar em um script Bash para ser interpretada como uma string literal, incluindo (potencialmente) novas linhas e outros caracteres especiais. Por exemplo, veja echo -n $'\tfoo\nbar'vs printf %q $'\tfoo\nbar'.

O findcomando funciona simplesmente imprimindo um único caractere para cada arquivo e depois contando-os em vez de contar as linhas.


1

Aqui está uma "força-bruta" maneira -ish para obter o seu resultado, usando find, echo, ls, wc, xargse awk.

find . -maxdepth 1 -type d -exec sh -c "echo '{}'; ls -1 '{}' | wc -l" \; | xargs -n 2 | awk '{print $1" "$2}'

Este trabalho. Mas a saída foi alterada se você tiver dirs com `` espaço no nome.
ShyBoy 23/10

Isso informará resultados incorretos se algum nome de arquivo tiver um caractere de nova linha.
Shawn J. Goff

-1
for i in *; do echo $i; ls $i | wc -l; done

4
Bem-vindo ao U&L. As respostas devem ser longas, com explicações e não simplesmente com a queda de código. Por favor, expanda isso e explique o que está acontecendo. Além disso, essa é uma maneira muito ineficiente de fazer isso e não manipula arquivos com espaços, por exemplo.
slm

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.