Ordenar un conjunto de nombres de ruta de archivos por sus nombres básicos

8

Supongamos que tengo una lista de rutas de archivos almacenados en una matriz

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" ) 

Quiero ordenar los elementos en la matriz de acuerdo con los nombres básicos de los nombres de archivo, en orden numérico

sortedfilearray=("dir2/0003.pdf" "dir1/0010.pdf" "dir3/0040.pdf") 

¿Cómo puedo hacer eso?

Solo puedo ordenar sus partes de nombre base:

basenames=()
for file in "${filearray[@]}"
do
    filename=${file##*/}
    basenames+=(${filename%.*})
done
sortedbasenamearr=($(printf '%s\n' "${basenames[@]}" | sort -n))

estoy pensando sobre

  • crear una matriz asociativa cuyas claves son los nombres de base y los valores son los nombres de ruta, por lo que el acceso a los nombres de ruta siempre se realiza a través de nombres de base.
  • crear otra matriz solo para nombres básicos y aplicar sorta la matriz de nombres básicos.

Gracias.

Tim
fuente
1
No es una buena idea, pero puede ordenar en bash
Jeff Schaller
Tenga cuidado con una matriz en los nombres básicos, si pudiera tener dir1 / 42.pdf y dir2 / 42.pdf
Jeff Schaller
Eso (diferentes nombres de ruta con el mismo nombre base) no sucede en mi caso. Pero si un script bash puede manejarlo, será genial. No tengo requisitos razonablemente buenos sobre cómo ordenar los nombres de ruta con el mismo nombre base, tal vez alguien más pueda. dir1 dir2están inventados y en realidad son nombres de ruta arbitrarios.
Tim

Respuestas:

4

Al contrario de ksh o zsh, bash no tiene soporte incorporado para ordenar matrices o listas de cadenas arbitrarias. Puede ordenar los globos o la salida de aliasor seto typeset(aunque los últimos 3 no están en el orden de clasificación local del usuario), pero eso no se puede usar prácticamente aquí.

No hay nada en el cofre de herramientas POSIX que pueda ordenar fácilmente listas arbitrarias de cadenas tampoco ( sortordena líneas, por lo que solo las secuencias cortas de caracteres (LINE_MAX a menudo son más cortas que PATH_MAX) de caracteres que no sean NUL y nueva línea, mientras que las rutas de archivo son secuencias no vacías de bytes. que 0).

Entonces, si bien podría implementar su propio algoritmo de clasificación en awk(usando el <operador de comparación de cadenas) o inclusobash (usando [[ < ]]), para rutas arbitrarias en bash, de forma portátil, lo más fácil puede ser recurrir a perl:

Con bash4.4+, podrías hacer:

readarray -td '' sorted_filearray < <(perl -MFile::Basename -l0 -e '
  print for sort {basename($a) cmp basename($b)} @ARGV' -- "${filearray[@]}")

Eso da una strcmp()orden similar. Para un orden basado en las reglas de intercalación de la configuración regional como en globos o la salida de ls, agregue un -Mlocaleargumento a perl. Para la ordenación numérica (más como GNU, sort -gya que admite números como +3, 1.2e-5y no miles de separadores, aunque no hexadimales), use en <=>lugar de cmp(y nuevamente -Mlocalepara que se respete la marca decimal del usuario como para el sortcomando).

Estará limitado por el tamaño máximo de argumentos para un comando. Para evitar eso, puede pasar la lista de archivos a perlsu stdin en lugar de a través de argumentos:

readarray -td '' sorted_filearray < <(
  printf '%s\0' "${filearray[@]}" | perl -MFile::Basename -0le '
    chomp(@files = <STDIN>);
    print for sort {basename($a) cmp basename($b)} @files')

Con versiones anteriores de bash, podría usar un while IFS= read -rd ''bucle en lugar de readarray -d ''o perlgenerar la lista de rutas correctamente citadas para que pueda pasarla eval "array=($(perl...))".

Con zsh, puede simular una expansión global para la que puede definir un orden de clasificación:

sorted_filearray=(/(e{'reply=($filearray)'}oe{'REPLY=$REPLY:t'}))

Con reply=($filearray)realmente forzamos la expansión global (que inicialmente era justa /) para ser los elementos de la matriz. Luego definimos el orden de clasificación para que se base en la cola del nombre de archivo.

Para un strcmp()orden similar, fije la configuración regional en C. Para la ordenación numérica (similar a GNU sort -V, no lo sort -nque hace una diferencia significativa al comparar 1.4y 1.23(en lugares donde .está el signo decimal), por ejemplo), agregue el ncalificador global.

En lugar de oe{expression}, también puede usar una función para definir un orden de clasificación como:

by_tail() REPLY=$REPLY:t

o más avanzados como:

by_numbers_in_tail() REPLY=${(j:,:)${(s:,:)${REPLY:t}//[^0-9]/,}}

(entonces a/foo2bar3.pdf(2,3 números) se ordena después b/bar1foo3.pdf(1,3) pero antes c/baz2zzz10.pdf(2,10)) y se usa como:

sorted_filearray=(/(e{'reply=($filearray)'}no+by_numbers_in_tail))

Por supuesto, esos se pueden aplicar en globos reales, ya que para eso están destinados principalmente. Por ejemplo, para una lista de pdfarchivos en cualquier directorio, ordenados por basename / tail:

pdfs=(**/*.pdf(N.oe+by_tail))

¹ Si una strcmp()ordenación basada en es aceptable, y para cadenas cortas, puede transformar las cadenas a su codificación hexadecimal awkantes de pasar sorty volver a transformar después de la ordenación.

Stéphane Chazelas
fuente
Vea esta respuesta a continuación para obtener una gran frase
kael
9

sorten GNU coreutils permite el separador de campo personalizado y la clave. Establece /como separador de campo y ordena en función del segundo campo para ordenar en el nombre base, en lugar de la ruta completa.

printf "%s\n" "${filearray[@]}" | sort -t/ -k2 Producirá

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
Gowtham
fuente
44
Esta es una opción estándar para sort, no una extensión GNU. Esto funcionará si todas las rutas tienen la misma longitud.
Kusalananda
Misma respuesta al mismo tiempo :)
MiniMax
2
Esto funciona solo si las rutas contienen un único directorio cada una. ¿Qué hay de some/long/path/0011.pdf? Por lo que puedo ver en su página de manual, sortsí no contiene ninguna opción para ordenar por el último campo.
Federico Poloni
5

Ordenar con expresión gawk (compatible con bash 's readarray):

Matriz de muestra de nombres de archivo que contienen espacios en blanco :

filearray=("dir1/name 0010.pdf" "dir2/name  0003.pdf" "dir3/name 0040.pdf")

readarray -t sortedfilearr < <(printf '%s\n' "${filearray[@]}" | awk -F'/' '
   BEGIN{PROCINFO["sorted_in"]="@val_num_asc"}
   { a[$0]=$NF }
   END{ for(i in a) print i}')

La salida:

echo "${sortedfilearr[*]}"
dir2/name 0003.pdf dir1/name 0010.pdf dir3/name 0040.pdf

Acceso a un solo artículo:

echo "${sortedfilearr[1]}"
dir1/name 0010.pdf

Eso supone que ninguna ruta de archivo contiene caracteres de nueva línea. Tenga en cuenta que la clasificación numérica de los valores @val_num_ascsolo se aplica a la parte numérica principal de la clave (ninguna en este ejemplo) con respaldo a la comparación léxica (basada en strcmp(), no el orden de clasificación del entorno local) para los vínculos.

RomanPerekhrest
fuente
4
oldIFS="$IFS"; IFS=$'\n'
if [[ -o noglob ]]; then
  setglob=1; set -o noglob
else
  setglob=0
fi

sorted=( $(printf '%s\n' "${filearray[@]}" |
            awk '{ print $NF, $0 }' FS='/' OFS='/' |
            sort | cut -d'/' -f2- ) )

IFS="$oldIFS"; unset oldIFS
(( setglob == 1 )) && set +o noglob
unset setglob

La clasificación de los nombres de archivo con nuevas líneas en sus nombres causará problemas en el sortpaso.

Genera una /lista delimitada awkque contiene el nombre base en la primera columna y la ruta completa como las columnas restantes:

0003.pdf/dir2/0003.pdf
0010.pdf/dir1/0010.pdf
0040.pdf/dir3/0040.pdf

Esto es lo que se ordena y cutse usa para eliminar la primera /columna delimitada. El resultado se convierte en una nueva bashmatriz.

Kusalananda
fuente
@ StéphaneChazelas Un poco peludo, pero está bien ...
Kusalananda
Tenga en cuenta que podría decirse que calcula el nombre base incorrecto para rutas como /some/dir/.
Stéphane Chazelas
@ StéphaneChazelas Sí, pero el OP dijo específicamente que tenía rutas de archivos, así que supondré que hay un nombre base apropiado al final de la ruta.
Kusalananda
Tenga en cuenta que en una configuración regional típica de GNU no C, se a/x.c++ b/x.c-- c/x.c++ordenaría en ese orden, aunque se -clasifique antes +porque -, +y /el peso principal es IGNORE (por lo que comparar x.c++/a/x.c++contra x.c--/b/x.c++primero se compara xcaxccon xcbxc, y solo en caso de empate los otros pesos (donde -viene antes +) sería considerado.
Stéphane Chazelas
Eso podría solucionarse uniéndose en /x/lugar de /, pero eso no resolvería el caso en el que, en la configuración regional C en sistemas basados ​​en ASCII, se a/fooordenaría después, a/foo.txtpor ejemplo, porque se /ordena después ..
Stéphane Chazelas
4

Dado que " dir1y dir2son nombres de ruta arbitrarios", no podemos contar con ellos consistentes en un solo directorio (o del mismo número de directorios). Por lo tanto, debemos convertir la última barra diagonal en los nombres de ruta a algo que no ocurra en otra parte del nombre de ruta. Suponiendo que el carácter @no aparece en sus datos, puede ordenar por nombre base de esta manera:

cat pathnames | sed 's|\(.*\)/|\1@|' | sort -t@ -k+2 | sed 's|@|/|'

El primer sedcomando reemplaza la última barra en cada nombre de ruta con el separador elegido, el segundo invierte el cambio. (Por simplicidad, supongo que los nombres de ruta se pueden entregar uno por línea. Si están en una variable de shell, conviértalos primero a uno por línea).

alexis
fuente
¡Decir ah! ¡Esto es genial! Lo hice un poco más robusto (y un poco más feo) por sustituía un carácter no mostrando de este modo: cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Acabo de agarrarlo \4de la mesa ASCII. Aparentemente "¿FIN DEL TEXTO"?)
Kael
@kael, \4es ^D(control-D). A menos que lo escriba usted mismo en la terminal, es un personaje de control ordinario. En otras palabras, seguro de usar de esta manera.
alexis
3

Solución corta (y algo rápida): al agregar el índice de matriz a los nombres de archivo y ordenarlos, más tarde podemos crear una versión ordenada basada en las indicaciones ordenadas.

Esta solución solo necesita bash builtins, así como el sortbinario, y también funciona con todos los nombres de archivo que no incluyen un \ncarácter de nueva línea .

index=0 sortedfilearray=()
while read -r line ; do
    sortedfilearray+=("${filearray[${line##* }]}")
done <<< "$(for i in "${filearray[@]}" ; do
    echo "$(basename "$i") $((index++))"
done | sort -n)"

Para cada archivo, hacemos eco de su nombre base con su índice inicial agregado de esta manera:

0010.pdf 0
0003.pdf 1
0040.pdf 2

y luego enviado sort -n.

0003.pdf 1
0010.pdf 0
0040.pdf 2

Luego iteramos sobre las líneas de salida, extraemos el índice antiguo con expansión de variable bash ${line##* }e insertamos este elemento al final de la nueva matriz.

nyronium
fuente
1
+1 para una solución que no requiere pasar el nombre completo de cada archivo para ordenar
roaima
3

Esto ordena al anteponer los nombres de ruta del archivo con el nombre base, ordenar eso numéricamente y luego quitar el nombre base del frente de la cadena:

#!/bin/bash
#
filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir4/0003.pdf")

sortarray=($(
    for file in "${filearray[@]}"
    do
        echo "$file"
    done |
        sed -r 's!^(.*)/([[:digit:]]*)(.*)$!\2 \1/\2\3!' |
        sort -t $'\t' -n |
        sed -r 's![^ ]* !!'
))

for item in "${sortarray[@]}"
do
    echo "> $item <"
done

Sería más eficiente si tuviera los nombres de archivo en una lista que podría pasar directamente a través de una tubería en lugar de una matriz de shell, porque el trabajo real lo realiza la sed | sort | sedestructura, pero esto es suficiente.

La primera vez que encontré esta técnica fue cuando codifiqué en Perl; en ese idioma se le conocía como Transformación Schwartziana .

En Bash, la transformación que se proporciona aquí en mi código fallará si tiene no números en el nombre base del archivo. En Perl podría codificarse de manera mucho más segura.

roaima
fuente
Gracias. ¿Qué es una "lista" en bash? ¿Es diferente de bash array? Nunca oí hablar de eso y sería genial. Sí, almacenar los nombres de archivo en una "lista" podría ser una buena idea. Obtuve los nombres de archivo como $@o $*de los argumentos de la línea de comandos para ejecutar un script
Tim
El almacenamiento de los nombres de archivo en un archivo permite utilidades externas, pero también corre el riesgo de malinterpretar, por ejemplo, nuevas líneas.
Jeff Schaller
¿Se utiliza la Transformación de Schwartz para clasificar algún tipo de patrón de diseño, por ejemplo, plantilla, estrategia, ... patrones, tal como se presenta en el libro Design Pattern de Gang of Four?
Tim
@JeffSchaller afortunadamente no hay nuevas líneas en números. Si estuviera escribiendo un código seguro de nombre de archivo completamente genérico, posiblemente no estaría usando bash.
roaima
3

Para nombres de archivo de igual profundidad.

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir3/0014.pdf")

sorted_file_array=($(printf "%s\n" "${filearray[@]}" | sort -n -t'/' -k2))

Explicación

-k POS1 [, POS2] : la opción recomendada, POSIX, para especificar un campo de clasificación. El campo consiste en la parte de la línea entre POS1 y POS2 (o el final de la línea, si se omite POS2), inclusive . Los campos y las posiciones de los personajes están numerados comenzando con 1. Por lo tanto, para ordenar en el segundo campo, usaría '-k 2,2'.

-t SEPARADOR Use el separador de caracteres como el separador de campo cuando encuentre las claves de clasificación en cada línea. De forma predeterminada, los campos están separados por la cadena vacía entre un carácter que no es un espacio en blanco y un carácter de espacio en blanco.

La información se toma del hombre del género.

La matriz de impresión resultante

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
MiniMax
fuente