Вопрос Удалите все, кроме каждых 12-го файла


У меня есть несколько тысяч файлов в формате filename.12345.end. Я хочу сохранить только каждый 12-й файл, поэтому file.00012.end, file.00024.end ... file.99996.end и удалить все остальное.

Файлы могут также иметь номера раньше в имени файла и обычно имеют форму: file.00064.name.99999.end

Я использую оболочку Bash и не могу понять, как перебирать файлы, а затем вытаскивать номер и проверять, number%%12=0  удаляя файл, если нет. Может кто-нибудь мне помочь?

Спасибо, Дорина


14
2017-09-12 13:27


происхождения


Является ли количество файлов зависящим только от имени файла? - Arronical
Кроме того, файлы всегда имеют 5 цифр, а суффикс и префикс всегда одинаковы? - Arronical
Да, это всегда 5 цифр. Я не уверен, правильно ли получил ваш первый вопрос. Файлы с разными именами файлов разные, и мне нужны эти конкретные файлы, которые имеют номера 00012, 00024 и т. Д. - Dorina
@ Дорина, пожалуйста редактировать ваш вопрос и сделать это ясно. Он меняет все! - terdon♦
И все они в одном каталоге, не так ли? - Sergiy Kolodyazhnyy


ответы:


Вот решение Perl. Это должно быть намного быстрее для тысяч файлов:

perl -e '@bad=grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV; unlink @bad' *

Которые могут быть дополнительно сконденсированы в:

perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

Если у вас слишком много файлов и вы не можете использовать простые *, вы можете сделать что-то вроде:

perl -e 'opendir($d,"."); unlink grep{/(\d+)\.end/ && $1 % 12 != 0} readdir($dir)'

Что касается скорости, сравните этот подход и оболочку, предоставленную в одном из других ответов:

$ touch file.{01..64}.name.{00001..01000}.end
$ ls | wc
  64000   64000 1472000
$ time for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

real    2m44.258s
user    0m9.183s
sys     1m7.647s

$ touch file.{01..64}.name.{00001..01000}.end
$ time perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

real    0m0.610s
user    0m0.317s
sys     0m0.290s

Как вы можете видеть, разница огромна, как и ожидалось,

объяснение

  • -e просто говорит perl для запуска скрипта, указанного в командной строке.
  • @ARGV - это специальная переменная, содержащая все аргументы, заданные скрипту. Поскольку мы это даем *, он будет содержать все файлы (и каталоги) в текущем каталоге.
  • grep будет искать список имен файлов и искать любые, которые соответствуют строке чисел, точка и end (/(\d+)\.end/),

  • Поскольку числа (\d) находятся в группе захвата (круглые скобки), они сохраняются как $1, Итак grep затем проверит, является ли это число кратным 12, а если нет, имя файла будет возвращено. Другими словами, массив @bad содержит список файлов, которые нужно удалить.

  • Затем список передается unlink() который удаляет файлы (но не каталоги).


18
2017-09-12 16:15





Учитывая, что ваши имена файлов находятся в формате file.00064.name.99999.end, сначала нам нужно убрать все, кроме нашего номера. Мы будем использовать for чтобы сделать это.

Нам также нужно сообщить оболочке Bash, чтобы использовать базу 10, потому что арифметика Bash будет обрабатывать их числа, начинающиеся с 0 в качестве базы 8, что будет бесполезно для нас.

Как скрипт, который будет запущен, когда в каталоге, содержащем файлы, используйте:

#!/bin/bash

