Мой подход:
find . -depth -name "* *" -execdir bash -c 'pwd; for f in "$@"; do mv -nv "$f" "${f// /_}"; done' dummy {} +
Многострочная версия для удобства чтения:
find . -depth -name "* *" -execdir \
bash -c '
pwd
for f in "$@"; do
mv -nv "$f" "${f// /_}"
done
' dummy {} +
Объяснение:
find . -name "* *" находит объекты, которые необходимо переименовать. Обратите внимание, что find очень гибок в своих тестах, поэтому, если вы хотите (например) переименовать только каталоги, начните с find . -depth -type d -name "* *" .
-execdir выполняет данный процесс (bash) в каталоге, в котором находится объект, поэтому любой путь, переданный {} , всегда имеет вид ./bar , а не ./foo/bar . Это означает, что нам не нужно заботиться обо всем пути. Недостатком является то, что mv -v не будет показывать вам путь, поэтому я добавил pwd только для информации (вы можете опустить его, если хотите).
bash позволяет нам использовать синтаксис "${baz// /_}" .
-depth гарантирует, что следующее не произойдет: команда find переименовывает каталог (если применимо), а затем пытается обработать его содержимое по старому пути.
{} + может кормить bash несколькими объектами (вопреки синтаксису {} \;). Мы перебираем их с помощью for f in "$@" . Дело не в том, чтобы запускать отдельный процесс bash для каждого объекта, поскольку создание нового процесса обходится дорого. Я думаю, что мы не можем легко избежать запуска отдельных mv s; тем не менее, сокращение количества вызовов bash кажется хорошей оптимизацией (pwd является встроенным в bash и не стоит нам процесса). Однако -execdir ... {} + не будет передавать файлы из разных каталогов вместе. Используя -exec ... {} + вместо -execdir ... {} + мы можем еще больше сократить количество процессов, но тогда нам нужно заботиться о путях, а не только о именах файлов (сравните этот другой ответ , кажется, делать достойную работу, но while read замедляет его). Это вопрос скорости против (относительной) простоты. Мое решение с -exec ниже.
dummy раз перед тем, как {} станет $0 внутри нашего bash . Нам нужен этот фиктивный аргумент, потому что "$@" эквивалентно "$1" "$2" ... (не "$0" "$1" ...). Таким образом, все, что передано {} , доступно позже как "$@" .
Более сложная, слегка оптимизированная версия (различные трюки ${...} взяты из другого ответа):
find . -depth -name "* *" -exec \
bash -c '
for f in "$@"; do
n="${f##*/}"
mv -nv "$f" "${f%/*}/${n// /_}"
done
' dummy {} +
Другой (экспериментальный!) Подход предполагает vidir. Хитрость в том, что vidir использует $EDITOR который может не быть интерактивным редактором:
find . -name "* *" | EDITOR='sed -i s/\d32/_/g' vidir -
Предостережения:
- Это не удастся для имен файлов / каталогов со специальными символами (например, новые строки).
- Мы не можем использовать
s/ /_/g напрямую, \d32 - это обходной путь.
- Из-за того, как работает
vidir , такой подход будет сложным, если вы захотите заменить цифру или табуляцию.
- Здесь
vidir работает с путями, а не только с именами файлов (базовыми именами), поэтому переименование только файлов (т.е. не каталогов) может быть затруднено.
Тем не менее, если вы знаете, что делаете, это может сделать работу еще быстрее. Я не рекомендую такое ( AB ) использование vidir в общем случае, хотя. Я включил его в свой ответ, потому что я нашел этот экспериментальный подход интересным.