Комбинируйте команды find -printf и find -exec grep -l.

Вопрос или проблема

Как я могу find файлы, содержащие конкретный контент (grepable), и отсортировать найденные файлы по времени последнего изменения? (Я хочу выбрать только один самый новый файл)

Эта команда находит файлы:

find subfolder/ -maxdepth 1 -type f -exec grep -l 'blue_wizards' {} \;`

Эта команда находит файлы и сортирует их:

find subfolder/ -maxdepth 1 -type f -printf "%T+ %p\n" | sort

Но если я предоставлю -printf, ввод для -exec grep будет не таким, каким я хочу.

Было бы проще использовать zsh и его квалификаторы глобов:

grep -le blue_wizards -- subfolder/*(D.Om)

Где:

  • D также включает скрытые файлы, как find по умолчанию
  • . ограничивает до обычных файлов (как -type f)
  • Om сортирует в обратном порядке по возрасту, как ls -rt с самым старым файлом первым

Вы можете сделать то же самое с использованием GNU find, cut и xargs с любой оболочкой, похожей на Bourne, используя:

find subfolder/ -maxdepth 1 -type f -printf '%T@\t%p\0' |
  LC_ALL=C sort -zn |
  cut -zf2- |
  xargs -r0 grep -le blue_wizards --

С более старыми версиями GNU cut, возможно, вам потребуется использовать tr '\0\n' '\n\0' | cut -f2- | tr '\0\n' '\n\0' или обратиться к GNU sed, который, как и GNU sort, поддерживает -z гораздо дольше: LC_ALL=C sed -z 's/^[^:]*://' (и изменить \t на : в аргументе -printf).

Обратите внимание, что в любом случае -printf специфичен для GNU (-maxdepth также является расширением GNU, хотя теперь поддерживается большим количеством реализаций find).

%T@ лучше, чем %T+, так как это избегает вычисления календарной даты для соответствующего времени модификации (mtime), но также более важно, потому что это работает правильно при изменениях по зимнему времени (DST). Если использовать %T+ для сортировки, вы будете хотеть зафиксировать TZ на UTC0, например, для получения времен UTC, не затронутых DST.

Сравните:

$ ls --full-time -ldrt a b
-rw-r--r-- 1 stephane stephane 13 2020-10-25 01:43:28.989189400 +0100 b
-rw-r--r-- 1 stephane stephane 13 2020-10-25 01:31:20.112312300 +0000 a
$ find a b -printf '%T+ %T@ %p\n' | sort
2020-10-25+01:31:20.1123123000 1603589480.1123123000 a
2020-10-25+01:43:28.9891894000 1603586608.9891894000 b
$ TZ=UTC0 find a b -printf '%T+ %T@ %p\n' | sort
2020-10-25+00:43:28.9891894000 1603586608.9891894000 b
2020-10-25+01:31:20.1123123000 1603589480.1123123000 a

a новее b, хотя он кажется старше с %T+, так как он был изменен после перехода на зимнее время в моем часовом поясе (Европа/Лондон здесь).


Чтобы получить новейший файл, вы, вероятно, захотите инвертировать этот порядок (измените Om на om и sort на sort -r) и заставить grep выводить список файлов с разделителем NUL, а не с разделителем новой строки, чтобы его можно было надежно обработать (так как новая строка является таким же допустимым символом в имени файла, как и любой другой), для чего вам понадобится GNU grep или совместимый и принять первый.

Например, чтобы сохранить путь к новейшему файлу в переменной $newest (и вернуть статус выхода с ошибкой, если нет соответствующего файла):

zsh + GNU grep:

grep --null -le blue_wizards -- subfolder/*(D.om) |
  IFS= read -rd '' newest

bash + GNU find + GNU sort + GNU cut + GNU xargs + GNU grep:

IFS= read -rd '' newest < <(
  find subfolder/ -maxdepth 1 -type f -printf '%T@\t%p\0' |
    LC_ALL=C sort -rzn |
    cut -zf2- |
    xargs -r0 grep --null -le blue_wizards --
)

Чтобы получить все из них (новейшие первыми) в массив:

zsh + GNU grep:

files=(${(0)"$(grep --null -le blue_wizards -- subfolder/*(D.om))"})
print -r newest: $files[1]

bash 4.4+ + GNU find + GNU sort + GNU cut + GNU xargs + GNU grep:

readarray -td '' files < <(
  find subfolder/ -maxdepth 1 -type f -printf '%T@\t%p\0' |
    LC_ALL=C sort -rzn |
    cut -zf2- |
    xargs -r0 grep --null -le blue_wizards --
)
printf '%s\n' "newest: ${files[0]}"

Если вы хотите сделать это по POSIX, то есть, используя только стандартный синтаксис sh и утилит, это будет невозможно сделать без некоторых предположений о том, какие байтовые значения могут содержать имена файлов. Если вы можете гарантировать, что имена файлов не содержат символы новой строки и являются достаточно короткими, вы сможете сделать следующее:

dir=subfolder
file=$(
  export LC_ALL=C
  CDPATH= cd -P -- "$dir" &&
    ls -At |
      sed 's/"/"\\""/g; s/.*/"&"/; s|^|./|' |
      xargs -E '' sh -c '
        [ "$#" -eq 0 ] || 
          find "$@" -type f -exec grep -l blue_wizards {} +' sh |
      head -n 1
)
[ -n "$file" ] && newest="$dir/$file"

Если вы можете сделать еще больше предположений, например, имена файлов также содержат только буквенно-цифровые символы, ., - из переносимого набора символов, не начинаются с -, все являются обычными файлами, $CDPATH не установлен, $dir не содержит компонентов .., не начинается с - или +:

dir=subfolder
file=$(
  cd "$dir" &&
    ls -At |
      xargs grep -l blue_wizards |
      head -n 1
)
[ -n "$file" ] && newest="$dir/$file"

Самым простым может быть объединение обеих команд find следующим образом:

find subfolder/ -maxdepth 1 -type f \
    -exec grep -q 'blue_wizards' {} \; \
    -printf "%T+ %p\n" | sort

Дело в том, что -exec ; может использоваться как условие, оно истинно или ложно, в зависимости от статуса выхода команды. Также grep -q для получения только статуса выхода, и никакого вывода.

Это требует создания копии grep для каждого файла, так что это не совсем оптимально в этом плане. -exec {} + не работает таким же образом: вы не можете получить статус выхода для отдельных файлов таким образом, так что это всегда оценивается как истинное. Также с -printf "%T+ %p\n" у вас будут проблемы, если какие-либо имена файлов содержат символы новой строки.

что если вы просто передадите в grep?

find subfolder/ -maxdepth 1 -type f -printf “%T+ %p\n” | grep -iE ‘blue_wizards’ (или ‘.blue_wizards.‘ для полного выделения строки)

Ответ или решение

Для решения задачи поиска файлов, содержащих определенный текст, с последующей сортировкой по времени последнего изменения и выбором самого нового файла, мы можем комбинировать команды find, grep и sort. В контексте запроса, давайте рассмотрим, как можно реализовать это на практике, избегая стандартных ошибок и учитывая нюансы Unix-систем.

Решение задачи с использованием стандартных инструментов Unix

Чтобы найти файлы, которые содержат строку 'blue_wizards', и отсортировать их по времени последнего изменения, мы используем GNU find, sort и xargs. Вот пошаговое объяснение:

  1. Команда поиска файлов с использованием find и grep:

    find subfolder/ -maxdepth 1 -type f -exec grep -l 'blue_wizards' {} \;

    Этот код находит файлы в указанной папке, содержащие текст 'blue_wizards'. Опция -exec позволяет выполнить команду grep для каждого найденного файла, где -l указывает выводить только имена файлов.

  2. Сортировка файлов по времени последнего изменения:

    find subfolder/ -maxdepth 1 -type f -printf '%T@ %p\n' | sort -r

    Здесь мы используем опцию -printf с find для вывода времени последнего изменения файла как числа секунд с момента эпохи UNIX. Это позволяет корректно обрабатывать изменения времени и сортировать через sort -r (обратная сортировка).

  3. Комбинация двух подходов для оптимизации:

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

    find subfolder/ -maxdepth 1 -type f \
       -exec grep -q 'blue_wizards' {} \; \
       -printf '%T@ %p\n' | sort -r | head -n 1 | cut -d ' ' -f2-
    • -exec grep -q используется для проверки наличия текста в файле, не выводя результат.
    • Результаты отфильтровываются и сортируются по времени изменения.
    • head -n 1 берет только первый (самый новый) файл в списке, а cut -d ' ' -f2- отделяет имя файла от временной метки.

Альтернативный подход с Zsh

Если у вас есть возможность использовать Zsh, можно воспользоваться его мощной системой глобов:

newest_file=$(grep -le blue_wizards -- subfolder/*(D.om[1]))
  • (D.om[1]) — эти квалификаторы заставляют Zsh искать файлы, включая скрытые (D), отобрать только обычные файлы (.), и сортировать их в порядке изменения (om).

Заключение

Представленные подходы позволяют эффективно и быстро находить файлы и сортировать их. Каждый из них имеет свои особенности и требует определенных настроек системы, будь то современный bash с поддержкой GNU-утилит или zsh. Важно также помнить, что для лучшей совместимости с различными Unix-системами необходимо учитывать переносимость кода, в зависимости от доступных утилит и используемой файловой системы.

Оцените материал
Добавить комментарий

Капча загружается...