¿Cómo puedo contar archivos con una extensión particular y los directorios en los que se encuentran?

14

Quiero saber cuántos archivos regulares tienen la extensión .cen una estructura de directorio grande y compleja, y también cuántos directorios se distribuyen entre estos archivos. La salida que quiero es solo esos dos números.

He visto esta pregunta sobre cómo obtener la cantidad de archivos, pero también necesito saber la cantidad de directorios en los que están los archivos.

  • Mis nombres de archivo (incluidos los directorios) pueden tener caracteres; pueden comenzar con .o -y tener espacios o líneas nuevas.
  • Podría tener algunos enlaces simbólicos cuyos nombres terminan en .c, y enlaces simbólicos a directorios. No quiero que se sigan o cuenten los enlaces simbólicos, o al menos quiero saber si se están contando y cuándo.
  • La estructura de directorios tiene muchos niveles y el directorio de nivel superior (el directorio de trabajo) tiene al menos un .carchivo.

Escribí rápidamente algunos comandos en el shell (Bash) para contarlos yo mismo, pero no creo que el resultado sea exacto ...

shopt -s dotglob
shopt -s globstar
mkdir out
for d in **/; do
     find "$d" -maxdepth 1 -type f -name "*.c" >> out/$(basename "$d")
done
ls -1Aq out | wc -l
cat out/* | wc -l

Esto genera quejas sobre redirecciones ambiguas, pierde archivos en el directorio actual y se tropeza con caracteres especiales (por ejemplo, la salida redirigida findimprime nuevas líneas en los nombres de archivo ) y escribe un montón de archivos vacíos (Uy).

¿Cómo puedo enumerar de manera confiable mis .carchivos y sus directorios que contienen?


En caso de que ayude, aquí hay algunos comandos para crear una estructura de prueba con nombres incorrectos y enlaces simbólicos:

mkdir -p cfiles/{1..3}/{a..b} && cd cfiles
mkdir space\ d
touch -- i.c -.c bad\ .c 'terrible
.c' not-c .hidden.c
for d in space\ d 1 2 2/{a..b} 3/b; do cp -t "$d" -- *.c; done
ln -s 2 dirlink
ln -s 3/b/i.c filelink.c

En la estructura resultante, 7 directorios contienen .carchivos y 29 archivos regulares terminan con .c(si dotglobestá desactivado cuando se ejecutan los comandos) (si he contado mal, hágamelo saber). Estos son los números que quiero.

Por favor, siéntase libre de no usar esta prueba en particular.

NB: Las respuestas en cualquier shell u otro idioma serán probadas y apreciadas por mí. Si tengo que instalar nuevos paquetes, no hay problema. Si conoces una solución GUI, te animo a compartir (pero podría no ir tan lejos como para instalar un DE completo para probarlo) :) Uso Ubuntu MATE 17.10.

Zanna
fuente
Escribir un programa para lidiar con malos hábitos de programación resultó ser bastante desafiante;)
WinEunuuchs2Unix

Respuestas:

16

No he examinado la salida con enlaces simbólicos pero:

find . -type f -iname '*.c' -printf '%h\0' |
  sort -z |
  uniq -zc |
  sed -zr 's/([0-9]) .*/\1 1/' |
  tr '\0' '\n' |
  awk '{f += $1; d += $2} END {print f, d}'
  • El findcomando imprime el nombre del directorio de cada .carchivo que encuentra.
  • sort | uniq -cnos dará cuántos archivos hay en cada directorio ( sortpuede ser innecesario aquí, no estoy seguro)
  • con sed, reemplazo el nombre del directorio con 1, eliminando así todos los posibles caracteres extraños, solo con el recuento y el 1resto
  • permitiéndome convertir a salida separada de nueva línea con tr
  • que luego resumo con awk, para obtener la cantidad total de archivos y la cantidad de directorios que contenían esos archivos. Tenga en cuenta que daquí es esencialmente lo mismo que NR. Podría haber omitido la inserción 1en el sedcomando e imprimirlo NRaquí, pero creo que esto es un poco más claro.

