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

14

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

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

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

Спасибо дорина

Дорина
источник
Номер файла зависит только от имени файла?
Arronical
Кроме того, файлы всегда имеют 5 цифр, и суффикс и префикс всегда одинаковы?
Arronical
Да, это всегда 5 цифр. Я не уверен, правильно ли я понял ваш первый вопрос. Файлы с разными именами файлов разные, и мне нужны эти конкретные файлы, которые имеют номера 00012, 00024 и т. Д.
Дорина
3
@ Дорина, пожалуйста, отредактируйте свой вопрос и проясните это. Это меняет все!
Тердон
2
И все они в одном каталоге, верно?
Сергей Колодяжный,

Ответы:

18

Вот решение 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()который удаляются файлы (но не каталоги).

terdon
источник
12

Учитывая, что ваши имена файлов в формате 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...части, что означает, что мы не начинаем удаление каталогов случайно.
  • file="${f%.*}"устанавливает переменную $ file в качестве имени файла, обрезая все, что идет после последнего ..
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]]это где основная арифметика вступает в силу. ${file##*.}Обрезает все до последнего .в нашем имени файла без расширения. $(( $num % $num2 ))является синтаксисом арифметики Bash для использования операции по модулю, а 10#в начале Bash указывает Bash использовать 10, чтобы справиться с этими надоедливыми ведущими нулями. $((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

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

Arronical
источник
5
Звонить rmнесколько тысяч раз можно довольно медленно. Я предлагаю echoимя файла вместо и трубы на выходе контура на xargs rm(варианты добавлений по мере необходимости): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
Дэвид Фёрстер
Я отредактировал, чтобы включить ваши предложенные улучшения скорости.
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; }1m11.450s / 0m10.695s / 0m16.800s с time { for f in *; do rm "$f"; done; }tmpfs. Bash v4.3.11, ядро ​​v4.4.19.
Дэвид Фёрстер
6

Вы можете использовать расширение скобок 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

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

Nykakin
источник
Мне нравится игра в гольф на этом.
Дэвид Фёрстер
1

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

 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-й файл одиннадцать раз.

Террик
источник
0

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

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', потому что результат оператора по модулю интерпретируется как логическое значение, и в {print}любом случае выполняется неявно.

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

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

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

Для использования только 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
DELT
источник
Мне нравится подход, но как вы создаете filenameчасть, если она не соответствует?
Arronical