Classifique uma matriz de nomes de caminho de arquivos por seus nomes de base

8

Suponha que eu tenha uma lista de nomes de caminho dos arquivos armazenados em uma matriz

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

Eu quero classificar os elementos na matriz de acordo com os nomes de base dos nomes dos arquivos, em ordem numérica

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

Como posso fazer isso?

Eu só posso classificar suas partes de nome de base:

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

Estou pensando em

  • criando um array associativo cujas chaves são os nomes de base e os valores são os nomes de caminho, então o acesso aos nomes de caminho é sempre feito através dos nomes de base.
  • criar outra matriz apenas para nomes de base e aplicar sort à matriz de nome de base.

Obrigado.

    
por Tim 23.09.2017 / 14:55

8 respostas

4

Ao contrário do ksh ou zsh, o bash não possui suporte embutido para ordenar matrizes ou listas de cadeias arbitrárias. Ele pode classificar globs ou a saída de alias ou set ou typeset (embora os últimos 3 não estejam na ordem de classificação de localidade do usuário), mas isso não pode ser usado praticamente aqui.

Não há nada na ferramenta POSIX que possa ordenar prontamente listas arbitrárias de strings ¹ ( sort classifica linhas, portanto, apenas sequências curtas (LINE_MAX sendo geralmente menores que PATH_MAX) de caracteres diferentes de NUL e newline, enquanto caminhos de arquivo não são -resvazie seqüências de bytes diferentes de 0).

Assim, você pode implementar seu próprio algoritmo de classificação em awk (usando o operador de comparação < string) ou mesmo bash (usando [[ < ]] ), para caminhos arbitrários em bash , portably, o mais fácil pode ser recorrer a perl :

Com bash4.4+ , você poderia fazer:

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

Isso gera uma ordem semelhante a strcmp() . Para um pedido com base nas regras de agrupamento do locale, como em globs ou na saída de ls , inclua um argumento -Mlocale em perl . Para classificação numérica (mais como GNU sort -g , pois suporta números como +3 , 1.2e-5 e não milhares de separadores, embora não hexadimais), use <=> em vez de cmp (e novamente -Mlocale para o usuário marca decimal a ser honrada como para o comando sort ).

Você será limitado pelo tamanho máximo de argumentos para um comando. Para evitar isso, você poderia passar a lista de arquivos para perl em seu stdin em vez de via argumentos:

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

Com versões anteriores de bash , você poderia usar um while IFS= read -rd '' em vez de readarray -d '' ou obter perl para exibir a lista de caminhos citados corretamente para poder passá-lo para eval "array=($(perl...))" .

Com zsh , você pode falsificar uma expansão glob para a qual você pode definir uma ordem de classificação:

by_tail() REPLY=$REPLY:t

Com reply=($filearray) , na verdade, forçamos a expansão glob (que inicialmente era apenas / ) a ser os elementos da matriz. Em seguida, definimos a ordem de classificação com base na cauda do nome do arquivo.