Hasta el momento tr, los datos están delimitados por NUL, a salvo de todos los nombres de archivo válidos.


Con zsh y bash, puede usar printf %qpara obtener una cadena entre comillas, que no tendría nuevas líneas. Entonces, podrías hacer algo como:

shopt -s globstar dotglob nocaseglob
printf "%q\n" **/*.c | awk -F/ '{NF--; f++} !c[$0]++{d++} END {print f, d}'

Sin embargo, aunque **no se supone que se expanda para enlaces simbólicos a directorios , no pude obtener el resultado deseado en bash 4.4.18 (1) (Ubuntu 16.04).

$ shopt -s globstar dotglob nocaseglob
$ printf "%q\n" ./**/*.c | awk -F/ '{NF--; f++} !c[$0]++{d++} END {print f, d}'
34 15
$ echo $BASH_VERSION
4.4.18(1)-release

Pero zsh funcionó bien, y el comando se puede simplificar:

$ printf "%q\n" ./**/*.c(D.:h) | awk '!c[$0]++ {d++} END {print NR, d}'
29 7

Dhabilita este globo para seleccionar archivos de puntos, .selecciona archivos regulares (por lo tanto, no enlaces simbólicos) e :himprime solo la ruta del directorio y no el nombre del archivo (como find's' %h) (Ver secciones sobre Generación y modificadores de nombre de archivo ). Entonces, con el comando awk solo necesitamos contar el número de directorios únicos que aparecen, y el número de líneas es el recuento de archivos.

muru
fuente
Eso es genial. Utiliza exactamente lo que se necesita y no más. Gracias por enseñar :)
Zanna
@Zanna si publica algunos comandos para recrear una estructura de directorio con enlaces simbólicos, y la salida esperada con enlaces simbólicos, podría corregir esto en consecuencia.
muru
He agregado algunos comandos para hacer una estructura de prueba (innecesariamente complicada como de costumbre) con enlaces simbólicos.
Zanna
@ Zanna Creo que este comando no necesita ningún ajuste para obtener 29 7. Si agrego -La find, eso va hasta 41 10. ¿Qué salida necesitas?
muru
1
Se agregó un método zsh + awk. Probablemente haya alguna forma de que zsh mismo imprima el conteo para mí, pero no tengo idea de cómo.
muru
11

Python tiene os.walk, lo que hace que tareas como esta sean fáciles, intuitivas y automáticamente robustas incluso frente a nombres de archivos extraños como los que contienen caracteres de nueva línea. Esta secuencia de comandos de Python 3, que había publicado originalmente en el chat , está pensado para ejecutarse en el directorio actual (pero que no tiene que estar ubicado en el directorio actual, y se puede cambiar cuál es el camino que pasa a os.walk):

#!/usr/bin/env python3

import os

dc = fc = 0
for _, _, fs in os.walk('.'):
    c = sum(f.endswith('.c') for f in fs)
    if c:
        dc += 1
        fc += c
print(dc, fc)

Eso imprime el recuento de directorios que contienen directamente al menos un archivo cuyo nombre termina en .c, seguido de un espacio, seguido del recuento de archivos cuyos nombres terminan en .c. Los archivos "ocultos", es decir, los archivos cuyos nombres comienzan con ., se incluyen, y los directorios ocultos se recorren de manera similar.

