Dividir directorio grande en subdirectorios

Tengo un directorio con aproximadamente 2.5 millones de archivos y tiene más de 70 GB.

Quiero dividir esto en subdirectorios, cada uno con 1000 archivos en ellos.

Aquí está el comando que he intentado usar:

i=0; for f in *; do d=dir_$(printf %03d $((i/1000+1))); mkdir -p $d; mv "$f" $d; let i++; done 

Ese comando me funciona a pequeña escala, pero puedo dejarlo funcionando durante horas en este directorio y no parece que haga nada.

Estoy abierto a hacer esto de cualquier manera a través de la línea de comandos: perl, python, etc. De cualquier manera sería la más rápida para hacer esto …

Sospecho que si lo verificaste, notaste que tu progtwig estaba moviendo los archivos, aunque muy lentamente. Lanzar un progtwig es bastante caro (al menos en comparación con hacer una llamada al sistema), ¡y lo haces tres o cuatro veces por archivo! Como tal, lo siguiente debería ser mucho más rápido:

 perl -e' my $base_dir_qfn = "."; my $i = 0; my $dir; opendir(my $dh, $base_dir_qfn) or die("Can'\''t open dir \"$base_dir_qfn\": $!\n"); while (defined( my $fn = readdir($dh) )) { next if $fn =~ /^(?:\.\.?|dir_\d+)\z/; my $qfn = "$base_dir_qfn/$fn"; if ($i % 1000 == 0) { $dir_qfn = sprintf("%s/dir_%03d", $base_dir_qfn, int($i/1000)+1); mkdir($dir_qfn) or die("Can'\''t make directory \"$dir_qfn\": $!\n"); } rename($qfn, "$dir_qfn/$fn") or do { warn("Can'\''t move \"$qfn\" into \"$dir_qfn\": $!\n"); next; }; ++$i; } ' 

Nota: la respuesta útil de ikegami basada en Perl es la forma de proceder : realiza toda la operación en un solo proceso y, por lo tanto, es mucho más rápida que la solución de utilidades estándar de Bash + a continuación.


Una solución basada en bash debe evitar los bucles en los que se llama a las utilidades externas para que se realicen de manera razonable.
Su propia solución llama a dos utilidades externas y crea una subshell en cada iteración de bucle, lo que significa que terminará creando aproximadamente 7.5 millones de procesos (!) En total.

La siguiente solución evita los bucles, pero, dada la gran cantidad de archivos de entrada, aún tardará bastante en completarse (terminará creando 4 procesos por cada 1000 archivos de entrada, es decir, aproximadamente 10.000 procesos):

 printf '%s\0' * | xargs -0 -n 1000 bash -O nullglob -c ' dirs=( dir_*/ ) dir=dir_$(printf %04s $(( 1 + ${#dirs[@]} ))) mkdir "$dir"; mv "$@" "$dir"' - 
  • printf '%s\0' * imprime una lista separada por NUL de todos los archivos en el directorio.
    • Tenga en cuenta que dado que printf es un Bash incorporado en lugar de una utilidad externa, el máximo. la longitud de la línea de comando según lo informado por getconf ARG_MAX no se aplica.
  • xargs -0 -n 1000 invoca el comando especificado con fragmentos de 1000 nombres de archivos de entrada.

    • Tenga en cuenta que xargs -0 no es estándar, pero es compatible con Linux y BSD / OSX.
    • El uso de entradas separadas por NUL pasa de forma robusta a los nombres de archivo sin temor a dividirlos en varias partes, e incluso funciona con nombres de archivos con nuevas líneas incrustadas (aunque esos nombres de archivos son muy raros).
  • bash -O nullglob -c ejecuta la cadena de comando especificada con la opción nullglob activada, lo que significa que un patrón de agrupación que no coincide con nada se expandirá a la cadena vacía.

    • La cadena de comandos cuenta los directorios de salida creados hasta el momento, para determinar el nombre del siguiente directorio de salida con el siguiente índice más alto, crea el siguiente directorio de salida y mueve el lote actual de (hasta) 1000 archivos allí.

Si el directorio no está en uso, sugiero lo siguiente

 find . -maxdepth 1 -type f | split -l 1000 -d -a 5 

esto creará un número n de archivos llamados x00000 – x02500 (solo para asegurarse de que 5 dígitos, aunque 4 funcionarán también). Luego puede mover los 1000 archivos listados en cada archivo a un directorio correspondiente.

quizás set -o noclobber para eliminar el riesgo de anulaciones en caso de conflicto de nombres.

para mover los archivos, es más fácil usar la expansión de llaves para iterar sobre los nombres de archivos

 for c in x{00000..02500}; do d="d$c"; mkdir $d; cat $c | xargs -I f mv f $d; done 

Probablemente sea más lento que un progtwig Perl (1 minuto para 10.000 archivos) pero debería funcionar con cualquier shell compatible con POSIX.

 #! /bin/sh nd=0 nf=0 /bin/ls | \ while read file; do case $(expr $nf % 10) in 0) nd=$(/usr/bin/expr $nd + 1) dir=$(printf "dir_%04d" $nd) mkdir $dir ;; esac mv "$file" "$dir/$file" nf=$(/usr/bin/expr $nf + 1) 

hecho

Con bash, puedes usar la expansión aritmética $ ((…)).

Y, por supuesto, esta idea puede mejorarse mediante el uso de xargs: no debe tomar más de ~ 45 segundos para 2.5 millones de archivos.

 nd=0 ls | xargs -L 1000 echo | \ while read cmd; do nd=$((nd+1)) dir=$(printf "dir_%04d" $nd) mkdir $dir mv $cmd $dir done 

Yo usaría lo siguiente desde la línea de comando:

 find . -maxdepth 1 -type f |split -l 1000 for i in `ls x*` do mkdir dir$i mv `cat $i` dir$i& 2>/dev/null done 

La clave es el “&” que subraya cada statement mv.

Gracias a Karakfa por la idea dividida.