Classifique uma matriz de nomes de caminhos de arquivos pelos nomes de base

8

Suponha que eu tenha uma lista de nomes de caminhos de 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 de arquivos, em ordem numérica

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

Como eu posso fazer isso?

Só posso classificar as partes do nome da base:

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

Eu estou pensando sobre

  • criando uma matriz associativa cujas chaves são os nomes de base e os valores são os nomes de caminho, portanto, o acesso aos nomes de caminho é sempre feito via nomes de base.
  • criando outra matriz apenas para nomes de base e aplique- sorta à matriz de nome de base.

Obrigado.

Tim
fonte
1
Não é uma boa idéia, mas você pode classificar em bash
Jeff Schaller
Cuidado com um array digitado nos nomes de base, se você pudesse ter dir1 / 42.pdf e dir2 / 42.pdf
Jeff Schaller
Isso (nomes de caminho diferentes com o mesmo nome de base) não acontece no meu caso. Mas se um script bash puder lidar com isso, será ótimo. Não tenho requisitos razoavelmente bons sobre como classificar nomes de caminho com o mesmo nome de base, talvez alguém o faça. dir1 dir2são apenas inventadas e, na verdade, são nomes de caminho arbitrários.
Tim

Respostas:

4

Ao contrário do ksh ou zsh, o bash não tem suporte interno para classificar matrizes ou listas de cadeias arbitrárias. Ele pode classificar globs ou a saída de aliasor setor typeset(embora os últimos 3 não estejam na ordem de classificação do código do idioma do usuário), mas isso não pode ser usado praticamente aqui.

Não há nada no baú da ferramenta POSIX que possa classificar prontamente listas arbitrárias de seqüências de caracteres¹ ( sortclassifica linhas, apenas sequências curtas (LINE_MAX geralmente são mais curtas que PATH_MAX) de caracteres diferentes de NUL e newline, enquanto os caminhos de arquivo são sequências de bytes não vazias, que 0).

Portanto, embora você possa implementar seu próprio algoritmo de classificação em awk(usando o <operador de comparação de cadeias) ou mesmobash (usando [[ < ]]), para caminhos arbitrários em bash, de maneira portátil, o mais fácil pode ser o de perl:

Com bash4.4+, você pode fazer:

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

Isso dá uma strcmp()ordem semelhante. Para uma ordem baseada em regras de agrupamento da localidade como em bolhas ou a saída de ls, adicione um -Mlocaleargumento para perl. Para classificação numérica (mais parecida com o GNU sort -g, pois suporta números como +3, 1.2e-5e não milhares de separadores, embora não hexadimais), use em <=>vez de cmp(e novamente -Mlocalepara que a marca decimal do usuário seja honrada como para o sortcomando).

Você ficará limitado pelo tamanho máximo de argumentos a um comando. Para evitar isso, você pode passar a lista de arquivos para perlseu stdin em vez de via 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')

Nas versões mais antigas do bash, você pode usar um while IFS= read -rd ''loop em vez de readarray -d ''ou obter perla lista de caminhos citados corretamente, para que possa transmiti-lo eval "array=($(perl...))".

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

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

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

Para uma strcmp()ordem semelhante, fixe o código do idioma para C. Para a classificação numérica (semelhante ao GNU sort -V, não o sort -nque faz uma diferença significativa ao comparar 1.4e 1.23(em locais onde .é a marca decimal), por exemplo), adicione o nqualificador glob.

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

by_tail() REPLY=$REPLY:t

ou mais avançados como:

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

(so a/foo2bar3.pdf(2,3 números) classifica depois de b/bar1foo3.pdf(1,3) mas antes de c/baz2zzz10.pdf(2,10)) e usa como:

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

Obviamente, eles podem ser aplicados em globs reais, pois é para isso que eles se destinam principalmente. Por exemplo, para uma lista de pdfarquivos em qualquer diretório, classificados por nome de base / cauda:

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

¹ Se a strcmp()classificação baseada em uma é aceitável, e para cadeias curtas, você pode transformar as cadeias em sua codificação hexadecimal awkantes de passar para sorte transformar novamente após a classificação.

Stéphane Chazelas
fonte
Veja esta resposta abaixo de um grande one-liner bash: unix.stackexchange.com/a/394166/41735
kael
9

sortno GNU coreutils permite separador de campo personalizado e chave. Você define /como separador de campos 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 vai produzir

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
Gowtham
fonte
4
Esta é uma opção padrão para sort, não uma extensão GNU. Isso funcionará se os caminhos tiverem o mesmo comprimento.
Kusalananda
Mesma resposta no mesmo tempo :)
MiniMax
2
Isso funciona apenas se os caminhos contiverem um único diretório cada. Que tal some/long/path/0011.pdf? Tanto quanto posso ver na sua página de manual, sortdoes não contém nenhuma opção para classificar pelo último campo.
Federico Poloni
5