os.walkrecorre recursivamente una jerarquía de directorios. Enumera todos los directorios a los que se puede acceder de forma recursiva desde el punto de partida, y proporciona información sobre cada uno de ellos como una tupla de tres valores root, dirs, files. Para cada directorio al que atraviesa (incluido el primero cuyo nombre le da):

  • rootcontiene el nombre de ruta de ese directorio. Tenga en cuenta que esto es totalmente ajeno a "directorio raíz" del sistema /(y también sin relación con /root) a pesar de que iba a ir a aquellos si se inicia allí. En este caso, rootcomienza en la ruta ., es decir, el directorio actual, y va a todas partes debajo de él.
  • dirscontiene una lista de las rutas de todos los subdirectorios del directorio cuyo nombre se encuentra actualmente en root.
  • filescontiene una lista de las rutas de todos los archivos que residen en el directorio cuyo nombre se encuentra actualmente rootpero que no son directorios en sí mismos. Tenga en cuenta que esto incluye otros tipos de archivos que no son archivos normales, incluidos enlaces simbólicos, pero parece que no espera que tales entradas terminen .cy esté interesado en ver alguna que sí lo haga.

En este caso, solo necesito examinar el tercer elemento de la tupla files(que llamo fsen el script). Al igual que el findcomando, Python os.walkatraviesa subdirectorios para mí; Lo único que tengo que inspeccionar es el nombre de los archivos que contiene cada uno de ellos. Sin findembargo, a diferencia del comando, os.walkautomáticamente me proporciona una lista de esos nombres de archivo.

Ese guión no sigue enlaces simbólicos. Es muy probable que no desee que se sigan los enlaces simbólicos para una operación de este tipo, ya que podrían formar ciclos, y porque incluso si no hay ciclos, los mismos archivos y directorios pueden atravesarse y contarse varias veces si son accesibles a través de diferentes enlaces simbólicos.

Si alguna vez quisiste os.walkseguir enlaces simbólicos, lo que normalmente no harías, entonces puedes pasar followlinks=truea él. Es decir, en lugar de escribir os.walk('.'), podrías escribir os.walk('.', followlinks=true). Reitero que rara vez querrá eso, especialmente para una tarea como esta en la que enumera recursivamente una estructura de directorio completa, sin importar cuán grande sea, y cuenta todos los archivos que cumplen algún requisito.

Eliah Kagan
fuente
7

Encuentra + Perl:

$ find . -type f -iname '*.c' -printf '%h\0' | 
    perl -0 -ne '$k{$_}++; }{ print scalar keys %k, " $.\n" '
7 29

Explicación

El findcomando encontrará los archivos normales (por lo que no hay enlaces simbólicos o directorios) y luego imprimirá el nombre del directorio en el que están ( %h) seguido de \0.

  • perl -0 -ne: lea la entrada línea por línea ( -n) y aplique el script dado por -ea cada línea. La -0fija el separador de línea de entrada a \0lo que podemos leer la entrada nula delimitado.
  • $k{$_}++: $_es una variable especial que toma el valor de la línea actual. Esto se usa como una clave para el hash %k , cuyos valores son el número de veces que se vio cada línea de entrada (nombre del directorio).
  • }{: esta es una forma abreviada de escribir END{}. Cualquier comando después del }{se ejecutará una vez, después de que se haya procesado toda la entrada.
  • print scalar keys %k, " $.\n": keys %k devuelve una matriz de claves en el hash %k. scalar keys %kda la cantidad de elementos en esa matriz, la cantidad de directorios vistos. Esto se imprime junto con el valor actual de $., una variable especial que contiene el número de línea de entrada actual. Como esto se ejecuta al final, el número de línea de entrada actual será el número de la última línea, por lo tanto, el número de líneas vistas hasta ahora.

Puede ampliar el comando perl a esto, para mayor claridad:

find  . -type f -iname '*.c' -printf '%h\0' | 
    perl -0 -e 'while($line = <STDIN>){
                    $dirs{$line}++; 
                    $tot++;
                } 
                $count = scalar keys %dirs; 
                print "$count $tot\n" '
terdon
fuente
4

Aquí está mi sugerencia:

#!/bin/bash
tempfile=$(mktemp)
find -type f -name "*.c" -prune >$tempfile
grep -c / $tempfile
sed 's_[^/]*$__' $tempfile | sort -u | grep -c /

