Удалить все, кроме самых последних файлов X в Bash

Есть ли простой способ в довольно стандартной среде UNIX с bash выполнить команду, чтобы удалить все, кроме самых последних X-файлов из каталога?

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

И чтобы было ясно, присутствует только один файл, он никогда не должен быть удален.

21 ответ

Решение

Проблемы с существующими ответами:

  • невозможность обрабатывать имена файлов со встроенными пробелами или символами новой строки.
    • в случае решений, которые вызывают 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 использовать -d '\n', что делает xargs Рассмотрите каждую входную строку как отдельный аргумент, но передайте столько аргументов, сколько поместится в командной строке одновременно:

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

-r ( --no-run-if-empty ) гарантирует, что rm не вызывается, если нет ввода.

Если у вас есть BSD xargs (в том числе на OS X), вы можете использовать -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) строки, которые имеют конечный / (/$).
    • Предостережение: поскольку символическая ссылка, которая указывает на каталог, технически сама по себе не является каталогом, такие символические ссылки не будут исключены.
  • 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

Удалите все кроме 5 (или любого другого числа) самых последних файлов в каталоге.

rm `ls -t | awk 'NR>5'`
(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

Более простой вариант ответа thelsdj:

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

ls -tr отображает все файлы, сначала самые старые (сначала сначала -t, либо наоборот -r).

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

xargs rm вызывает rm для каждого выбранного файла.

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 find для -printf, GNU sort для -z, GNU awk для "\0" и GNU xargs для -0, но обрабатывает файлы со встроенными символами новой строки или пробелами.

Все эти ответы терпят неудачу, когда есть каталоги в текущем каталоге. Вот то, что работает:

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

Это:

  1. работает, когда есть каталоги в текущем каталоге

  2. пытается удалить каждый файл, даже если предыдущий не может быть удален (из-за разрешений и т. д.)

  3. отказоустойчив, когда количество файлов в текущем каталоге чрезмерно и xargs будет нормально тебя перепутать -x)

  4. не учитывает пробелы в именах файлов (возможно, вы используете не ту ОС?)

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

Список имен файлов по времени изменения, цитируя каждое имя файла. Исключить первые 3 (3 самых последних). Удалить оставшиеся.

РЕДАКТИРОВАТЬ после полезного комментария от mklement0 (спасибо!): Исправлен аргумент -n+3, и обратите внимание, что это не будет работать должным образом, если имена файлов содержат символы новой строки и / или каталог содержит подкаталоги.

Для Linux (инструменты GNU) эффективный и надежный способ сохранитьnновейшие файлы в текущем каталоге при удалении остальных:

      n=5

find . -maxdepth 1 -type f -printf '%T@ %p\0' |
sort -z -nrt ' ' -k1,1 |
sed -z -e "1,${n}d" -e 's/[^ ]* //' |
xargs -0r rm -f

Для БСД,findне имеет-printfпредикат, не может выводить байты NULL и + не может обрабатыватьNULL-записи с разделителями.

Вот решение, которое не поддерживает новые строки в путях, но защищает от них, отфильтровывая их:

      #!/bin/bash
n=5

find . -maxdepth 1 -type f ! -path $'*\n*' -exec stat -f '%.9Fm %N' {} + |
sort -nrt ' ' -k1,1 |
awk -v n="$n" -F'^[^ ]* ' 'NR > n {printf "%s%c", $2, 0}' |
xargs -0 rm -f

примечание: я использую bashиз-за $'\n'обозначение. Поскольку вы можете определить переменную, содержащую буквальный перевод строки, и использовать ее вместо этого.


Решение для UNIX и Linux (вдохновлено AIX/HP-UX/SunOS/BSD/Linux).ls -b):

Некоторые платформы не предоставляютfind -printf, ни , ни поддержкаNUL-записи с разделителями //awk//. Вот почему использование, вероятно, является наиболее переносимым способом решения проблемы, потому что оно доступно по умолчанию почти в каждой ОС.

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

примечание: значение по умолчанию имеет разрешение секунды, но начиная сperl-5.8.9вы можете получить разрешение менее секунды с помощьюstatфункция модуляTime::HiRes(когда это поддерживают и ОС, и файловая система). Это то, что я использую здесь; если вы не предоставляете его, вы можете удалить‑MTime::HiRes=statиз командной строки.

      n=5

find . '(' -name '.' -o -prune ')' -type f -exec \
perl -MTime::HiRes=stat -le '
    foreach (@ARGV) {
        @st = stat($_);
        if ( @st > 0 ) {
            s/([\\\n])/sprintf( "\\%03o", ord($1) )/ge;
            print sprintf( "%.9f %s", $st[9], $_ );
        }
        else { print STDERR "stat: $_: $!"; }
    }
' {} + |

sort -nrt ' ' -k1,1 |

