Сортировать массив путей к файлам по их базовым именам

8

Предположим, что у меня есть список путей файлов, хранящихся в массиве

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

Я хочу отсортировать элементы в массиве в соответствии с базовыми именами имен файлов в числовом порядке

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

Как я могу это сделать?

Я могу сортировать только их базовые части:

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

Я думаю о

  • создание ассоциативного массива, ключи которого являются базовыми именами, а значения - именами путей, поэтому доступ к путевым именам всегда осуществляется через базовые имена.
  • создание другого массива только для базовых имен и применение sortк массиву базовых имен.

Спасибо.

Тим
источник
1
Это не очень хорошая идея, но вы можете сортировать в bash
Джефф Шаллер
Осторожнее с массивом, основанным на базовых именах, если бы вы могли иметь dir1 / 42.pdf и dir2 / 42.pdf
Джефф Шаллер
Это (разные пути с одним и тем же базовым именем) не происходит в моем случае. Но если bash-скрипт справится с этим, это будет здорово. У меня нет достаточно хороших требований к тому, как сортировать имена путей с одинаковыми базовыми именами, может, кто-то другой может. dir1 dir2только что составлены, и они на самом деле являются произвольными путями.
Тим

Ответы:

4

В отличие от ksh или zsh, bash не имеет встроенной поддержки сортировки массивов или списков произвольных строк. Он может сортировать глобусы или выходные данные aliasили setили typeset(хотя последние 3 не в порядке сортировки локали пользователя), но это практически невозможно использовать здесь.

В инструментарии POSIX нет ничего, что могло бы легко сортировать произвольные списки строк¹ (sort сортирует строки, поэтому только короткие (LINE_MAX часто короче PATH_MAX) последовательности символов, отличных от NUL и новой строки, в то время как пути к файлам являются непустыми последовательностями байтов, отличными от других. чем 0).

Таким образом, хотя вы можете реализовать свой собственный алгоритм сортировки в awk(используя <оператор сравнения строк) или дажеbash (используя [[ < ]]) для произвольных путей bash, переносимо, проще всего прибегнуть к perl:

С помощью bash4.4+вы можете сделать:

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

Это дает strcmp()похожий порядок. Для порядка, основанного на правилах сортировки локали, таких как globs или выходные данные ls, добавьте -Mlocaleаргумент в perl. Для числовой сортировки (больше похожей на GNU, так sort -gкак она поддерживает числа, подобные +3, 1.2e-5а не разделители тысяч, хотя и не шестнадцатеричные), используйте <=>вместо cmp(и снова -Mlocaleдля того, чтобы десятичная отметка пользователя учитывалась, как для sortкоманды).

Вы будете ограничены максимальным размером аргументов команды. Чтобы избежать этого, вы можете передать список файлов perlна его стандартный ввод вместо аргументов:

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

В более старых версиях bashвы могли бы использовать while IFS= read -rd ''цикл вместо readarray -d ''или получить perlдля вывода список путей, правильно заключенных в кавычки, чтобы вы могли передать его eval "array=($(perl...))".

С помощью zshвы можете подделать расширение glob, для которого вы можете определить порядок сортировки:

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

С помощью reply=($filearray)мы фактически форсируем глобальное расширение (которое изначально было просто/ ) быть элементами массива. Затем мы определяем порядок сортировки на основе хвоста имени файла.

Для strcmp()порядка, подобного языку, установите языковой стандарт на C. Для числовой сортировки (аналогично GNU sort -V, sort -nкоторая не имеет существенного значения при сравнении 1.4и 1.23(например, в языковых стандартах, где .используется десятичная метка), добавьтеn квалификатор glob.

Вместо этого oe{expression}вы также можете использовать функцию для определения порядка сортировки, например:

by_tail() REPLY=$REPLY:t

или более продвинутые, такие как:

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

(так a/foo2bar3.pdf(2,3 числа) сортирует после b/bar1foo3.pdf(1,3), но до c/baz2zzz10.pdf(2,10)) и использует как:

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

Конечно, они могут быть применены к реальным шарам, поскольку это то, для чего они в первую очередь предназначены. Например, для списка pdfфайлов в любом каталоге, отсортированного по базовому имени / хвосту:

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

¹ Если strcmp()приемлема сортировка на основе и для коротких строк, вы можете преобразовать строки в их шестнадцатеричное кодирование с помощью awkдо передачи в sortи преобразовать обратно после сортировки.

Стефан Шазелас
источник
Посмотрите этот ответ ниже для отличной однострочной версии
kael
9

sortв GNU coreutils позволяет настраивать разделитель полей и ключ. Вы устанавливаете в /качестве разделителя полей и сортируете на основе второго поля для сортировки по базовому имени, а не по всему пути.

printf "%s\n" "${filearray[@]}" | sort -t/ -k2 будет производить

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
Gowtham
источник
4
Это стандартная опция sort, а не расширение GNU. Это будет работать, если все пути имеют одинаковую длину.
Кусалананда
Тот же ответ в то же время :)
MiniMax
2
Это работает, только если пути содержат один каталог каждый. Как насчет some/long/path/0011.pdf? Насколько я вижу из его справочной страницы, она sortне содержит опций сортировки по последнему полю.
Федерико Полони
5

Сортировка с поглазеть выражением (поддерживается Баш «сreadarray ):

Пример массива имен файлов, содержащих пробелы :

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

Выход:

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

Доступ к одному элементу:

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

Это предполагает, что ни один путь к файлу не содержит символов новой строки. Обратите внимание, что числовая сортировка значений в @val_num_ascотносится только к ведущей числовой части ключа (в данном примере - ни одной) с отступом к лексическому сравнению (на основе strcmp()порядка сортировки локали) для связей.