Este breve script crea un archivo temporal, encuentra todos los archivos dentro y debajo del directorio actual que termina en .cy escribe la lista en el archivo temporal. grepluego se usa para contar los archivos (siguiendo ¿Cómo puedo obtener un recuento de archivos en un directorio usando la línea de comando? ) dos veces: La segunda vez, los directorios que se enumeran varias veces se eliminan usandosort -u después de quitar los nombres de archivo de cada línea usando sed.

Esto también funciona correctamente con las nuevas líneas en los nombres de archivo: grep -c / cuenta solo las líneas con una barra inclinada y, por lo tanto, solo considera la primera línea de un nombre de archivo de varias líneas en la lista.

Salida

$ tree
.
├── 1
   ├── 1
      ├── test2.c
      └── test.c
   └── 2
       └── test.c
└── 2
    ├── 1
       └── test.c
    └── 2

$ tempfile=$(mktemp);find -type f -name "*.c" -prune >$tempfile;grep -c / $tempfile;sed 's_[^/]*$__' $tempfile | sort -u | grep -c /
4
3
postre
fuente
4

Pequeño shellscript

Sugiero un pequeño shellscript de bash con dos líneas de comando principales (y una variable filetypepara facilitar el cambio para buscar otros tipos de archivos).

No busca ni en enlaces simbólicos, solo archivos regulares.

#!/bin/bash

filetype=c
#filetype=pdf

# count the 'filetype' files

find -type f -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l | tr '\n' ' '

# count directories containing 'filetype' files

find -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)'" \;|grep 'contains file(s)$'|wc -l

Shellscript detallado

Esta es una versión más detallada que también considera enlaces simbólicos,

#!/bin/bash

filetype=c
#filetype=pdf

# counting the 'filetype' files

echo -n "number of $filetype files in the current directory tree: "
find -type f -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l

echo -n "number of $filetype symbolic links in the current directory tree: "
find -type l -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l
echo -n "number of $filetype normal files in the current directory tree: "
find -type f -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l
echo -n "number of $filetype symbolic links in the current directory tree including linked directories: "
find -L -type f -name "*.$filetype" -ls 2> /tmp/c-counter |sed 's#.* \./##' | wc -l; cat /tmp/c-counter; rm /tmp/c-counter

# list directories with and without 'filetype' files (good for manual checking; comment away after test)
echo '---------- list directories:'
 find    -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)' || echo '{} empty'" \;
echo ''
#find -L -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)' || echo '{} empty'" \;

# count directories containing 'filetype' files

echo -n "number of directories with $filetype files: "
find -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)'" \;|grep 'contains file(s)$'|wc -l

# list and count directories including symbolic links, containing 'filetype' files
echo '---------- list all directories including symbolic links:'
find -L -type d -exec bash -c "ls -AF '{}' |grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)' || echo '{} empty'" \;
echo ''
echo -n "number of directories (including symbolic links) with $filetype files: "
find -L -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)'" \; 2>/dev/null |grep 'contains file(s)$'|wc -l

# count directories without 'filetype' files (good for checking; comment away after test)

echo -n "number of directories without $filetype files: "
find -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null || echo '{} empty'" \;|grep 'empty$'|wc -l

Prueba de salida

Del breve shellscript:

$ ./ccntr 
29 7

Del shellscript detallado:

$ LANG=C ./c-counter
number of c files in the current directory tree: 29
number of c symbolic links in the current directory tree: 1
number of c normal files in the current directory tree: 29
number of c symbolic links in the current directory tree including linked directories: 42
find: './cfiles/2/2': Too many levels of symbolic links
find: './cfiles/dirlink/2': Too many levels of symbolic links
---------- list directories:
. empty
./cfiles contains file(s)
./cfiles/2 contains file(s)
./cfiles/2/b contains file(s)
./cfiles/2/a contains file(s)
./cfiles/3 empty
./cfiles/3/b contains file(s)
./cfiles/3/a empty
./cfiles/1 contains file(s)
./cfiles/1/b empty
./cfiles/1/a empty
./cfiles/space d contains file(s)

