Создание пайпа от ls к mv

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

Я пытаюсь создать небольшую утилиту для себя в командной строке – для заданного текущего рабочего каталога я хотел бы найти самый недавно добавленный файл и mv его в имя, заданное аргументом.

Я пытался достичь этого с помощью:

ls -tr | tail -n 1 | xargs -0 -J % mv % SQL_warning_2.png

… однако bash сообщает:

mv: переименовать Screenshot 2020-06-22 в 17.53.23.png в SQL_warning_2.png: Нет такого файла или каталога

Я считаю, что это означает, что xargs разделяет аргументы по пробелам в имени файла: Screenshot 2020-06-22 at 17.53.23.png, но, насколько мне известно, для ls нет флага -print0.

Если некоторые из моих предположений неверны, прошу прощения – я очень не знаком с xargs.

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

ls -tr | tail -n 1 | xargs -I % mv -- % SQL_warning_2.png

Флаг -I % заставит xargs вызывать утилиту один раз на каждую строку, прочитанную с заменой % на текст этой строки. В отличие от этого, -J % делит строку по пробелам:

$ echo 1 2 3 | xargs -J {} printf '"%s"\n' {}
"1"
"2"
"3"
$ echo 1 2 3 | xargs -I {} printf '"%s"\n' {}
"1 2 3"

Вы хотите переименовать самый недавно измененный файл в текущем каталоге. Это можно сделать с помощью оболочки zsh следующим образом:

mv -- *(.om[1]) SQL_warning_2.png

Это вызовет mv с именем самого недавно измененного обычного файла в качестве первого аргумента. Это om[1] в квалификаторе glob сортирует список имен по меткам времени mtime (от самого нового к самому старому) и выбирает первый. Предшествующая точка выбирает только обычные файлы (не каталоги и т.д.)

Из bash:

zsh -c 'mv -- *(.om[1]) SQL_warning_2.png'

Я бы сделал что-то подобное:

mv -- "$(ls -t|head -n1)" new_filename

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

Да, с -0 xargs ожидает список, разделенный NUL, на stdin.

Здесь вы кормите его частью имени самого нового файла после последнего символа новой строки в нем, за которым следует символ новой строки (этот символ новой строки добавляется ls и не является частью имени файла).

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

Здесь вам нужно убедиться, что ls выводит список, разделенный NUL, вместо списка, разделенного новой строкой, для чего вам нужна GNU-версия ls начиная с версии 9.0 или новее и использование его опции --zero.

Либо вам нужно вернуться к формату ввода по умолчанию для xargs (без -0), где аргументы разделяются пробелами (список которых зависит от реализации xargs и, возможно, локали) или новыми строками, и где "..." и '...' и \ используются для экранирования этих символов и друг друга (в другом способе, нежели операторы кавычек в той же оболочке). Поскольку некоторые реализации xargs пытаются интерпретировать свой ввод как текст, но имена файлов могут содержать любые байтовые значения (кроме NUL и /), вам также нужно будет выполнить эту обработку в C локали.

export LC_ALL=C
ls -td ./* | awk '
  {gsub(/"/, "\"\\\"\"")} # экранировать "
  NR == 1 {printf "\"%s", $0; next}
  /\// {exit} # / означает начало второго файла
  {printf "\"\\\n\"%s", $0} # экранировать новые строки
  END {if (NR) print "\""}' |
  xargs -J % mv % newname

Как видите, надежное использование xargs – это настоящая головная боль. Некоторые реализации xargs также имеют очень низкий предел на размер аргументов или строк ввода. Обратите внимание, что -J – это не переносимое расширение BSD.

Работать с произвольными именами файлов, используя утилиты на основе строк, такие как ls, также очень сложно.

Лучше всего использовать zsh, который может самостоятельно сортировать список файлов по времени изменения, как показал @Kusalananda:

mv -- *(.om[1]) newname

В bash вы также можете сделать:

IFS= read -rd / newest < <(ls -td ./*) && newest=${newest%.}
newest=${newest%?} # удалить новую строку
[ -n "$newest" ] && mv "$newest" newname

(также будет работать в ksh93 или zsh). Как и в предыдущем подходе с xargs, мы используем ./*, чтобы понимать, на какой строке начинается второй файл в списке.

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

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

Решение с использованием bash:

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

# Сначала получаем имя самого нового файла
newest_file=$(ls -t1 | head -n 1)

# Проверяем, найден ли файл и перемещаем его
if [[ -n "$newest_file" ]]; then
  mv -- "$newest_file" "SQL_warning_2.png"
else
  echo "Файл не найден."
fi

Объяснение:

  1. Используем команду ls -t1, которая выводит файлы, отсортированные по времени последнего изменения, где -t сортирует их от новых к старым, а -1 обеспечивает вывод каждого имени файла на отдельной строке.
  2. Затем с помощью head -n 1 получаем самое верхнее имя файла.
  3. Проверяем, было ли получено имя файла. Если да, используем команду mv, чтобы переместить его в новое имя SQL_warning_2.png. Флаг -- перед именем файла позволяет избежать проблем с именами, которые могут начинаться с дефисов.

Альтернативное решение с использованием find:

Если вы хотите более гибкое решение, которое также включает подкаталоги, используйте find:

newest_file=$(find . -type f -printf '%T@ %p\n' | sort -n | tail -n 1 | cut -d' ' -f2-)

if [[ -n "$newest_file" ]]; then
  mv -- "$newest_file" "SQL_warning_2.png"
else
  echo "Файл не найден."
fi

Объяснение:

  1. Команда find . -type f -printf '%T@ %p\n' находит все файлы в текущем каталоге и подкаталогах, вывода время последней модификации в числовом формате, за которым следует имя файла.
  2. Сортируем этот список по времени, используя sort -n.
  3. С помощью tail -n 1 находим последний файл (самый новый) и выделяем его имя с помощью cut -d' ' -f2-.
  4. Перемещаем его, как и в предыдущем примере.

Оба подхода корректно обрабатывают имена файлов, содержащие пробелы и специальные символы, благодаря использованию кавычек. Убедитесь, что подаваемое имя для перемещения SQL_warning_2.png не конфликтует с уже существующим файлом, так как команда mv перезапишет его без каких-либо предупреждений.

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

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