Сортировка и mv файлов на основе имени файла (с пробелами), рекурсивно

Сортировка и mv файлов на основе имени файла (с пробелами), рекурсивно

Я допустил ошибку и свалил файлы в один каталог. К счастью, я могу отсортировать их по имени файла: ''' 2019-02-19 20.18.58.ndpi_2_2688_2240.jpg ''' Где#немного или2в данном конкретном случае это информация о местоположении, так сказать. Диапазон составляет 0-9, и все имена файлов имеют одинаковую длину, так что это число всегда будет в одной и той же позиции имени файла (26-й символ, включая пробелы, с подчеркиваниями по бокам). Я нашел эту замечательную ссылку: команда для поиска файлов путем поиска только части их имен?

Однако я не могу передать вывод в команду move. Я попытался зациклить вывод в переменную, но это тоже не сработало:

for f in find . -name '*_0_*' ; do  mv "$f" /destination/directory ; done

Судя по этой ссылке, я, возможно, поставил * или некоторые кавычки не в том месте:mv: невозможно stat Такого файла или каталога нет в скрипте оболочки.

Тем не менее, у меня много каталогов, и я хотел бы отсортировать их в идентичную структуру каталогов где-нибудь в другом месте:

-Flowers (to be sorted)          -Flowers-buds               -Flowers-stems
     -Roses                          -Roses                      -Roses
        buds.jpg       ===>             buds.jpg     ===>           stems.jpg
        stems.jpg
        petals.jpg
     -Daisies                       -Daisies                    -Daisies
        buds.jpgs                       buds.jpg                   stems.jpg
        stems.jpg
        petals.jpg
     -Tulips                        -Tulips                     -Tulip
        buds.jpgs                       buds.jpg                  stems.jpg
        stems.jpg
        petals.jpg