number of directories with c files: 7
---------- list all directories including symbolic links:
. empty
./cfiles contains file(s)
./cfiles/2 contains file(s)
find: './cfiles/2/2': Too many levels of symbolic links
./cfiles/2/b contains file(s)
./cfiles/2/a contains file(s)
./cfiles/3 empty
./cfiles/3/b contains file(s)
./cfiles/3/a empty
./cfiles/dirlink empty
find: './cfiles/dirlink/2': Too many levels of symbolic links
./cfiles/dirlink/b contains file(s)
./cfiles/dirlink/a contains file(s)
./cfiles/1 contains file(s)
./cfiles/1/b empty
./cfiles/1/a empty
./cfiles/space d contains file(s)

number of directories (including symbolic links) with c files: 9
number of directories without c files: 5
$ 
sudodus
fuente
4

Perl One liner simple:

perl -MFile::Find=find -le'find(sub{/\.c\z/ and -f and $c{$File::Find::dir}=++$c}, @ARGV); print 0 + keys %c, " $c"' dir1 dir2

O más simple con findcomando:

find dir1 dir2 -type f -name '*.c' -printf '%h\0' | perl -l -0ne'$c{$_}=1}{print 0 + keys %c, " $."'

Si te gusta el golf y tienes Perl reciente (como de menos de una década):

perl -MFile::Find=find -E'find(sub{/\.c$/&&-f&&($c{$File::Find::dir}=++$c)},".");say 0+keys%c," $c"'
find -type f -name '*.c' -printf '%h\0'|perl -0nE'$c{$_}=1}{say 0+keys%c," $."'
Hynek -Pichi- Vychodil
fuente
2

Considere usar el locatecomando que es mucho más rápido que el findcomando.

Ejecutando en datos de prueba

$ sudo updatedb # necessary if files in focus were added `cron` daily.
$ printf "Number Files: " && locate -0r "$PWD.*\.c$" | xargs -0 -I{} sh -c 'test ! -L "$1" && echo "regular file"' _  {} | wc -l &&  printf "Number Dirs.: " && locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -cu | wc -l
Number Files: 29
Number Dirs.: 7

Gracias a Muru por su respuesta para ayudarme a eliminar enlaces simbólicos del conteo de archivos en la respuesta de Unix y Linux .

Gracias a Terdon por su respuesta de $PWD(no dirigida a mí) en la respuesta de Unix y Linux .


Respuesta original a continuación referenciada por comentarios

Forma corta:

$ cd /
$ sudo updatedb
$ printf "Number Files: " && locate -cr "$PWD.*\.c$"
Number Files: 3523
$ printf "Number Dirs.: " && locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l 
Number Dirs.: 648
  • sudo updatedbActualice la base de datos utilizada por el locatecomando si los .carchivos se crearon hoy o si ha eliminado .carchivos hoy.
  • locate -cr "$PWD.*\.c$"ubica todos los .carchivos en el directorio actual y son hijos ( $PWD). En lugar de imprimir nombres de archivos e imprimir, contar con -cargumento. Los rEspecifica expresión regular en lugar de defecto *pattern*a juego que puede producir demasiados resultados.
  • locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l. Ubique todos los *.carchivos en el directorio actual y a continuación. Elimine el nombre del archivo seddejando solo el nombre del directorio. Cuente el número de archivos en cada directorio usando uniq -c. Contar el número de directorios con wc -l.

Comience en el directorio actual con one-liner

$ cd /usr/src
$ printf "Number Files: " && locate -cr "$PWD.*\.c$" &&  printf "Number Dirs.: " && locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l
Number Files: 3430
Number Dirs.: 624

Observe cómo han cambiado el recuento de archivos y el recuento de directorios. Creo que todos los usuarios tienen el /usr/srcdirectorio y pueden ejecutar los comandos anteriores con diferentes recuentos dependiendo del número de núcleos instalados.

