Видаліть усі файли, окрім кожного 12-го


14

У мене є кілька тисяч файлів у форматі імені файлу.12345.end. Я хочу лише зберегти кожен 12-й файл, тому файл.00012.end, файл.00024.end ... файл.99996.end та видалити все інше.

Файли також можуть мати номери раніше у своєму імені файлу і зазвичай мають форму: file.00064.name.99999.end

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

Дякую, Дорина


Чи залежить лише номер файлу від назви файла?
Арронічний

Також, чи завжди у файлах є 5 цифр, а суфікс і префікс завжди однакові?
Арронічний

Так, це завжди 5 цифр. Я не впевнений, чи правильно я отримаю ваше перше запитання. Файли з різними іменами різні, і мені потрібні ці конкретні файли, які мають цифри 00012, 00024 і т. Д.
Дорина,

3
@Dorina, будь ласка, відредагуйте своє запитання та уточніть це. Це все змінює!
тердон

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()який видаляє файли (але не каталоги).


12

З огляду на те, що ваші імена файлів у форматі file.00064.name.99999.end, спершу потрібно обрізати все, крім нашої кількості. Для цього ми використаємо forцикл.

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

Також буде працювати правильно.


5
Дзвінки rmв кілька тисяч разів можуть бути досить повільними. Я пропоную echoім'я файлу замість і труби на виході контуру на xargs rm(варіанти додавань по мірі необхідності): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
Девід Фоерстер

Я відредагував, щоб включити запропоноване покращення швидкості.
Арронічний

Насправді після тестування на каталозі з 55999 файлами оригінальна версія займала 2 хвилини 48 секунд, xargsверсія займала 5 хвилин 1 сек. Це може бути пов’язано з накладними витратами на echo@DavidFoerster?
Арронічний

Незвичайно. Для 60.000 файлів я отримую 0m0.659s / 0m0.545s / 0m0.380s (реальний / користувач / 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

Працює безнадійно повільно для великої кількості файлів - для генерування тисяч імен потрібен час і пам’ять - тож це більше фокус, ніж фактично ефективне рішення.


Мені подобається гольф на коді на цьому.
Девід Фоерстер

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 аргументів.


3
З вашим підходом є кілька питань: -depthпотрібно бути раніше -name; ii) це не вдасться, якщо будь-яке з назв файлів містить пробіл; iii) ви припускаєте, що файли будуть перераховані у порядку зростання (це те, що ви awkпротестуєте), але це майже точно не буде. Тому це видалить випадковий набір файлів.
тердон

d'oh! Ти цілком прав, моя погана (коментар відредагований). Я отримав помилку через неправильне розміщення і не пам’ятав -depth. І все-таки це було найменше питань тут, найважливішим є те, що ви видаляєте випадковий набір файлів, а не ті, які хоче ОП.
тердон

О, і ні, -depthне приймає значення, і це робить протилежне тому, що ви думаєте, що це робить. Див man find. "-Depth Обробляти вміст кожного каталогу перед самим каталогом." Таким чином, це насправді спуститься у підкаталоги і спричинить хаос всюди.
тердон

I) І те, -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

Мені подобається підхід, але як ви генеруєте filenameчастину, якщо вона не є послідовною?
Аронічний
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.