Classificação com expressão gawk (suportada pelo 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 @val_num_ascse aplica apenas à parte numérica inicial da chave (nenhum neste exemplo) com fallback para comparação lexical (com base na strcmp()ordem de classificação do código do idioma), para vínculos.

RomanPerekhrest
fonte
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 sortetapa.

Ele gera uma /lista delimitada com awko 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 que é classificado e cuté usado para remover a primeira /coluna delimitada. O resultado é transformado em uma nova bashmatriz.

Kusalananda
fonte
@ StéphaneChazelas Um pouco peludo, mas tudo bem ...
Kusalananda
Observe que, sem dúvida, ele calcula o nome de base errado para caminhos como /some/dir/.
Stéphane Chazelas
@ StéphaneChazelas Sim, mas o OP disse especificamente que ele tinha caminhos de arquivos, então eu assumirei que existe um nome de base adequado no final do caminho.
Kusalananda
Observe que em uma localidade típica GNU não C, a/x.c++ b/x.c-- c/x.c++ela seria classificada nessa ordem, embora seja -classificada antes +porque -, +e /o peso principal de IGNORE (portanto, a comparação x.c++/a/x.c++com as x.c--/b/x.c++primeiras compara e xcaxccontra xcbxc, e somente em caso de empate, os outros pesos (onde -vem antes +) seria considerado.
Stéphane Chazelas
Isso poderia ser contornado através da junção em /x/vez de /, mas isso não resolveria o caso em que, no local C em sistemas baseados em ASCII, a/fooclassificaria depois, a/foo.txtpor exemplo, porque /classifica depois ..
Stéphane Chazelas
4

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

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

O primeiro sedcomando substitui a última barra em cada nome de caminho pelo separador escolhido, o segundo reverte a alteração. (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 primeiro para o formato de um por linha.)

alexis
fonte
Ha! Isso é ótimo! Eu fiz isso um pouco mais robusto (e um pouco mais feia) por subbing um caractere não exibindo assim: cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Eu apenas peguei \4da tabela ASCII Aparentemente "FIM DO TEXTO".?)
kael
@kael, \4é ^D(control-D). A menos que você digite no terminal, é um caractere de controle comum. Em outras palavras, seguro de usar dessa maneira.
21418 alexis
3

Solução curta (e um tanto rápida): anexando o índice da matriz aos nomes dos arquivos e ordenando-os, podemos criar mais tarde uma versão classificada com base nas indicações ordenadas.

Essa solução precisa apenas dos bash do bash, bem como do sortbinário, e também funciona com todos os nomes de arquivos que não incluem um \ncaractere de nova linha .

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, repetimos o nome da base com o índice inicial anexado da seguinte forma:

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

e depois enviado sort -n.

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

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

nyronium
fonte
1
+1 para uma solução que não requer passando o nome completo de cada arquivo a espécie
roaima
3

Isso classifica anexando os nomes de caminho do arquivo com o nome da base, classificando-o numericamente e 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:]]*)(.*)$!\2 \1/\2\3!' |
        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 poderia ser passada diretamente através de um canal, e não como uma matriz de shell, porque o trabalho real é realizado pelo sed | sort | sed estrutura, mas isso é suficiente.

Eu me deparei com essa técnica ao codificar em Perl; naquela língua, era conhecida como Transformação Schwartziana .

No Bash, a transformação, conforme indicado aqui no meu código, falhará se você tiver dados não numéricos no nome de base do arquivo. No Perl, poderia ser codificado com muito mais segurança.

roaima
fonte
obrigado. o que é uma "lista" no bash? É diferente da matriz bash? Eu nunca ouvi falar disso e seria ótimo. sim, armazenar os nomes de arquivos em uma "lista" pode ser uma boa ideia. Eu tenho os nomes de arquivos como $@ou $*de argumentos de linha de comando para executar um script
Tim
Armazenar os nomes de arquivo em um arquivo permite utilitários externos, mas também corre o risco de interpretar mal, por exemplo, novas linhas.
Jeff Schaller
A Schwartzian Transform é usada na classificação de algum tipo de padrão de design, por exemplo, modelo, estratégia, ... padrões, conforme introduzido no livro Design Pattern da Gang of Four?
Tim
@JeffSchaller, felizmente, não há novas linhas em números. Se eu estivesse escrevendo um código totalmente genérico, seguro para nome de arquivo, possivelmente não estaria usando o bash.
roaima 23/09
3

Para nomes de arquivos com profundidade igual.

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] - A opção POSIX recomendada para especificar um campo de classificação. O campo consiste na parte da linha entre POS1 e POS2 (ou no final da linha, se o POS2 for omitido), inclusive . Os campos e as posições dos caracteres são numerados começando com 1. Então, para classificar no segundo campo, você usaria `-k 2,2 '.

-t SEPARATOR Use o caractere SEPARATOR como separador de campos ao localizar as chaves de classificação em cada linha. Por padrão, os campos são separados pela sequência vazia entre um caractere que não é de espaço em branco e um caractere de espaço em branco.

As informações são retiradas do homem do tipo.

A impressão resultante da matriz

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