Как получить последние N файлов в каталоге?

15

У меня есть много файлов, которые упорядочены по имени файла в каталоге. Я хочу скопировать последние N (скажем, N = 4) файлов в мой домашний каталог. Как я должен это делать?

cp ./<the final 4 files> ~/

Sibbs Gambling
источник
1
Во всех ответах ниже вам может потребоваться добавить «LC_ALL = ...» перед командами, использующими диапазоны, чтобы их диапазоны использовали правильную локаль для вас (локали могут измениться, а затем порядок символов может измениться). Например, в некоторых локалях [az] может содержать все буквы алфавита, кроме «Z», поскольку буквы упорядочены: aAbBcC .... zZ. Существуют другие варианты (акцентированные буквы и даже более экзотические порядки Обычный выбор:LC_ALL=C
Оливье Дюлак

Ответы:

23

Это легко сделать с помощью массивов bash / ksh93 / zsh:

a=(*)
cp -- "${a[@]: -4}" ~/

Это работает для всех не скрытых имен файлов, даже если они содержат пробелы, символы табуляции, новые строки или другие сложные символы (при условии, что в текущем каталоге есть как минимум 4 не скрытых файла с bash ).

Как это устроено

  • a=(*)

    Это создает массив aсо всеми именами файлов. Имена файлов, возвращаемые bash, сортируются в алфавитном порядке. (Я предполагаю, что это то, что вы подразумеваете под «упорядоченным по имени файла». )

  • ${a[@]: -4}

    Это возвращает последние четыре элемента массива a(при условии, что массив содержит как минимум 4 элемента с bash).

  • cp -- "${a[@]: -4}" ~/

    Это копирует последние четыре имени файла в ваш домашний каталог.

Копировать и переименовывать одновременно

Это скопирует последние четыре файла только в домашний каталог и в то же время добавит строку a_к имени скопированного файла:

a=(*)
for fname in "${a[@]: -4}"; do cp -- "$fname" ~/a_"$fname"; done

Скопируйте из другого каталога, а также переименуйте

Если мы используем a=(./some_dir/*)вместо a=(*), тогда у нас есть проблема присоединения каталога к имени файла. Одним из решений является:

a=(./some_dir/*) 
for f in "${a[@]: -4}"; do cp "$f"  ~/a_"${f##*/}"; done

Другое решение - использовать подоболочку и cdкаталог в подоболочке:

(cd ./some_dir && a=(*) && for f in "${a[@]: -4}"; do cp -- "$f" ~/a_"$f"; done) 

После завершения подоболочки оболочка возвращает нас в исходный каталог.

Убедиться, что порядок соответствует

Вопрос просит файлы "упорядоченные по имени файла". Этот порядок, отмечает Оливье Дюлак в комментариях, будет варьироваться от одного региона к другому. Если важно иметь фиксированные результаты, не зависящие от настроек компьютера, то лучше всего указывать локаль явно при определении массива a. Например:

LC_ALL=C a=(*)

Вы можете узнать, в какой локали вы находитесь в данный момент, запустив localeкоманду.

John1024
источник
9

Если вы используете, zshвы можете заключить в скобки ()список так называемых глобальных квалификаторов, которые выбирают нужные файлы. В вашем случае это было бы

cp *(On[1,4]) ~/

Здесь Onимена файлов сортируются в алфавитном порядке в обратном порядке и [1,4]занимают только первые 4 из них.

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

cp -- *(.On[1,4]) ~

Добавьте Dклассификатор, если вы также хотите рассмотреть скрытые файлы (dot-файлы):

cp -- *(D.On[1,4]) ~
jimmij
источник
2
Или: *([-4,-1]).
Стефан Шазелас
2
@ StéphaneChazelas да, давайте уточним для будущих читателей, что сортировка в алфавитном порядке в обычном направлении ( on) является поведением по умолчанию, поэтому указывать это не нужно, и -перед цифрами отсчитывает имена файлов снизу.
Джимми
7

Вот решение с использованием чрезвычайно простых команд bash.

find . -maxdepth 1 -type f | sort | tail -n 4 | while read -r file; do cp "$file" ~/; done

Объяснение:

find . -maxdepth 1 -type f

Находит все файлы в текущем каталоге.

sort

Сортирует по алфавиту

tail -n 4

Показывать только последние 4 строки.

while read -r file; do cp "$file" ~/; done

Зацикливается на каждой строке, выполняя команду копирования.

gswong
источник
6

Пока вы находите, что сортировка оболочки приемлема, вы можете просто сделать:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
cp "$@" /path/to/target/dir

Это очень похоже на bashпредлагаемое решение для конкретного массива, но должно переноситься на любую POSIX-совместимую оболочку. Некоторые заметки об обоих методах:

  1. Важно, чтобы вы приводили свои cpаргументы w / cp --или вы получали одну из .точек или /заголовок каждого имени. Если вам не удастся сделать это, вы рискуете первым аргументом первой -черты в cpначале, который может быть интерпретирован как опция и может привести к сбою операции или к непредвиденным результатам.

    • Даже если вы работаете с текущим каталогом в качестве исходного каталога, это легко сделать как ... set -- ./*или array=(./*).
  2. При использовании обоих методов важно, чтобы в вашем массиве arg было как минимум столько элементов, сколько вы пытаетесь удалить - я делаю это здесь с математическим расширением. Это shiftпроисходит только в том случае, если в массиве arg есть по крайней мере 4 элемента, а затем удаляются только те первые аргументы, которые дают излишек в 4 ..

    • Так что в set 1 2 3 4 5случае это сместит 1прочь, но в set 1 2 3случае это ничего не сместит.
    • Например: a=(1 2 3); echo "${a[@]: -4}"печатает пустую строку.

Если вы копируете из одного каталога в другой, вы можете использовать pax:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
pax -rws '|.*/|new_prefix|' "$@" /path/to/target/dir

... который применил бы sedподстановку -style ко всем именам файлов при их копировании.

mikeserv
источник
4

Если есть только файлы и их имена не содержат пробелов, $IFSсимволов новой строки (и вы не изменили ) или символов глобуса (или некоторых непечатаемых символов с некоторыми реализациями ls) и не начинаются с ., то вы можете сделать это :

cp -- $(ls | tail -n 4) ~/
Хауке Лагинг
источник
Это называется заменой команды, и это действительно удобно. tldp.org/LDP/abs/html/commandsub.html#CSPARENS
iyrin
Благодарность! Оно работает. Можно ли быстро добавить префикс a_ко ВСЕМ четырем файлам? Я пытался echo "a_$(ls | tail -n 4)", но это только prepends первый файл.
Sibbs Gambling
@SibbsGambling Мой подход не подходит для этого, но ответ John1024 может быть легко адаптирован к этому.
Хауке Лагинг
5
В общем случае lsвывод синтаксического анализа считается дурным тоном, поскольку обычно он приводит к ужасным ошибкам в именах файлов, которые содержат не только пробелы, но также символы табуляции, новые строки или другие допустимые, но "сложные" символы.
HBruijn
2
@HaukeLaging Даже если в настоящее время это не проблема (потому что у меня нет «сложных» огненных имен в моем каталоге), я мог бы через 2 года, и вдруг все
встало
1

Это простое решение bash без какого-либо цикла кода. Скопируйте последние 4 файла из текущего каталога в место назначения:

$ find . -maxdepth 1 -type f |tail -n -4|xargs cp -t "$destdir"
yasin_alm
источник
1
Как вы гарантируете, что findотдаете файлы в нужном порядке? Как вы гарантируете, что файлы, содержащие пробельные символы (включая символы новой строки) в их именах, обрабатываются правильно?
Кусалананда