Почему эта команда xargs не работает?

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

Я хотел удалить все расширения .sh, поэтому сделал это:

ls *.sh | xargs -I {} mv {} `basename {} .sh`

Однако это не работает, он ведет себя так, будто basename возвращает неизмененное имя файла.

Почему это происходит?

Например, это работает:

ls *.sh | xargs -I {} echo `basename {}.jpg .jpg`;

ИЗМЕНЕНИЕ:

Решение: одинарные кавычки предотвращают оценивание `basename ...` оболочкой перед выполнением команды.

ls *.sh | xargs -I {}  sh -c 'mv {} `basename {} .sh`'

Проблема в том, что команда basename выполняется до выполнения пайплайна. Чтобы это заработало, вам нужно, чтобы xargs выполнял basename, и вы можете сделать это с помощью sh -c, например:

ls *.sh | xargs -L1 sh -c 'basename $1 .sh' dummy

Примечания:

  • Если не сказать xargs, куда вставить имена файлов, они будут добавлены в конец командной строки.
  • Вы должны использовать ключ -L1 или его эквивалент, чтобы xargs передавал только один аргумент в sh.
  • Использование вывода ls может иметь нежелательные последствия.

Редактировать

Удалены устаревшие параметры, спасибо TechZilla

Я не уверен в вашем вопросе, вы действительно спрашиваете, почему ваша первая строка не работает? Или ищете правильный способ переименовать все файлы .sh?

Предполагая, что это самый чистый метод, я предпочитаю готовить параметры команд перед xargs.
Для удаления расширения .sh я часто рассматриваю этот подход,

 find -maxdepth 1 -type f -iname '*.sh'  | sed 'p;s_.sh$__' | xargs -L2 mv

ls *.sh | xargs -I {} mv {} `basename {} .sh`

В этой команде есть множество ошибок. Самая очевидная – это то, что basename {} .sh будет выполнен первым оболочкой, которая выведет {}, так что команда, выполняемая во второй части пайплайна, будет xargs -I {} mv {} {}, но также:

  • отсутствует опция -d у ls, так что если какие-либо из этих файлов .sh являются каталогами, то будет перечислено их содержимое.
  • отсутствует -- для всех ls, mv и basename, поэтому если любое из имен файлов начинается с -, они будут рассматриваться как опции этими утилитами.
  • Даже в ls -d -- *.sh оболочка находит неподходящие .sh файлы, сортирует их список и передает его в ls. Все ls делает, это проходит через них, сортирует снова и проверяет, что каждый существует, и выводит их разделенными новой строкой, так что это скорее бессмысленно¹.
  • xargs, даже с -I (ему было бы еще хуже без него, если бы вы не использовали опции -0/-d) обрабатывает кавычки и обратные слэши и отбрасывает ведущие пробелы. В любом случае, новая строка является такой же допустимой буквой, как и любая в имени файла, так что вывод ls не поддается постобработке, если вы не используете --quoting-style=shell-always или --zero опции недавних версий GNU ls (последняя поддается обработке только xargs).
  • Стандартный ввод mv, в зависимости от реализации xargs, будет либо /dev/null, либо конвейером, так что если mv запрашивает подтверждение, он либо получит пустой ответ (что, к счастью, трактуется как нет), либо хуже — прочитает ответ из списка файлов, предполагая, что xargs еще не прочитал его все!

Таким образом, если вы действительно хотите использовать xargs с ls и basename, вам потребуется современная система GNU и:

{
  ls -d --zero -- *.sh 3<&- | xargs -r0 sh -c '
    for file do
      mv -- "$file" "$(basename -- "$file" .sh)"
    done <&3 3<&-' sh
} 3<&0

Где нам нужно запустить оболочку, которая будет расширять $(basename...) для каждого файла.

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

for file in *.sh; do
  mv -- "$file" "${file%.sh}"
done

Или использовать инструмент, предназначенный для пакетного переименования, такой как один из многих вариантов rename, или mmv, или zsh‘s zmv:

autoload -Uz
zmv '(*).sh' '$1'

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


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

Переменные внешние данные (в данном случае имена файлов, найденные в текущем каталоге) никогда не должны расширяться (здесь с помощью xargs, который расширяет {} плейсхолдер) без проверки на безопасность в кодовый аргумент интерпретатора языка (здесь sh, но то же самое относится к perl/awk/sed…²).

В ls *.sh | xargs -I {} sh -c 'mv {} `basename {} .sh`, если, например, существует файл с именем $(reboot).sh, это выполнит sh с mv $(reboot).sh `basename $(reboot).sh .sh` в качестве аргумента c и вызовет reboot (дважды).


¹ Единственное, что здесь полезно, это для оболочек (таких как bash, когда опция failglob не включена), у которых по-прежнему есть этот дефект, при котором шаблоны без совпадений передаются как есть. Если *.sh не совпадает ни с одним файлом, в некоторых оболочках будет передан литерал *.sh в ls, и ls сообщит об ошибке, что файл *.sh не существует, и не выведет ничего на stdout, что, пока вы используете -r в xargs, предотвратит выполнение mv.

² Некоторые из них более опасны, чем другие. Очевидно, sh/awk/perl могут выполнять любые команды, так может и GNU sed, хотя любой sed также может w писать в файлы. В другой крайности спектра, bc/dc могут причинить не так много вреда.

.

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

Команда xargs в вашем примере не работает, потому что она вызывает basename до того, как будет выполнена команда xargs. Давайте разберемся с этим подробнее.

Проблема с командой

Оригинальная команда выглядит так:

ls *.sh | xargs -I {} mv {} `basename {} .sh`

На первый взгляд, эта команда пытается перечислить все файлы с расширением .sh, а затем переименовать их, убирая это расширение. Однако она имеет несколько критических недостатков.

1. Эвалюация аргументов

Когда вы используете обратные кавычки (`) для вызова basename, они выполняются до выполнения команды xargs. Таким образом, Shell пытается выполнить basename с заменой {}, но так как {} не имеет значения до выполнения xargs, происходит ошибка. В результате команда mv получает неправильные аргументы.

2. Ограничение по использования ls

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

3. Потенциальные проблемы с файлами

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

4. Уязвимость к выполнению команд

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

Решение проблемы

Для того чтобы команда работала корректно, вы можете использовать sh -c. Корректная команда будет выглядеть вот так:

ls *.sh | xargs -I {} sh -c 'mv "$1" "$(basename "$1" .sh)"' _ {}

Здесь мы вызываем sh -c, который интерпретирует команду и использует переменную $1 для доступа к имени файла.

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

Как альтернатива, для переименования файлов лучше всего использовать цикл for в оболочке. Например:

for file in *.sh; do
  mv -- "$file" "${file%.sh}"
done

Этот способ безопаснее и более читаем, поскольку не требует использования xargs или внешних команд.

Заключение

При работе с командами, которые обрабатывают имена файлов, важно учитывать безопасность и правильность их выполнения. Используйте простой for цикл или find в сочетании с exec для более безопасной и эффективной работы.

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

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