Forma larga:

La forma larga incluye el tiempo para que pueda ver cuánto más rápido locateha terminado find. Incluso si tiene que correr sudo updatedb, es muchas veces más rápido que uno solo find /.

───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ sudo time updatedb
0.58user 1.32system 0:03.94elapsed 48%CPU (0avgtext+0avgdata 7568maxresident)k
48inputs+131920outputs (1major+3562minor)pagefaults 0swaps
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ time (printf "Number Files: " && locate -cr $PWD".*\.c$")
Number Files: 3523

real    0m0.775s
user    0m0.766s
sys     0m0.012s
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ time (printf "Number Dirs.: " && locate -r $PWD".*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l) 
Number Dirs.: 648

real    0m0.778s
user    0m0.788s
sys     0m0.027s
───────────────────────────────────────────────────────────────────────────────────────────

Nota: Estos son todos los archivos en TODAS las unidades y particiones. es decir, también podemos buscar comandos de Windows:

$ time (printf "Number Files: " && locate *.exe -c)
Number Files: 6541

real    0m0.946s
user    0m0.761s
sys     0m0.060s
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ time (printf "Number Dirs.: " && locate *.exe | sed 's%/[^/]*$%/%' | uniq -c | wc -l) 
Number Dirs.: 3394

real    0m0.942s
user    0m0.803s
sys     0m0.092s

Tengo tres particiones NTFS de Windows 10 montadas automáticamente en /etc/fstab . ¡Ten en cuenta que localizar lo sabe todo!

Cuenta interesante:

$ time (printf "Number Files: " && locate / -c &&  printf "Number Dirs.: " && locate / | sed 's%/[^/]*$%/%' | uniq -c | wc -l)
Number Files: 1637135
Number Dirs.: 286705

real    0m15.460s
user    0m13.471s
sys     0m2.786s

Lleva 15 segundos contar 1,637,135 archivos en 286,705 directorios. YMMV.

Para obtener un desglose detallado sobre locateel manejo de expresiones regulares del comando (parece que no es necesario en estas preguntas y respuestas, pero se usa por si acaso), lea esto: ¿ Use "localizar" en algún directorio específico?

Lectura adicional de artículos recientes:

WinEunuuchs2Unix
fuente
1
Esto no cuenta los archivos en un directorio específico. Como señala, cuenta todos los archivos (o directorios, o cualquier otro tipo de archivo) coincidentes .c(tenga en cuenta que se romperá si hay un archivo nombrado -.cen el directorio actual ya que no está citando *.c) y luego imprimirá todos los directorios en el sistema, independientemente de si contienen archivos .c.
terdon
@terdon Puede pasar un directorio ~/my_c_progs/*.c. Está contando 638 directorios con .cprogramas, el total de directorios se muestra más tarde como 286,705. Revisaré la respuesta a la comilla doble "* .c". Gracias por el consejo.
WinEunuuchs2Unix
3
Sí, puede usar algo como locate -r "/path/to/dir/.*\.c$", pero eso no se menciona en ninguna parte de su respuesta. Solo da un enlace a otra respuesta que menciona esto pero sin explicación de cómo adaptarlo para responder la pregunta que se hace aquí. Toda su respuesta se centra en cómo contar la cantidad total de archivos y directorios en el sistema, lo cual no es relevante para la pregunta que se hizo, ¿cómo puedo contar la cantidad de archivos .c y la cantidad de directorios que contienen? archivos c en un directorio específico ". Además, sus números están equivocados, pruébelo en el ejemplo en el OP.
terdon
@terdon Gracias por tu aporte. He mejorado la respuesta con sus sugerencias y una respuesta que publicó en otro sitio SE para la $PWDvariable: unix.stackexchange.com/a/188191/200094
WinEunuuchs2Unix
1
Ahora debe asegurarse de que $PWDno contenga caracteres que tal vez sean especiales en una expresión regular
muru