Видаліть усі файли, крім самих останніх X у файлі bash


157

Чи є простий спосіб у досить стандартному середовищі UNIX з bash виконати команду для видалення всіх файлів, крім самих останніх X, з каталогу?

Щоб дати трохи більше конкретного прикладу, уявіть собі, що робота із cron виписує файл (скажімо, файл журналу чи резервну копію резервного копіювання) у каталог щогодини. Мені б хотілося запустити ще одну роботу з cron, яка видалила б найстаріші файли в цьому каталозі, поки не буде менше, скажімо, 5.

І щоб було зрозуміло, присутній лише один файл, його ніколи не слід видаляти.

Відповіді:


117

Проблеми з наявними відповідями:

  • неможливість обробки імен файлів із вбудованими пробілами чи новими рядками.
    • що стосується рішень, які викликають rmбезпосередньо під заміною команди без котирування ( rm `...`), існує додатковий ризик непередбачуваного глобалізації.
  • неможливість розрізнити файли та каталоги (тобто, якщо каталоги потрапили до числа 5 останніх модифікованих елементів файлової системи, ви фактично збережете менше 5 файлів, і застосування rmдо каталогів не вдасться).

відповідь wnoise вирішує ці питання, але рішення є специфічним (і досить складним) для GNU .

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

Для запису, ось пояснення того, чому, як правило, не годиться аналізувати lsвихід: http://mywiki.wooledge.org/ParsingLs

ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}

Вищезазначене є неефективним , оскільки xargsмає викликати rmодин раз для кожного імені файлу.
Можливо, ваша платформа xargsдозволить вам вирішити цю проблему:

Якщо у вас є GNU xargs , команда use -d '\n', яка змушує xargsрозглядати кожен рядок введення окремим аргументом, але передає стільки ж аргументів, скільки буде розміщено в командному рядку відразу :

ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --

-r( --no-run-if-empty) гарантує, що rmне буде викликано, якщо немає вводу.

Якщо у вас є BSD xargs (включаючи macOS ), ви можете використовувати -0для обробки NULрозділеного вводу, попередньо переклавши нові рядки в NUL( 0x0) chars., Який також передає (як правило) всі назви файлів відразу (також працюватиме з GNU xargs):

ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --

Пояснення:

  • ls -tpдрукує назви елементів файлової системи, відсортованих за тим, як нещодавно вони були змінені, у порядку зменшення (спочатку останні змінили елементи спочатку) ( -t), з каталогізаторами, надрукованими з /відміткою для позначення їх як таких ( -p).
  • grep -v '/$'потім вилучаємо каталоги з отриманого списку, опускаючи ( -v) рядки, у яких є кінцевий /( /$).
    • Caveat : Оскільки симпосилання, яка вказує на каталог , технічно сама по собі не є каталогом, такі посилання не будуть виключені.
  • tail -n +6пропускає перші 5 записів у лістингу, фактично повертаючи всі, крім 5 останніх модифікованих файлів, якщо такі є.
    Зауважте, що для виключення Nфайлів N+1необхідно перейти до tail -n +.
  • xargs -I {} rm -- {}(та його варіації) потім посилається rmна всі ці файли; якщо матчів взагалі xargsнемає, нічого не зробить.
    • xargs -I {} rm -- {}визначає заповнювач заповнення, {}який представляє кожен рядок введення в цілому , тому rmвін викликається один раз для кожного вхідного рядка, але з назви файлів із вбудованими пробілами, обробленими правильно.
    • --у всіх випадках гарантує , що будь-які імена файлів , які відбуваються , щоб почати з -не помиляємось для опцій по rm.

Варіації на вихідній задачі, в разі , якщо відповідні файли повинні бути оброблені по окремо або зібраний в масиві оболонки :

# One by one, in a shell loop (POSIX-compliant):
ls -tp | grep -v '/$' | tail -n +6 | while IFS= read -r f; do echo "$f"; done

# One by one, but using a Bash process substitution (<(...), 
# so that the variables inside the `while` loop remain in scope:
while IFS= read -r f; do echo "$f"; done < <(ls -tp | grep -v '/$' | tail -n +6)

# Collecting the matches in a Bash *array*:
IFS=$'\n' read -d '' -ra files  < <(ls -tp | grep -v '/$' | tail -n +6)
printf '%s\n' "${files[@]}" # print array elements