Para uma ordem semelhante a strcmp() , corrija a localidade para C. Para classificação numérica (semelhante a GNU sort -V , não sort -n , que faz uma diferença significativa ao comparar 1.4 e 1.23 (em locales onde . é a marca decimal), por exemplo, adicione o qualificador n glob.

Em vez de oe{expression} , você também pode usar uma função para definir uma ordem de classificação como:

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

ou mais avançados como:

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

(então a/foo2bar3.pdf (2,3 números) classifica após b/bar1foo3.pdf (1,3) mas antes de c/baz2zzz10.pdf (2,10)) e usar como:

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

Naturalmente, eles podem ser aplicados em globos reais, pois é para isso que eles são destinados principalmente. Por exemplo, para uma lista de pdf arquivos em qualquer diretório, classificados por basename / tail:

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

¹ Se uma classificação strcmp() -based for aceitável e para strings curtas, você poderá transformar as strings em sua codificação hexadecimal com awk antes de passar para sort e transformar novamente após a classificação.

    
por 23.09.2017 / 19:31
9

sort no GNU coreutils permite o separador e a chave do campo personalizado. Você define / como separador de campo e classifica com base no segundo campo para classificar no nome da base, em vez do caminho inteiro.

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

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
    
por 23.09.2017 / 17:44
5

Classificando com a expressão gawk (suportada por bash 's readarray ):

Exemplo de matriz de nomes de arquivos contendo espaços em branco :

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}')

A saída:

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

Acessando item único:

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

Isso pressupõe que nenhum caminho de arquivo contenha caracteres de nova linha. Observe que a classificação numérica dos valores em @val_num_asc se aplica apenas à parte numérica principal da chave (nenhum neste exemplo) com fallback para comparação lexical (com base em strcmp() , não na ordem de classificação do código do idioma) para gravações. / p>     

por 23.09.2017 / 17:08
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

A classificação de nomes de arquivos com novas linhas em seus nomes causará problemas na etapa sort .

Ele gera uma lista / -delimited com awk que contém o nome da base na primeira coluna e o caminho completo como as colunas restantes:

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

Isso é o que é classificado, e cut é usado para remover a primeira coluna / -delimited. O resultado é transformado em uma nova matriz bash .

    
por 23.09.2017 / 15:30
3

Solução curta (e um pouco rápida): Ao anexar o índice da matriz aos nomes de arquivos e classificá-los, podemos posteriormente criar uma versão classificada com base nos índices classificados.

Esta solução só precisa de bash builtins, bem como o sort binário, e também funciona com todos os nomes de arquivos que não incluam um novo caractere \n .

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 arquivo, fazemos eco de seu nome de base com seu índice inicial acrescentado assim:

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

e depois enviado por sort -n .

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

Depois, iteramos as linhas de saída, extraímos o índice antigo com a expansão da variável bash ${line##* } e inserimos esse elemento no final da nova matriz.

    
por 23.09.2017 / 15:43
3

Isso classifica os nomes de caminho do arquivo com o nome da base, classificando-o numericamente e, em seguida, removendo o nome da base da frente da string:

#!/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:]]*)(.*)$! /!' |
        sort -t $'\t' -n |
        sed -r 's![^ ]* !!'
))

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

Seria mais eficiente se você tivesse os nomes de arquivos em uma lista que pudesse ser passada diretamente através de um pipe em vez de como uma matriz shell, porque o trabalho real é feito pela estrutura sed | sort | sed , mas isso é suficiente.

Eu me deparei com essa técnica pela primeira vez ao codificar em Perl; nessa linguagem, era conhecido como uma Transformação Schwartziana .

No Bash, a transformação como dada aqui no meu código falhará se você tiver não-numéricos no basename do arquivo. Em Perl, poderia ser codificado com muito mais segurança.

    
por 23.09.2017 / 15:34
3

Para nomes de arquivos de profundidade iguais.

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

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

Explicação

-k POS1[,POS2] - The recommended, POSIX, option for specifying a sort field. The field consists of the part of the line between POS1 and POS2 (or the end of the line, if POS2 is omitted), inclusive. Fields and character positions are numbered starting with 1. So to sort on the second field, you'd use '-k 2,2'.

-t SEPARATOR Use character SEPARATOR as the field separator when finding the sort keys in each line. By default, fields are separated by the empty string between a non-whitespace character and a whitespace character.

A informação é tirada do homem do tipo.

A matriz resultante é impressa

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
    
por 23.09.2017 / 17:45
3

Como " dir1 e dir2 são nomes de caminho arbitrários", não podemos contar com eles consistindo em um único diretório (ou com o mesmo número de diretórios). Portanto, precisamos converter a barra last nos nomes dos caminhos para algo que não ocorra em outro lugar no nome do caminho. Supondo que o caractere @ não ocorra em seus dados, você pode classificar por nome de base assim:

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

O primeiro comando sed substitui a barra última em cada nome de caminho pelo separador escolhido, o segundo inverte a mudança. (Para simplificar, estou assumindo que os nomes de caminho podem ser entregues um por linha. Se eles estiverem em uma variável de shell, converta-os para o formato de uma por linha primeiro.)

    
por 24.09.2017 / 18:21