sed -e "1,${n}d" -e 's/[^ ]* //' |

perl -l -ne '
    s/\\([0-7]{3})/chr(oct($1))/ge;
    s/(["\n])/"\\$1"/g;
    print "\"$_\""; 
' |

xargs -E '' sh -c '[ "$#" -gt 0 ] && rm -f "$@"' sh

Пояснения:

  • Для каждого найденного файла первый получает время модификации и выводит его вместе с закодированным именем файла (каждыйnewlineиbackslashсимволы заменяются литералами\012и\134соответственно).

  • Теперь каждыйtime filenameгарантированно будет однострочным, поэтому POSIXsortиsedможно спокойно работать с этим потоком.

  • Второйperlдекодирует имена файлов и экранирует их для POSIX.

  • Наконец, призывает удалить файлы. shкоманда — это уловка, которая предотвращаетxargsот бегаrmкогда нет файлов для удаления.

Игнорирование перевода строки игнорирует безопасность и хорошее кодирование. У wnoise был единственный хороший ответ. Вот вариант его, который помещает имена файлов в массив $x

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

Я понимаю, что это старая ветка, но, возможно, кто-то от этого выиграет. Эта команда найдет файлы в текущем каталоге:

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

Затем уберите 4 самых последних файла из списка:

tail -n+5

Возьмите 2-й столбец (имя файла, а не метку времени):

awk '{ print $2; }'

А затем оберните все это в утверждение for:

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

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

Если имена файлов не имеют пробелов, это будет работать:

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 скрипт, который удалит их правильно

С зш

Предполагая, что вам нет дела до существующих каталогов, и у вас будет не более 999 файлов (выберите большее число, если хотите, или создайте цикл while).

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

В *(.om[6,999]), . означает, что файлы o означает порядок сортировки, m значит по дате внесения изменений (поставить a за время доступа или c для изменения индекса), [6,999] выбирает диапазон файла, поэтому сначала не нужно 5

Адаптация отличного ответа @mklement0 с некоторыми параметрами и без необходимости перехода к папке, содержащей файлы, которые нужно удалить...

      TARGET_FOLDER="/my/folder/path"
FILES_KEEP=5
ls -tp "$TARGET_FOLDER"**/* | grep -v '/$' | tail -n +$((FILES_KEEP+1)) | xargs -d '\n' -r rm --

[Ссылка (ы).: https://stackoverflow.com/a/3572628/3223785]

Спасибо!

Мне нужно было элегантное решение для busybox (маршрутизатора), все решения для xargs или array были для меня бесполезны - такой команды там не было. find и 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', Не то чтобы мы когда-либо создавали файлы с такими именами в реальной жизни!

Примечание. Если бы мы хотели передать переменную sh таким образом, мы бы просто изменили печать (простая форма, без пробелов):

print "VarName="$1

установить переменную VarName к стоимости $1, Несколько переменных могут быть созданы за один раз. это VarName становится обычной переменной sh и впоследствии может использоваться в скрипте или оболочке. Итак, чтобы создать переменные с помощью awk и вернуть их обратно в оболочку:

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

Удаляет все, кроме 10 последних (большинство последних) файлов

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

Если менее 10 файлов, ни один файл не будет удален, и у вас будет: error head: недопустимое количество строк - 0

Посчитать файлы с помощью bash

Нашел интересный 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

Начиная с SC2010 — ShellCheck Wiki предупреждает о синтаксическом анализеlsвывод плохой, я предлагаю простой однострочник сstatбез загадочногоawk/sedсценарий:

      stat -c '%y %n' /path/* | sort -r | tail -n +2 | cut -d' ' -f4 | xargs -r rm -v --

tail's+2Параметр контролирует, сколько самых новых файлов осталось, т.е. не было удалено. Для входного значенияNу тебя осталосьN-1из новейших файлов.

Модифицированная версия ответа @Fabien, если вы хотите указать путь. Полезно, если вы запускаете скрипт в другом месте.

ls -tr /path/foo/ | head -n -5 | xargs -I% --no-run-if-empty rm /path/foo/%

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

Я сделал это в скрипт оболочки bash. Использование: 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

Запуск на Debian (предположим, что то же самое на других дистрибутивах, которые я получаю: rm: не могу удалить каталог `..'

что довольно раздражает..

Во всяком случае я поправил выше, а также добавил grep в команду. В моем случае у меня есть 6 файлов резервных копий в каталоге, например, file1.tar file2.tar file3.tar и т. Д., И я хочу удалить только самый старый файл (удалить первый файл в моем случае)

Сценарий, который я запустил для удаления самого старого файла:

ls -C1 -t | grep файл | awk 'NR>5'|xargs rm

Это (как и выше) удаляет первый из моих файлов, например, file1.tar, это также остается с file2 file3 file4 file5 и file6

Другие вопросы по тегам