2
Безумовно, краще, ніж більшість інших відповідей тут, тому я радий надати свою підтримку, навіть якщо я вважаю, що ігнорування випадку нового рядка - це справа, яку потрібно робити лише обережно.
Чарльз Даффі

2
Якщо ви lsне знаходитесь у поточному каталозі, то шляхи до файлів містять '/', це означає, що grep -v '/'нічого не відповідатиме. Я вважаю grep -v '/$', що ви хочете виключити лише каталоги.
waldol1

1
@ waldol1: Спасибі; Я оновив відповідь, щоб включити вашу пропозицію, яка також робить grepкоманду концептуальніше зрозумілішою. Однак зауважте, що описана вами проблема не виходила б з одного шляху до каталогу; наприклад, ls -p /private/varвсе одно надрукували б лише найменування файлів. Тільки якщо ви передали кілька аргументів файлів (як правило, через глобус), ви побачили б фактичні шляхи у висновку; наприклад, ls -p /private/var/*(і ви також побачите вміст відповідних підкаталогів, якщо ви також не включені -d).
mklement0

108

Видаліть усі, крім 5 (або будь-яку кількість) останніх файлів у каталозі.

rm `ls -t | awk 'NR>5'`

2
Мені це потрібно було лише для розгляду моїх архівних файлів. змінити ls -tнаls -td *.bz2
Джеймс Т Снелл

3
Я використовував це для каталогів, змінивши його на rm -rf ls -t | awk 'NR>1'(я хотів лише останніх). Дякую!
lohiaguitar91

11
ls -t | awk 'NR>5' | xargs rm -f якщо ви віддаєте перевагу трубам і вам потрібно придушити помилку, якщо нічого не можна видалити.
H2ONaCl

16
Короткий і читабельний, можливо, але небезпечний у використанні; якщо спробувати видалити файл, створений за допомогою touch 'hello * world', це видалить абсолютно все з поточного каталогу .
Чарльз Даффі

1
Незважаючи на те, що на це відповіли у 2008 році, це працює як шарм і просто те, що мені потрібно було просто видалити старі резервні копії з певного каталогу. Дивовижно.
Ренс Тіллманн

86
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm

Ця версія підтримує імена з пробілами:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm

20
Ця команда неправильно обробляє файли з пробілами в іменах.
tylerl

5
(ls -t|head -n 5;ls)- командна група . Він друкує 5 останніх файлів двічі. sortз'єднує однакові лінії. uniq -uвидаляє дублікати, так що залишаються всі, крім 5 останніх файлів. xargs rmдзвінки rmпо кожному з них.
Фабієн

15
Це видаляє всі ваші файли, якщо у вас є 5 або менше! Додати --no-run-if-emptyдо, xargsяк у, (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rmбудь ласка, оновіть відповідь
Gonfi den Tschal

3
Навіть той, що "підтримує імена з пробілами", небезпечний. Розглянемо ім’я, яке містить буквальні лапки: touch 'foo " bar'викине всю решту команди.
Чарльз Даффі

2
... це безпечніше використовувати , xargs -d $'\n'ніж ін'єкційні лапки в зміст, хоча NUL задає вхідний потік (що вимагає використовувати що - то інше , ніж lsна самому ділі робити праворуч) є варіантом ідеально.
Чарльз Даффі

59

Простіший варіант відповіді thelsdj:

ls -tr | head -n -5 | xargs --no-run-if-empty rm 

ls -tr відображає всі файли, найстаріші спочатку (-t новітні перші, -r зворотні).

head -n -5 відображає всі, крім 5 останніх рядків (тобто 5 найновіших файлів).

xargs rm викликає rm для кожного вибраного файлу.


15
Потрібно додати --no-run-if-empty до xargs, щоб воно не вийшло з ладу, якщо менше 5 файлів.
Том

лс -1тр | голова -n -5 | xargs rm <---------- вам потрібно додати -1 до ls, інакше ви не отримаєте список результатів, щоб голова правильно працювала проти
Al Joslin

3
@AlJoslin, -1за замовчуванням, коли вихід на конвеєр, тому тут це не обов'язково. Це має набагато більші проблеми, пов’язані з поведінкою за замовчуванням під xargsчас розбору імен з пробілами, цитатами та c.
Чарльз Даффі

здається, що --no-run-if-emptyв моїй оболонці не розпізнається Я використовую Cmder на windows.
StayFoolish

Можливо, потрібно використовувати -0варіант, якщо назви файлів можуть містити пробіли. Ще не перевірили. джерело
Кіт

18
find . -maxdepth 1 -type f -printf '%T@ %p\0' | sort -r -z -n | awk 'BEGIN { RS="\0"; ORS="\0"; FS="" } NR > 5 { sub("^[0-9]*(.[0-9]*)? ", ""); print }' | xargs -0 rm -f

Потрібен пошук GNU для -printf, і GNU сортування для -z, і GNU awk для "\ 0", і GNU xargs для -0, але обробляє файли з вбудованими новими рядками або пробілами.


2
Якщо ви хочете видалити каталоги, просто змініть -f на -d і додайте -r до rm. знайти. -maxdepth 1 -тип d -printf '% T @% p \ 0' | сортувати -r -z -n | awk 'ПОЧАТИ {RS = "\ 0"; ORS = "\ 0"; FS = ""} NR> 5 {sub ("^ [0-9] * (. [0-9] *)?", ""; друк} '| xargs -0 rm -rf
alex

1
На перший погляд, я здивований складності (або, з цього приводу, необхідності) awkлогіки. Чи пропускаю я якісь вимоги у питанні ОП, які роблять це необхідним?
Чарльз Даффі

@Charles Duffy: Sub () видаляє часову позначку, за якою відсортовано. Марка часу, вироблена "% T @", може містити частину дробу. Розщеплення на простір за допомогою FS розбиває шляхи з вбудованими пробілами. Я гадаю, що видалення за допомогою перших космічних творів, але читати майже так само важко. Роздільники RS та ORS не можна встановити в командному рядку, оскільки вони є NUL.
похмуріть

1
@wnoise, мій звичайний підхід до цього полягає в з'єднанні в while read -r -d ' '; IFS= -r -d ''; do ...петлю оболонки - перше зчитування закінчується на просторі, а друге продовжується до NUL.
Чарльз Даффі

@Charles Duffy: Я завжди пильную сирої оболонки, можливо, через візантійські проблеми цитування. Зараз я вважаю, що GNU sed -z -e 's/[^ ]* //; 1,5d'є найяснішим. (Або , можливо sed -n -z -e 's/[^ ]* //; 6,$p',
wnoise

14

Усі ці відповіді не вдається, якщо в поточному каталозі є каталоги. Ось що працює:

find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm

Це:

  1. працює, коли в поточному каталозі є каталоги

  2. намагається видалити кожен файл, навіть якщо попередній не вдалося видалити (через дозволи тощо)

  3. не працює в безпеці, коли кількість файлів у поточному каталозі надмірна і xargsзазвичай перекручує вас ( -x)

  4. не обслуговує пробіли у іменах (можливо, ви використовуєте неправильну ОС?)


5
Що станеться, якщо findповернеться більше імен файлів, ніж можна передавати в одному командному рядку ls -t? (Підказка: Ви отримуєте кілька запусків ls -t, кожен з яких сортується лише індивідуально, а не має глобально правильний порядок сортування; таким чином, ця відповідь погано порушена під час роботи з досить великими каталогами).
Чарльз Даффі

12
ls -tQ | tail -n+4 | xargs rm

Перелічіть імена файлів за часом модифікації, цитуючи кожне ім'я файлу. Виключіть перші 3 (3 останніх). Видаліть залишилися.

EDIT після корисного коментаря від mklement0 (спасибі!): Виправлено аргумент -n + 3, і зауважте, що це не буде працювати, як очікувалося, якщо назви файлів містять нові рядки та / або каталог містить підкаталоги.


Схоже, цей -Qваріант не існує на моїй машині.
П'єр-Адрієн Буйсон

4
Гм, цей варіант є в основних програмах GNU протягом ~ 20 років, але він не згадується у варіантах BSD. Ви на mac?
Марк

Я справді є. Не думав, що існують відмінності для таких дійсно основних команд між сучасними системами. Дякую за вашу відповідь !
П’єр-Адрієн Буйсон

3
@ Марк: ++ для -Q. Так, -Qце розширення GNU (ось специфікація POSIXls ). Невеликий застереження (рідко проблема на практиці): -Qкодує вбудовані нові рядки у назви файлів як буквальні \n, які rmне розпізнаються. Щоб виключити перші 3 , xargsаргумент повинен +4. Нарешті, застереження, яке стосується і більшості інших відповідей: ваша команда буде працювати лише за призначенням, якщо в поточному режимі немає підкаталогів .
mklement0

1
Коли нічого зняти, у вас є дзвінки xargs з --no-run-if-emptyопцією:ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Олів'є Лекривейн

8

Ігнорування нових рядків - це ігнорування безпеки та хорошого кодування. wnoise мав єдину добру відповідь. Ось варіант щодо його, який розміщує назви файлів у масиві $ x

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )

2
Я б запропонував очищення IFS- інакше ви ризикуєте втратити пробіли з останнього файлу з назви файлів. Можна застосувати це до команди read:while IFS= read -rd ''; do
Чарльз Даффі

1
чому "${REPLY#* }"?
msciwoj

4

Якщо у іменах файлів немає пробілів, це спрацює:

ls -C1 -t| awk 'NR>5'|xargs rm

Якщо у назви файлів є пробіли, щось подібне

ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh

Основна логіка:

  • отримати список файлів у порядку часу, один стовпець
  • отримати всі, крім перших 5 (n = 5 для цього прикладу)
  • перша версія: надішліть їх до rm
  • друга версія: gen скрипт, який видалить їх належним чином

Не забувайте про while readхитрість поводження з просторами: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
pinkeen

1
@pinkeen, не зовсім безпечний, як там. while IFS= read -r dбуло б трохи краще - -rзапобігає використанню літералів зворотної косої риски read, а також IFS=запобігає автоматичному обрізанню пробілів пробілу.
Чарльз Даффі

4
До речі, якщо хтось переживає ворожі імена файлів, це надзвичайно небезпечний підхід. Розглянемо файл, створений за допомогою touch $'hello \'$(rm -rf ~)\' world'; Буквальні цитати всередині імені файлу будуть протиставляти літературні цитати, які ви додаєте sed, в результаті чого код у назві файлу виконується.
Чарльз Даффі

1
(щоб було зрозуміло, вище "це" посилалося на | shформу, що є тією, що має вразливість оболонки для введення оболонки).
Чарльз Даффі

2

З зш

Якщо припустити, що ви не переймаєтесь наявними каталогами, і у вас не буде більше 999 файлів (виберіть більшу кількість, якщо хочете, або створіть цикл часу).

[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])

В *(.om[6,999]), в .файлах значить, oзасіб порядок сортування вгору, mзасоби по даті модифікації (покласти aна час доступу або cдля зміни инода), то [6,999]вибирає діапазон файлу, тому не Р.М. 5 перших.


Інтригуюче, але все життя мені не вдалося змусити класифікатора глобального сортування ( om) працювати (будь-яке сортування, яке я пробував, не показало ефекту - ні на OSX 10.11.2 (пробував із zsh 5.0.8 та 5.1.1) , ні на Ubuntu 14.04 (zsh 5.0.2)) - що мені не вистачає ?. Що стосується діапазону кінцевої точки: немає необхідності в жорсткий код, просто використовувати -1для позначення останнього запису і , таким чином , включають в себе всі інші файли: [6,-1].
mklement0

2

Я усвідомлюю, що це стара нитка, але, можливо, хтось від цього виграє. Ця команда знайде файли в поточному каталозі:

for F in $(find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n' | sort -r -z -n | tail -n+5 | awk '{ print $2; }'); do rm $F; done

Це трохи надійніше, ніж деякі попередні відповіді, оскільки дозволяє обмежити пошуковий домен файлами, що відповідають виразам. Спочатку знайдіть файли, які відповідають будь-яким умовам. Роздрукуйте ці файли із позначками поруч із ними.

find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n'

Далі сортуйте їх за часовими позначками:

sort -r -z -n

Потім зніміть із списку чотири останні файли:

tail -n+5

Візьміть 2-й стовпець (ім'я файлу, а не часова позначка):

awk '{ print $2; }'

А потім загорніть все це у формуляр для твердження:

for F in $(); do rm $F; done

Це може бути більш багатослівна команда, але я мав набагато більше удачі, коли я міг орієнтуватися на умовні файли та виконувати проти них більш складні команди.


1

знайшов цікавий cmd у Sed-Onliners - Видалити останні 3 рядки - це ідеально підходить для іншого способу шкіри кота (гаразд ні), але ідея:

 #!/bin/bash
 # sed cmd chng #2 to value file wish to retain

 cd /opt/depot 

 ls -1 MyMintFiles*.zip > BigList
 sed -n -e :a -e '1,2!{P;N;D;};N;ba' BigList > DeList

 for i in `cat DeList` 
 do 
 echo "Deleted $i" 
 rm -f $i  
 #echo "File(s) gonzo " 
 #read junk 
 done 
 exit 0

1

Видаляє всі, крім 10 останніх (найчастіше) файлів

ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm

Якщо менше 10 файлів не видаляється жоден файл, і у вас буде: error error: незаконний кількість рядків - 0

Для підрахунку файлів з bash


1

Мені потрібно було елегантне рішення для зайнятого ящика (маршрутизатора), всі рішення xargs або масиву були для мене марними - такої команди там немає. Знайти і mtime - це не відповідна відповідь, оскільки ми говоримо про 10 пунктів, а не обов'язково 10 днів. Відповідь Еспо була найкоротшою та найчистішою та, ймовірно, найбільш неперевершеною.

Помилка з пробілами, і коли жодні файли не потрібно видаляти, обидва просто вирішуються стандартним способом:

rm "$(ls -td *.tar | awk 'NR>7')" 2>&-

Трохи освітніша версія: ми можемо це зробити все, якщо будемо використовувати awk по-іншому. Зазвичай я використовую цей метод для передачі (повернення) змінних від awk до sh. Оскільки ми весь час читали, що неможливо зробити, я прошу відрізнятись: ось метод.

Приклад для файлів .tar без проблем щодо пробілів у імені файлу. Для перевірки замініть "rm" на "ls".

eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}')

Пояснення:

ls -td *.tarперелічені всі файли .tar, відсортовані за часом. Щоб застосувати до всіх файлів у поточній папці, видаліть частину "d * .tar"

awk 'NR>7... пропускає перші 7 рядків

print "rm \"" $0 "\"" будує рядок: rm "ім'я файлу"

eval виконує його

Оскільки ми використовуємо rm, я б не використовував вищезазначену команду в сценарії! Більш розумне використання:

(cd /FolderToDeleteWithin && eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}'))

У разі використання ls -tкоманди не завдасть ніякої шкоди на таких нерозумних прикладах, як: touch 'foo " bar'іtouch 'hello * world' . Не те, щоб ми коли-небудь створювали файли з такими іменами в реальному житті!

Sidenote. Якби ми хотіли передати змінну до sh таким чином, ми просто змінили б друк (проста форма, не допускається пробілів):

print "VarName="$1

встановити змінну VarNameдо значення $1. Кілька змінних можна створити за один раз. Це VarNameстає звичайною змінною sh і може бути нормально використане в сценарії або оболонці згодом. Отже, щоб створити змінні з awk та повернути їх до оболонки:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"

0
leaveCount=5
fileCount=$(ls -1 *.log | wc -l)
tailCount=$((fileCount - leaveCount))

# avoid negative tail argument
[[ $tailCount < 0 ]] && tailCount=0

ls -t *.log | tail -$tailCount | xargs rm -f

2
xargsбез -0мінімуму або з голим мінімумом -d $'\n'ненадійний; спостерігайте, як це поводиться з файлом з пробілами або цитатами у його імені.
Чарльз Даффі

0

Я перетворив це на сценарій bash shell. Використання: keep NUM DIRде NUM - це кількість файлів, які потрібно зберегти, а DIR - каталог, який потрібно очистити.

#!/bin/bash
# Keep last N files by date.
# Usage: keep NUMBER DIRECTORY
echo ""
if [ $# -lt 2 ]; then
    echo "Usage: $0 NUMFILES DIR"
    echo "Keep last N newest files."
    exit 1
fi
if [ ! -e $2 ]; then
    echo "ERROR: directory '$1' does not exist"
    exit 1
fi
if [ ! -d $2 ]; then
    echo "ERROR: '$1' is not a directory"
    exit 1
fi
pushd $2 > /dev/null
ls -tp | grep -v '/' | tail -n +"$1" | xargs -I {} rm -- {}
popd > /dev/null
echo "Done. Kept $1 most recent files in $2."
ls $2|wc -l
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.