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

StackOverflow https://stackoverflow.com/questions/25785

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение

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

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

ответ wnoise решает эти проблемы, но решение ГНУ-специфический (и довольно сложный).

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

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

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

Вышеупомянутое неэффективный, потому что xargs должен вызвать rm один раз за каждый имя файла.
Ваша платформа xargs может позволить вам решить эту проблему:

Если у вас есть ГНУ xargs, использовать -d '\n', что делает xargs рассматривать каждую строку ввода как отдельный аргумент, но передавать столько аргументов, сколько поместится в командной строке однажды:

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

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

Если у вас есть БСД xargs (в том числе на ОС Х), вы можете использовать -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; обратите внимание, что это не будет работать должным образом, если имена файлов содержат символы новой строки и/или каталог содержит подкаталоги.

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

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

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

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

Если в именах файлов есть пробелы, что-то вроде

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

Основная логика:

  • получить список файлов в порядке времени, один столбец
  • получить все, кроме первых 5 (в этом примере n=5)
  • первая версия:отправь это в РМ
  • вторая версия:создайте скрипт, который удалит их правильно

С зш

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

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

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

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

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

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

awk '{ print $2; }'

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

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

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

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

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

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

Если файлов меньше 10, ни один файл не удаляется, и у вас будет:заголовок ошибки:неправильное количество строк -- 0

Для подсчета файлов с помощью bash

Мне нужно было элегантное решение для busybox (маршрутизатора), все решения по xargs или массивам были для меня бесполезны — такой команды там не было.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"
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 (предположим, в других дистрибутивах то же самое, я получаю:РМ:невозможно удалить каталог `..'

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

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

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

ls -c1 -t | Греп -файл | awk 'nr> 5' | xargs rm

Это (как указано выше) удаляет первый из моих файлов, например.file1.tar это также остается с файлом2 файл3 файл4 файл5 и файл6

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top