...и многое другое на основе этого числа (#). Это практично сделать в bash? Я запускаю MacOS в терминале с установленным coreutils, поэтому инструменты должны вести себя как GNU linux, а не darwin (BSD).

решение1

Вы можете сделать это с помощью findи mv, но это не самый простой подход. Вы на macOS, так чтозшпредустановлен. Zsh поставляется с удобным инструментом для массового переименования файлов, который называетсяzmv. Запустите zshв терминале, затем в zsh выполните что-то вроде:

autoload zmv
zmv -n '[^_]#_([0-9])_*' '/elsewhere/${1}/${f}'

Пояснения:

  • -nговорит zmv, чтобы отображалось то, что он будет делать, но на самом деле ничего не делает. Когда вы будете довольны тем, что он показывает, запустите команду снова без -n.
  • Первый невариантный аргумент — этошаблон подстановочных знаков. zmvбудет действовать в отношении соответствующих файлов (и игнорировать несоответствующие файлы).
  • [^_]#означает ноль или более символов, отличных от _. Zsh дает вам доступ к тем же подстановочным знакам, что и bash, и многим другим. #— это функция, доступная только в zsh (в bash она доступна с другим синтаксисом), которая означает «любое количество предыдущих символов». Шаблон [^_]#_[0-9]_*соответствует любому имени файла, содержащему одну цифру между двумя подчеркиваниями и без других подчеркиваний перед этой цифрой.
  • Скобки вокруг [0-9]делают цифру доступной, как $1в заменяющем тексте.
  • В тексте замены можно использовать символы ${1}, ${2}и т. д. для ссылки на заключенные в скобки фрагменты в исходном шаблоне, а также ${f}для ссылки на полное исходное имя.

Например, фрагмент выше перемещается 2019-02-19 20.18.58.ndpi_2_2688_2240.jpgв /elsewhere/2/2019-02-19 20.18.58.ndpi_2_2688_2240.jpg. Ваш вопрос не описывает, что именно нужно переместить и куда именно, но вы должны иметь возможность получить то, что хотите, изменив команду выше. Если вы не можете понять, отредактируйте свой вопрос, добавив конкретные примеры того, что должно и не должно совпадать.

Если у вас есть файлы в подкаталогах, вы можете использовать */в начале шаблона. Если вам нужно сопоставить каталог, чтобы ссылаться на него с помощью , вам нужно поместить скобки вокруг : вы не можете поместить скобки вокруг слеша. Если вы хотите пройти по каталогу рекурсивно и сопоставить файлы с любым долгом, используйте for${NUM}***/рекурсивная подстановка. В качестве исключения вам нужно использовать (**/)для доступа к пути к каталогу в тексте замены как . В таких случаях часто проще не использовать скобки, а вместо этого использовать${NUM}модификаторына ${f}для извлечения частей исходного пути в целом. Например, выполните то же переименование, что и выше, но переместите файлы из текущего каталога в параллельную структуру под /elsewhere, но с дополнительным уровнем 0, 1и т. д. непосредственно перед именем файла:

autoload zmv
zmv -n '**/[^_]#_([0-9])_*' '/elsewhere/${f:h}/${1}/${f:t}'

Если сложно сопоставить файлы по имени, можно использовать другой подход — сопоставить их по времени изменения¹. Если вы просто скопировали кучу файлов в каталог, который не менялся некоторое время, то новые файлы — это те, которые имеют недавнее время изменения. Вы можете сопоставить файлы по времени изменения в zsh с помощьюквалификатор globc. Например, чтобы вывести список файлов, которые были изменены в последний раз за последний час:

ls -lctr *(ch-1)

Вместо того, чтобы искать время окончания, вы можете обратиться кНпоследние измененные файлы с квалификаторами glob oc(для сортировки по возрастанию ctime) и (для сохранения только первых[1,N]Н(соответствует). Например, если вы знаете, что только что переместили 42 файла в этот каталог и с тех пор ничего в нем не меняли:

ls -lctr *(oc[1,42])

Если вы хотите использовать квалификаторы glob с zmv, вам нужно передать -Qопцию. Например, чтобы переместить файлы в каталог на основе их имени, как указано выше, но игнорировать все файлы, которые не изменялись в течение последнего часа:

zmv -n -Q '[^_]#_([0-9])_*(ch-1)' '/elsewhere/${1}/${f}'

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

Например, чтобы переместить обычные файлы из текущего каталога в совершенно новую иерархию, если их имя содержит _0_:

zmodload -m -F zsh/files 'b:zf_*'
for x in **/*_0_*(.); do
  zf_mkdir -p /destination/directory/$x:h
  zf_mv ./$x /destination/directory/$x
done

( :hеще одна функция zsh:модификатор истории.)

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

zmodload -m -F zsh/files 'b:zf_*'
for source in **/*_<->_*(.); do
  suffix=${${source:t}#*_<->_}
  n=${${source%$suffix}##*_}
  destination=${source:h}/$n/${source:t}
  zf_mkdir -p /destination/directory/$destination:h
  zf_mv ./$source /destination/directory/$destination
done

Если вы хотите узнать о других методах массового переименования файлов, вы можете просмотретьвопросы, отмеченные renameна этом сайте.

¹ Время изменения inode файла, называемое «временем изменения» или «ctime» для краткости, — это время последнего перемещения файла или последнего изменения его атрибутов, таких как разрешения. Оно отличается от времени изменения (mtime).

решение2

Оболочка разделяет ввод по пробелам. Вы можете использовать подстановку bash с рекурсивным, **чтобы правильно разделить имена файлов:

shopt -s globstar
for f in **/*_0_*; do mv "$f" /dest/; done

Если все они направляются в одно и то же место, цикл не нужен:

shopt -s globstar
mv **/*_0_*  /dest/

... или используйте find -exec, который передает имена файлов напрямую из find в exec()вызов, без участия оболочки:

find . -name '*_0_*' -exec mv {} /dest/ \;

Или используйте readplus find -print0для разделения по null. Это излишне для этой проблемы, полезно при глобализации и find -execне подходит для этой задачи:

while IFS=  read -r -d ''; do mv "$REPLY" /dest/; done < <( find . -name '*_0_*' -print0 )

Чтобы изменить назначение верхнего уровня на основе имени файла, как в вашем примере, отрежьте части имени файла, используя ${var#remove_from_start}и ${var%remove_from_end}. Подстановочные знаки в шаблонах удаления удалят как можно меньше; чтобы удалить как можно больше, удвойте символ операции, как в ${…##…}или ${…%%…}:

    shopt -s globstar
    for f in **/*_0_*; do            # Flowers/Roses/stems.jpg 
      file=${f##*/}                  # stems.jpg
      base=${file%.*}                # stems
      path=${f%/*}                   # Flowers/Roses
      toppath=${path%%/*}            # Flowers
      subpath=${path#*/}             # Roses
      dest=$toppath-$base/$subpath/  # Flowers-stems/Roses/
      [[ -d $dest ]] || mkdir -p "$dest"
      mv "$f" "$dest"
    done

Связанный контент