¿Hay un comando bash que cuente los archivos?

182

¿Hay un comando bash que cuente la cantidad de archivos que coinciden con un patrón?

Por ejemplo, quiero obtener el recuento de todos los archivos en un directorio que coincida con este patrón: log*

hudi
fuente

Respuestas:

243

Este simple one-liner debería funcionar en cualquier shell, no solo bash:

ls -1q log* | wc -l

ls -1q le dará una línea por archivo, incluso si contienen espacios en blanco o caracteres especiales como líneas nuevas.

La salida se canaliza a wc -l, que cuenta el número de líneas.

Daniel
fuente
10
No lo usaría -l, ya que eso requiere stat(2)en cada archivo y con el propósito de contar no agrega nada.
camh
12
No lo usaría ls, ya que crea un proceso hijo. log*es expandido por el shell, no ls, por echolo que sería simple .
cdarke
2
Excepto que un eco no funcionará si tiene nombres de archivo con espacios o caracteres especiales.
Daniel
44
@WalterTross Eso es cierto (no es que la eficiencia fuera un requisito de la pregunta original). También descubrí que -q se encarga de los archivos con líneas nuevas, incluso cuando la salida no es el terminal. Y estas banderas son compatibles con todas las plataformas y shells en las que he probado. Actualizando la respuesta, ¡gracias a ti y a camh por la entrada!
Daniel
3
Si hay un directorio llamado logsen el directorio en cuestión, también se contará el contenido de ese directorio de registros. Esto probablemente no sea intencional.
mogsie
54

Puede hacer esto de manera segura (es decir, no será molestado por archivos con espacios o \nen su nombre) con bash:

$ shopt -s nullglob
$ logfiles=(*.log)
$ echo ${#logfiles[@]}

Debe habilitar nullglobpara que no obtenga el literal *.logen la $logfiles matriz si no coinciden los archivos. (Consulte Cómo "deshacer" un 'set -x'? Para ver ejemplos de cómo restablecerlo de manera segura).

Estera
fuente
2
Quizás explícitamente señale que esta es una respuesta exclusiva de Bash , especialmente para los visitantes nuevos que aún no están enteramente al día en la diferencia entre sh y bash
tripleee
Además, la final se shopt -u nullglobdebe omitir si nullglobno se ha desarmado, entonces comenzó.
tripleee
Nota: Reemplazar *.logcon solo *contará directorios. Si los archivos que desea enumerar tienen la convención de nomenclatura tradicional name.extension, use *.*.
AlainD
52

Muchas respuestas aquí, pero algunas no tienen en cuenta

  • nombres de archivo con espacios, líneas nuevas o caracteres de control en ellos
  • nombres de archivo que comienzan con guiones (imagine un archivo llamado -l)
  • archivos ocultos, que comienzan con un punto (si el globo era en *.loglugar delog*
  • directorios que coinciden con el glob (por ejemplo, un directorio llamado logsque coincide log*)
  • directorios vacíos (es decir, el resultado es 0)
  • directorios extremadamente grandes (enumerarlos todos podría agotar la memoria)

Aquí hay una solución que los maneja a todos:

ls 2>/dev/null -Ubad1 -- log* | wc -l

Explicación:

  • -Uhace lsque no se ordenen las entradas, lo que significa que no necesita cargar todo el listado de directorios en la memoria
  • -bimprime escapes de estilo C para caracteres no gráficos, lo que hace que las nuevas líneas se impriman como \n.
  • -aimprime todos los archivos, incluso los archivos ocultos (no es estrictamente necesario cuando el glob log*implica que no hay archivos ocultos)
  • -dimprime directorios sin intentar enumerar el contenido del directorio, que es lo que lsnormalmente haría
  • -1 se asegura de que esté en una columna (ls hace esto automáticamente cuando escribe en una tubería, por lo que no es estrictamente necesario)
  • 2>/dev/nullredirige stderr para que si hay 0 archivos de registro, ignore el mensaje de error. (Tenga en cuenta que shopt -s nullglob, en su lugar, se lsenumeraría todo el directorio de trabajo).
  • wc -lconsume la lista de directorios a medida que se genera, por lo que la salida de lsnunca está en la memoria en ningún momento.
  • --Los nombres de archivo se separan del comando utilizando --para no ser entendidos como argumentos para ls(en caso de que log*se elimine)

El shell se expandirá log*a la lista completa de archivos, lo que puede agotar la memoria si se trata de muchos archivos, por lo que es mejor ejecutarlo a través de grep:

ls -Uba1 | grep ^log | wc -l

Este último maneja directorios de archivos extremadamente grandes sin usar mucha memoria (aunque sí usa una subshell). El -dya no es necesario, porque solo enumera el contenido del directorio actual.

mogsie
fuente
48

Para una búsqueda recursiva:

find . -type f -name '*.log' -printf x | wc -c

wc -ccontará el número de caracteres en la salida de find, mientras que -printf xle indica findque imprima un solo xpara cada resultado.

Para una búsqueda no recursiva, haga esto:

find . -maxdepth 1 -type f -name '*.log' -printf x | wc -c
Will Vousden
fuente
66
Incluso si usted no tiene archivos con espacios, algún otro usuario de su script podría encontrar un archivo llamado maliciosamente, haciendo que las secuencias de comandos fallen. Además, otras personas que se encuentran con esto en StackOverflow pueden tener archivos con nuevas líneas y necesitan conocer las trampas.
mogsie
Para su información, si simplemente deja de lado -name '*.log', contará todos los archivos, que es lo que necesitaba para mi caso de uso. También la bandera -maxdepth es extremadamente útil, ¡gracias!
starmandeluxe
2
Esto todavía produce resultados incorrectos si hay nombres de archivo con nuevas líneas en ellos. La solución es fácil con find; simplemente imprima algo más que el nombre de archivo literal.
tripleee
8

La respuesta aceptada para esta pregunta es incorrecta, pero tengo poca reputación, así que no puedo agregarle ningún comentario.

La respuesta correcta a esta pregunta la da Mat:

shopt -s nullglob
logfiles=(*.log)
echo ${#logfiles[@]}

El problema con la respuesta aceptada es que wc -l cuenta el número de caracteres de nueva línea y los cuenta incluso si se imprimen en el terminal como '?' en la salida de 'ls -l'. Esto significa que la respuesta aceptada FALLA cuando un nombre de archivo contiene un carácter de nueva línea. He probado el comando sugerido:

ls -l log* | wc -l

e informa erróneamente un valor de 2 incluso si solo hay 1 archivo que coincide con el patrón cuyo nombre contiene un carácter de nueva línea. Por ejemplo:

touch log$'\n'def
ls log* -l | wc -l
Dan Yard
fuente
6

Si tiene muchos archivos y no desea usar la shopt -s nullglobsolución de matriz elegante y bash, puede usar find y así sucesivamente siempre que no imprima el nombre del archivo (que puede contener nuevas líneas).

find -maxdepth 1 -name "log*" -not -name ".*" -printf '%i\n' | wc -l

Esto encontrará todos los archivos que coincidan con log * y que no comiencen con .*: "not name. *" Es redundante, pero es importante tener en cuenta que el valor predeterminado para "ls" es no mostrar archivos de puntos, pero el valor predeterminado para encontrar es incluirlos.

Esta es una respuesta correcta y maneja cualquier tipo de nombre de archivo que pueda lanzarle, porque el nombre de archivo nunca se pasa entre los comandos.

Pero, la shopt nullglobrespuesta es la mejor respuesta!

mogsie
fuente
Probablemente debería actualizar su respuesta original en lugar de responder nuevamente.
qodeninja
Creo que usar findvs usar lsson dos formas diferentes de resolver el problema. findno siempre está presente en una máquina, pero lsgeneralmente lo está,
mogsie
2
Pero luego, una caja de manteca de cerdo que findprobablemente no tiene tampoco tiene todas esas opciones elegantes ls.
tripleee
1
Observe también cómo esto se extiende a un árbol de directorios completo si saca el-maxdepth 1
tripleee
1
Tenga en cuenta que esta solución contará los archivos dentro de directorios ocultos en su recuento. findhace esto por defecto. Esto puede crear confusión si uno no se da cuenta de que hay una carpeta secundaria oculta, y puede ser ventajoso usarla lsen algunas circunstancias, que no informa los archivos ocultos de manera predeterminada.
MrPotatoHead
6

Aquí está mi única línea para esto.

 file_count=$( shopt -s nullglob ; set -- $directory_to_search_inside/* ; echo $#)
zee
fuente
Me tomó un poco de google para entender, ¡pero esto es bueno! Por set -- lo tanto, no está haciendo nada excepto prepararnos $#, que almacena la cantidad de argumentos de la línea de comandos que se pasaron al programa de shell
xverges
@xverges Sí, "shopt -s nullglob" es para no contar los archivos ocultos (.files). set - es para almacenar / configurar el número de parámetros posicionales (número de archivos, en este caso). y # $ para mostrar el número de parámetros posicionales (recuento de archivos).
zee
3

Puede usar la opción -R para buscar los archivos junto con los que están dentro de los directorios recursivos

ls -R | wc -l // to find all the files

ls -R | grep log | wc -l // to find the files which contains the word log

puedes usar patrones en grep

Moh .S
fuente
3

Un comentario importante

(no hay suficiente reputación para comentar)

Esto es BUGGY :

ls -1q some_pattern | wc -l

Si shopt -s nullglobse configura, imprime el número de TODOS los archivos regulares, no solo los que tienen el patrón (probado en CentOS-8 y Cygwin). ¿Quién sabe qué otros errores sin sentido lstiene?

Esto es CORRECTO y mucho más rápido:

shopt -s nullglob; files=(some_pattern); echo ${#files[@]};

Hace el trabajo esperado.


Y los tiempos de ejecución difieren.
El primero: 0.006en CentOS y 0.083en Cygwin (en caso de que se use con cuidado).
El segundo: 0.000en CentOS y 0.003en Cygwin.

Niño pequeño
fuente
2

Puede definir dicho comando fácilmente, utilizando una función de shell. Este método no requiere ningún programa externo y no genera ningún proceso secundario. No intenta el lsanálisis peligroso y maneja caracteres "especiales" (espacios en blanco, líneas nuevas, barras invertidas, etc.) muy bien. Solo se basa en el mecanismo de expansión de nombre de archivo proporcionado por el shell. Es compatible con al menos sh, bash y zsh.

La siguiente línea define una función llamada countque imprime el número de argumentos con los que se ha llamado.

count() { echo $#; }

Simplemente llámelo con el patrón deseado:

count log*

Para que el resultado sea correcto cuando el patrón global no tiene coincidencia, la opción de shell nullglob(o failglob, que es el comportamiento predeterminado en zsh) debe establecerse en el momento en que ocurre la expansión. Se puede configurar así:

shopt -s nullglob    # for sh / bash
setopt nullglob      # for zsh

Dependiendo de lo que quiera contar, también podría estar interesado en la opción de shell dotglob.

Desafortunadamente, con bash al menos, no es fácil establecer estas opciones localmente. Si no desea configurarlos globalmente, la solución más sencilla es utilizar la función de esta manera más complicada:

( shopt -s nullglob ; shopt -u failglob ; count log* )

Si desea recuperar la sintaxis ligera count log*, o si realmente desea evitar generar una subshell, puede hackear algo en la línea de:

# sh / bash:
# the alias is expanded before the globbing pattern, so we
# can set required options before the globbing gets expanded,
# and restore them afterwards.
count() {
    eval "$_count_saved_shopts"
    unset _count_saved_shopts
    echo $#
}
alias count='
    _count_saved_shopts="$(shopt -p nullglob failglob)"
    shopt -s nullglob
    shopt -u failglob
    count'

Como beneficio adicional, esta función es de uso más general. Por ejemplo:

count a* b*          # count files which match either a* or b*
count $(jobs -ps)    # count stopped jobs (sh / bash)

Al convertir la función en un archivo de script (o un programa C equivalente), invocable desde la RUTA, también se puede componer con programas como findy xargs:

find "$FIND_OPTIONS" -exec count {} \+    # count results of a search
Maëlan
fuente
2

He pensado mucho en esta respuesta, especialmente teniendo en cuenta las cosas de no analizar . Al principio intenté

<¡ADVERTENCIA! NO FUNCIONÓ>
du --inodes --files0-from=<(find . -maxdepth 1 -type f -print0) | awk '{sum+=int($1)}END{print sum}'
</ ¡ADVERTENCIA! NO FUNCIONÓ>

que funcionó si solo hubiera un nombre de archivo como

touch $'w\nlf.aa'

pero falló si hice un nombre de archivo como este

touch $'firstline\n3 and some other\n1\n2\texciting\n86stuff.jpg'

Finalmente se me ocurrió lo que estoy poniendo a continuación. Tenga en cuenta que estaba tratando de obtener un recuento de todos los archivos en el directorio (sin incluir ningún subdirectorio). Creo que, junto con las respuestas de @Mat y @Dan_Yard, además de tener al menos la mayoría de los requisitos establecidos por @mogsie (no estoy seguro de la memoria). Creo que la respuesta de @mogsie es correcta, pero siempre trato de evitar el análisis a lsmenos que sea una situación extremadamente específica.

awk -F"\0" '{print NF-1}' < <(find . -maxdepth 1 -type f -print0) | awk '{sum+=$1}END{print sum}'

Más legible:

awk -F"\0" '{print NF-1}' < \
  <(find . -maxdepth 1 -type f -print0) | \
    awk '{sum+=$1}END{print sum}'

Esto está haciendo una búsqueda específica para archivos, delimitando la salida con un carácter nulo (para evitar problemas con espacios y saltos de línea), luego contando el número de caracteres nulos. El número de archivos será uno menos que el número de caracteres nulos, ya que habrá un carácter nulo al final.

Para responder la pregunta del OP, hay dos casos a considerar

1) Búsqueda no recursiva:

awk -F"\0" '{print NF-1}' < \
  <(find . -maxdepth 1 -type f -name "log*" -print0) | \
    awk '{sum+=$1}END{print sum}'

2) Búsqueda recursiva. Tenga en cuenta que lo que hay dentro del -nameparámetro puede necesitar ser cambiado para un comportamiento ligeramente diferente (archivos ocultos, etc.).

awk -F"\0" '{print NF-1}' < \
  <(find . -type f -name "log*" -print0) | \
    awk '{sum+=$1}END{print sum}'

Si a alguien le gustaría comentar cómo se comparan estas respuestas con las que he mencionado en esta respuesta, por favor hágalo.


Tenga en cuenta que llegué a este proceso de pensamiento al obtener esta respuesta .

bballdave025
fuente
1

Esto es lo que siempre hago:

ls log * | awk 'END {print NR}'

Shuang Liang
fuente
awk 'END{print NR}'debe ser equivalente a wc -l.
musiphil
0
ls -1 log* | wc -l

Lo que significa listar un archivo por línea y luego canalizarlo al comando de recuento de palabras con cambio de parámetro a líneas de recuento.

nudzo
fuente
La opción "-1" no es necesaria cuando se canaliza la salida ls. Pero es posible que desee ocultar el mensaje de error ls si ningún archivo coincide con el patrón. Sugiero "ls log * 2> / dev / null | wc -l".
JohnMudd
La discusión bajo la respuesta de Daniel también es relevante aquí. Esto funciona bien cuando no tiene directorios coincidentes o nombres de archivos con nuevas líneas, pero una buena respuesta debería al menos señalar estas condiciones de contorno, y una gran respuesta no debería tenerlas. Muchos errores se deben a que alguien copió / pegó el código que no entendió; por lo tanto, señalar las fallas al menos les ayuda a entender a qué prestar atención. (Por supuesto, muchos más errores ocurren porque ellos ignoraron las advertencias y luego las cosas cambiaron después de que pensaban que el código era probablemente lo suficientemente bueno para su propósito.)
tripleee
-1

Para contar todo simplemente canalice ls a la línea de conteo de palabras:

ls | wc -l

Para contar con el patrón, canalice a grep primero:

ls | grep log | wc -l
jturi
fuente