Вопрос или проблема
Я хотел удалить все расширения .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
опции недавних версий GNUls
(последняя поддается обработке только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
для более безопасной и эффективной работы.