for f in ./*
do
  if [[ -f "$f" ]]; then
    file="${f%.*}"
    if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
      rm "$f"
    fi
  else
    echo "$f is not a file, skipping."
  fi
done

Или вы можете использовать эту очень длинную уродливую команду, чтобы сделать то же самое:

for f in ./* ; do if [[ -f "$f" ]]; then file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; else echo "$f is not a file, skipping."; fi; done

Объяснить все части:

  • for f in ./* означает для всего, что находится в текущем каталоге, do .... Это устанавливает каждый файл или каталог, найденный как переменная $ f.
  • if [[ -f "$f" ]] проверяет, является ли найденный элемент файлом, если мы не перейдем к echo "$f is not... part, что означает, что мы не начинаем случайно удалять каталоги.
  • file="${f%.*}" устанавливает переменную $ file в качестве обрезания имени файла, которое происходит после последнего .,
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]] где находится основная арифметика. ${file##*.} обрезает все до последнего . в нашем имени файла без расширения. $(( $num % $num2 )) является синтаксисом для арифметики Баша для использования операции по модулю, 10# в начале говорит Башу использовать базу 10, чтобы справиться с этими надоедливыми ведущими 0. $((10#${file##*.} % 12)) затем оставит нам остальную часть нашего имени файла, деленную на 12. -ne 0проверяет, является ли остаток «не равным» нулю.
  • Если остаток не равен 0, файл удаляется с помощью rm команды, вы можете захотеть заменить rm с echo при первом запуске этого, чтобы проверить, что вы хотите удалить ожидаемые файлы.

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

if с помощью echo команда, чтобы предупреждать о каталогах, на самом деле не нужна, поскольку rm на своем собственном будет жаловаться на каталоги, а не удалять их, поэтому:

#!/bin/bash

for f in ./*
do
  file="${f%.*}"
  if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
    rm "$f"
  fi
done

Или

for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

Будет работать тоже правильно.


12
2017-09-12 15:10



призвание rm несколько тысяч раз может быть довольно медленным. Я предлагаю echo имя файла вместо этого и передать вывод цикла в xargs rm (добавьте опции по мере необходимости): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --, - David Foerster
Я отредактировал, чтобы включить предлагаемое повышение скорости. - Arronical
Фактически после тестирования в каталоге с 55999 файлами исходная версия заняла 2 минуты 48 секунд, xargs версия заняла 5 минут 1 сек. Это может быть связано с накладными расходами на echo @DavidFoerster? - Arronical
Странный. Для 60.000 файлов я получаю 0m0.659s / 0m0.545s / 0m0.380s (real / user / sys) с time { for f in *; do echo "$f"; done | xargs rm; } против 1 м11.450s / 0m10.695s / 0m16.800s с time { for f in *; do rm "$f"; done; } на tmpfs. Bash - v4.3.11, Kernel - v4.4.19. - David Foerster


Вы можете использовать расширение Bash для создания имен, содержащих каждый 12-й номер. Давайте создадим некоторые тестовые данные

$ touch file.{0..9}{0..9}{0..9}{0..9}{0..9}.end # create test data
$ mv file.00024.end file.00024.end.name.99999.end # testing this form of filenames

Тогда мы можем использовать следующие

$ ls 'file.'{00012..100..12}* # print these with numbers less than 100
file.00012.end                 file.00036.end  file.00060.end  file.00084.end
file.00024.end.name.99999.end  file.00048.end  file.00072.end  file.00096.end
$ rm 'file.'{00012..100000..12}* # do the job

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


6
2017-09-13 08:34



Мне нравится кодовое гольф-игра на этом. - David Foerster


Немного долго, но это то, что мне пришло в голову.

 for num in $(seq 1 1 11) ; do
     for sequence in $(seq -f %05g $num 12 99999) ; do
         rm file.$sequence.end.99999;
     done
 done

Объяснение: Удалите каждый 12-й файл одиннадцать раз.


1
2017-09-13 15:59





Во всем смирении я думаю, что это решение намного приятнее, чем другой ответ:

find . -name '*.end' -depth 1 | awk 'NR%12 != 0 {print}' | xargs -n100 rm

Немного объяснения. Сначала мы создаем список файлов с find, Мы получаем все файлы, чье имя заканчивается .end и которые находятся на глубине 1 (то есть, они находятся непосредственно в рабочем каталоге, а не в каких-либо подпапках. Вы можете оставить это, если нет подпапок). Список результатов будет отсортирован в алфавитном порядке.

Затем мы передаем этот список в awk, где мы используем специальную переменную NR который является номером строки. Мы оставляем каждый 12-й файл, печатая файлы, где NR%12 != 0, awk команда может быть сокращена до awk 'NR%12', потому что результат работы оператора modulo интерпретируется как булево значение, а {print} в любом случае неявно делается.

Итак, теперь у нас есть список файлов, которые нужно удалить, что мы можем сделать с xargs и rm. xargs выполняет заданную команду (rm) со стандартным вводом в качестве аргументов.

Если у вас много файлов, вы получите сообщение об ошибке «слишком длинный список аргументов» (на моей машине это ограничение составляет 256 кбайт, а минимальное значение, требуемое POSIX, составляет 4096 байт). Этого можно избежать -n 100 флаг, который разбивает аргументы каждые 100 слов (а не строки, что следует учитывать, если имена файлов имеют пробелы) и выполняет отдельный rm команда, каждая из которых содержит всего 100 аргументов.


0
2017-09-12 16:16



Есть несколько проблем с вашим подходом: -depth должно быть до -name; ii) это не удастся, если какое-либо из имен файлов содержит пробелы; iii) вы предполагаете, что файлы будут перечислены в порядке возрастания по порядку (это то, что ваш awk тестирует), но это почти наверняка не будет. Следовательно, это приведет к удалению случайного набора файлов. - terdon♦
d'ах! Вы совершенно правы, мой плохой (комментарий отредактирован). Я получил ошибку из-за неправильного размещения и не помню -depth, Тем не менее, это было наименьшее из проблем здесь, самым важным является то, что вы удаляете случайный набор файлов, а не те, которые нужны OP. - terdon♦
О, и нет, -depth не принимает значения, и это делает противоположное тому, что вы думаете, что оно делает. Видеть man find: "-depth Обработать содержимое каждого каталога до самого каталога.". Таким образом, это действительно спустится в подкаталоги и повредит повсюду. - terdon♦
I) Оба -depth n а также -maxdepth n существовать. Первое требует, чтобы глубина была ровно п, а с последней она могла быть <= n. II). Да, это плохо, но для этого конкретного примера это не проблема. Вы можете исправить это, используя find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rm, который использует нулевой байт в качестве разделителя записей (который не разрешен в именах файлов). III) Еще раз, в этом случае предположение является разумным. В противном случае вы можете вставить sort -n между find а также awk, или перенаправить find в файл и сортировать его, как вам нравится. - user593851
Ах, вы, вероятно, используете OSX. Это совсем другая реализация find, Опять же, главная проблема заключается в том, что вы предполагаете, что find возвращает отсортированный список. Это не так. - terdon♦


Для использования только bash первым моим подходом было бы: 1. переместить все файлы, которые вы хотите сохранить в другой каталог (т. Е. Все те, число которых в имени файла кратно 12), затем 2. удалить все остальные файлы в каталоге, затем 3. поместите несколько файлов из 12, которые вы сохранили, где они были. Так что-то вроде этого может работать:

cd dir_containing_files
mkdir keep_these_files
n=0
while [ "${n}" -lt 99999 ]; do
  padded_n="`echo -n "00000${n}" | tail -c 5`"
  mv "filename${padded_n}.end" keep_these_files/
  n=$[n+12]
done
rm filename*.end
mv keep_these_files/* .
rmdir keep_these_files

0
2017-09-14 01:42



Мне нравится этот подход, но как вы создаете filename если это не согласовано? - Arronical