RomanPerekhrest
источник
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

Сортировка имен файлов с символами новой строки в их именах вызовет проблемы на sortшаге.

Он генерирует /список с неограниченным доступом, awkкоторый содержит базовое имя в первом столбце и полный путь в качестве оставшихся столбцов:

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

Это то, что отсортировано, и cutиспользуется для удаления первого /столбца с неограниченным количеством. Результат превращается в новый bashмассив.

Кусалананда
источник
@ StéphaneChazelas Немного волосатый, но хорошо ...
Кусалананда
Обратите внимание, что, возможно, он вычисляет неправильное базовое имя для таких путей, как /some/dir/.
Стефан
@ StéphaneChazelas Да, но ОП специально сказал, что у него есть пути к файлам, поэтому я просто предположу, что в конце пути есть правильное базовое имя.
Кусалананда
Обратите внимание, что в типичной локали GNU, не относящейся к C, a/x.c++ b/x.c-- c/x.c++будет отсортирован в таком порядке, хотя -сортируется раньше, +потому что -, +и /первичный вес равен IGNORE (поэтому сравнение x.c++/a/x.c++с x.c--/b/x.c++первым сравнивается xcaxcс xcbxc, и только в случае связей будут другие веса (где -раньше +) будет рассмотрено.
Стефан
Это можно было бы обойти, присоединившись /x/вместо /, но это не учитывало бы случай, когда в локали C на системах, основанных на ASCII, a/fooсортировался бы, a/foo.txtнапример, потому что /сортирует после ..
Стефан
4

Поскольку « dir1и dir2являются произвольными путями», мы не можем рассчитывать на то, что они состоят из одного каталога (или одного и того же количества каталогов). Поэтому нам нужно преобразовать последнюю косую черту в путевых именах во что-то, чего нет в других местах путевого имени. Предположим, что символ @не встречается в ваших данных, вы можете отсортировать по базовому имени следующим образом:

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

Первая sedкоманда заменяет последнюю косую черту в каждом пути на выбранный разделитель, вторая отменяет изменение. (Для простоты я предполагаю, что имена путей могут быть доставлены по одному на строку. Если они находятся в переменной оболочки, сначала преобразуйте их в формат по одной строке.)

Alexis
источник
Ха! Это замечательно! Я сделал это немного более надежным (и немного уродливее) по заменял не-отображающее характер следующим образом: cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Я только что взял \4из таблицы ASCII. Видимо "КОНЕЦ ТЕКСТА"?)
Каэль
@kael, \4есть ^D(control-D). Если вы не введете его самостоятельно в терминале, это обычный управляющий символ. Другими словами, безопасно использовать таким способом.
Алексис
3

Краткое (и несколько быстрое) решение: добавив индекс массива к именам файлов и отсортировав их, мы позже можем создать отсортированную версию на основе отсортированных признаков.

Это решение требует только встроенных команд bash и sortдвоичного файла, а также работает со всеми именами файлов, которые не включают \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)"

Для каждого файла мы отображаем его базовое имя с начальным индексом, добавленным так:

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

а затем отправили через sort -n.

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

После этого мы перебираем выходные строки, извлекаем старый индекс с расширением переменной bash ${line##* }и вставляем этот элемент в конец нового массива.

nyronium
источник
1
+1 за решение, которое не требует передачи полного имени каждого файла для сортировки
roaima
3

Это сортирует путем добавления к путевым именам файлов базового имени, числовой сортировки и последующего удаления базового имени в начале строки:

#!/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

Было бы более эффективно, если бы у вас были имена файлов в списке, которые можно было бы передать непосредственно через канал, а не как массив оболочки, потому что фактическая работа выполняется sed | sort | sed структурой, но этого достаточно.

Впервые я столкнулся с этой техникой при кодировании на Perl; на этом языке это было известно как преобразование Шварца .

В Bash преобразование, как указано здесь в моем коде, завершится ошибкой, если у вас есть нечисловые значения в базовом имени файла. В Perl это можно кодировать гораздо безопаснее.

roaima
источник
Спасибо. что такое "список" в bash? Отличается ли он от массива bash? Я никогда не слышал об этом, и это было бы здорово. да, хранение имен файлов в «списке» может быть хорошей идеей. Я получил имена файлов как $@или $*из аргументов командной строки для запуска сценария
Тим
Хранение имен файлов в файле позволяет использовать внешние утилиты, но также рискует неверно истолковать, скажем, переводы строк.
Джефф Шаллер
Используется ли преобразование Шварцяна для сортировки какого-либо шаблона проектирования, например, шаблона, стратегии, ... шаблона, как это было представлено в книге «Шаблон проектирования» Банды четырех?
Тим
@JeffSchaller к счастью, в номерах нет новых строк. Если бы я писал совершенно общий код, безопасный для имен файлов, я бы, вероятно, не использовал бы bash.
Ройма
3

Для одинаковой глубины имен файлов.

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

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

объяснение

-k POS1 [, POS2] - рекомендуемая опция POSIX для указания поля сортировки. Поле состоит из части строки между POS1 и POS2 (или конца строки, если POS2 опущен), включительно . Поля и позиции символов нумеруются, начиная с 1. Поэтому для сортировки по второму полю вы должны использовать `-k 2,2 '.

-t СЕПАРАТОР Используйте символ СЕПАРАТОР в качестве разделителя полей при поиске ключей сортировки в каждой строке. По умолчанию поля разделяются пустой строкой между непробельным символом и пробельным символом.

Информация взята у человека рода.

Полученный массив печати

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
MiniMax
источник