Рекурсивно переименовывать файлы, используя find и sed - PullRequest
76 голосов
/ 25 января 2011

Я хочу просмотреть несколько каталогов и переименовать все файлы, заканчивающиеся на _test.rb, заканчивающиеся на _spec.rb.Это то, чего я никогда не понимал, как поступить с bash, поэтому на этот раз я подумал, что приложу некоторые усилия, чтобы добиться этого.Хотя я пока что не понял, мои лучшие усилия:

find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` \;

Примечание: после exec есть дополнительное эхо, так что команда печатается вместо запуска, пока я ее тестирую.

Когда я запускаю его, для каждого совпавшего имени файла выводится:

mv original original

, то есть замена на sed была потеряна.В чем прикол?

Ответы [ 19 ]

112 голосов
/ 29 июля 2012

Для ее решения наиболее близким к исходной проблеме, вероятно, следует использовать опцию xargs «args per command line»:

find . -name *_test.rb | sed -e "p;s/test/spec/" | xargs -n2 mv

Он рекурсивно находит файлы в текущем рабочем каталоге, повторяетисходное имя файла (p), а затем измененное имя (s/test/spec/) и все его данные передаются в mv парами (xargs -n2).Помните, что в этом случае сам путь не должен содержать строку test.

32 голосов
/ 25 января 2011

Это происходит потому, что sed получает строку {} в качестве входных данных, что можно проверить с помощью:

find . -exec echo `echo "{}" | sed 's/./foo/g'` \;

, который печатает foofoo для каждого файла в каталоге, рекурсивно. Причина такого поведения заключается в том, что конвейер выполняется оболочкой один раз, когда он разворачивает всю команду.

Нет способа заключить в кавычки конвейер sed таким образом, чтобы find выполнял его для каждого файла, поскольку find не выполняет команды через оболочку и не имеет представления о конвейерах или обратных кавычках. Руководство по GNU findutils объясняет, как выполнить подобную задачу, поместив конвейер в отдельный скрипт оболочки:

#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'

(Может быть какой-то извращенный способ использования sh -c и тонны кавычек, чтобы сделать все это в одной команде, но я не буду пытаться.)

23 голосов
/ 25 января 2011

вы можете рассмотреть другой способ, например

for file in $(find . -name "*_test.rb")
do 
  echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done
17 голосов
/ 16 октября 2011

Я считаю это короче

find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
9 голосов
/ 27 января 2011

Вы упоминаете, что вы используете bash в качестве оболочки, и в этом случае вам на самом деле не нужны find и sed для достижения пакетного переименования, которое вы хотите ...

Предполагая, что вы используете bash в качестве оболочки:

$ echo $SHELL
/bin/bash
$ _

... и предполагаете, что вы включили так называемый параметр оболочки globstar:

$ shopt -p globstar
shopt -s globstar
$ _

...и, наконец, при условии, что вы установили утилиту rename (находится в пакете util-linux-ng)

$ which rename
/usr/bin/rename
$ _

... тогда вы можете добиться пакетного переименования в bash one-liner следующим образом:

$ rename _test _spec **/*_test.rb

(опция оболочки globstar гарантирует, что bash найдет все соответствующие файлы *_test.rb, независимо от того, насколько глубоко они вложены в иерархию каталогов ... используйте help shoptчтобы узнать, как установить опцию)

9 голосов
/ 25 января 2011

Вы можете сделать это без sed, если хотите:

for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done

${var%%suffix} полоски suffix от значения var.

или, чтобы сделать это с помощью sed:

for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
5 голосов
/ 13 мая 2015

Самый простой способ :

find . -name "*_test.rb" | xargs rename s/_test/_spec/

Самый быстрый способ (при условии, что у вас 4 процессора):

find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/

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

Вы можете проверить лимит вашей системы, используя getconf ARG_MAX

В большинстве систем Linux вы можете использовать free -b или cat /proc/meminfo, чтобы узнать, сколько ОЗУ вам нужно для работы; В противном случае используйте top или приложение для мониторинга системной активности.

Более безопасный способ (при условии, что у вас есть 1000000 байт оперативной памяти для работы):

find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/
2 голосов
/ 07 июля 2016

Вот что сработало для меня, когда в именах файлов были пробелы. В приведенном ниже примере рекурсивно переименовываются все файлы .dar в файлы .zip:

find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.zip/`"' {} \;
2 голосов
/ 08 июля 2016

Для этого вам не нужно sed.Вы можете прекрасно справиться с циклом while, получающим результат от find до замены процесса .

Так что если у вас есть выражение find, которое выбирает нужные файлы, затем используйте синтаксис:

while IFS= read -r file; do
     echo "mv $file ${file%_test.rb}_spec.rb"  # remove "echo" when OK!
done < <(find -name "*_test.rb")

Это будет find файлов и переименовать все из них, чередуя строку _test.rb с конца и добавляя _spec.rb.

Для этого шагамы используем Расширение параметров оболочки , где ${var%string} удаляет кратчайший соответствующий шаблон "string" из $var.

$ file="HELLOa_test.rbBYE_test.rb"
$ echo "${file%_test.rb}"          # remove _test.rb from the end
HELLOa_test.rbBYE
$ echo "${file%_test.rb}_spec.rb"  # remove _test.rb and append _spec.rb
HELLOa_test.rbBYE_spec.rb

См. пример:

$ tree
.
├── ab_testArb
├── a_test.rb
├── a_test.rb_test.rb
├── b_test.rb
├── c_test.hello
├── c_test.rb
└── mydir
    └── d_test.rb

$ while IFS= read -r file; do echo "mv $file ${file/_test.rb/_spec.rb}"; done < <(find -name "*_test.rb")
mv ./b_test.rb ./b_spec.rb
mv ./mydir/d_test.rb ./mydir/d_spec.rb
mv ./a_test.rb ./a_spec.rb
mv ./c_test.rb ./c_spec.rb
1 голос
/ 11 декабря 2013

У меня нет смелости сделать это снова, но я написал это в ответ на Командная строка Find Sed Exec .Там спрашивающий хотел знать, как переместить все дерево, возможно, исключая один или два каталога, и переименовать все файлы и каталоги, содержащие строку "OLD" , чтобы вместо нее содержать "NEW" .

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

Он также явно избегает циклов как можно больше.Кроме рекурсивного поиска sed для более чем одного совпадения с шаблоном , насколько мне известно, другой рекурсии не существует.

И наконец, это полностью разделено null - оно не срабатывает ни по одному символу в любом имени файла, кроме null.Я не думаю, что вы должны иметь это.

Кстати, это ДЕЙСТВИТЕЛЬНО быстро.Смотрите:

% _mvnfind() { mv -n "${1}" "${2}" && cd "${2}"
> read -r SED <<SED
> :;s|${3}\(.*/[^/]*${5}\)|${4}\1|;t;:;s|\(${5}.*\)${3}|\1${4}|;t;s|^[0-9]*[\t]\(mv.*\)${5}|\1|p
> SED
> find . -name "*${3}*" -printf "%d\tmv %P ${5} %P\000" |
> sort -zg | sed -nz ${SED} | read -r ${6}
> echo <<EOF
> Prepared commands saved in variable: ${6}
> To view do: printf ${6} | tr "\000" "\n"
> To run do: sh <<EORUN
> $(printf ${6} | tr "\000" "\n")
> EORUN
> EOF
> }
% rm -rf "${UNNECESSARY:=/any/dirs/you/dont/want/moved}"
% time ( _mvnfind ${SRC=./test_tree} ${TGT=./mv_tree} \
> ${OLD=google} ${NEW=replacement_word} ${sed_sep=SsEeDd} \
> ${sh_io:=sh_io} ; printf %b\\000 "${sh_io}" | tr "\000" "\n" \
> | wc - ; echo ${sh_io} | tr "\000" "\n" |  tail -n 2 )

   <actual process time used:>
    0.06s user 0.03s system 106% cpu 0.090 total

   <output from wc:>

    Lines  Words  Bytes
    115     362   20691 -

    <output from tail:>

    mv .config/replacement_word-chrome-beta/Default/.../googlestars \
    .config/replacement_word-chrome-beta/Default/.../replacement_wordstars        

ПРИМЕЧАНИЕ: Приведенное выше function, вероятно, потребует GNU версий sed и find для правильной обработки find printf и sed -z -eи :;recursive regex test;t звонки.Если они вам недоступны, функциональность может быть дублирована с небольшими изменениями.

Это должно делать все, что вы хотели от начала до конца с очень небольшой суетой.Я сделал fork с sed, но я также практиковал некоторые sed рекурсивные методы ветвления, поэтому я здесь.Я думаю, это похоже на стрижку со скидкой в ​​парикмахерской.Вот рабочий процесс:

  • rm -rf ${UNNECESSARY}
    • Я специально исключил любой функциональный вызов, который может удалить или уничтожить данные любого рода.Вы упоминаете, что ./app может быть нежелательным.Удалите его или перенесите в другое место заранее, или, в качестве альтернативы, вы можете встроить подпрограмму \( -path PATTERN -exec rm -rf \{\} \) в find, чтобы сделать это программно, но это все ваше.
  • _mvnfind "${@}"
    • Объявите свои аргументы и вызовите рабочую функцию.${sh_io} особенно важно в том смысле, что сохраняет результат от функции.${sed_sep} приходит через секунду;это произвольная строка, используемая для ссылки на рекурсию sed в функции.Если для ${sed_sep} установлено значение, которое потенциально может быть найдено в любом из ваших путей или имен файлов, с которыми вы работали ... ну, просто не позволяйте этому быть.
  • mv -n $1 $2
    • Все дерево перемещается с самого начала.Это сэкономит много головной боли;поверь мне.Остальное, что вы хотите сделать - переименование - это просто вопрос метаданных файловой системы.Если вы, например, переносили это с одного диска на другой или пересекали границы файловой системы любого рода, лучше сделать это сразу с помощью одной команды.Это также безопаснее.Обратите внимание на параметр -noclobber, установленный для mv;как написано, эта функция не будет помещать ${SRC_DIR} туда, где ${TGT_DIR} уже существует.
  • read -R SED <<HEREDOC
    • Я разместил здесь все команды sed для сохранения наизбегать неприятностей и читать их в переменную, чтобы подать в sed ниже.Объяснение ниже.
  • find . -name ${OLD} -printf
    • Мы начинаем процесс findfind мы ищем только то, что нужно переименовать, потому что мы уже выполнили все операции с местами на месте mv с первой командой функции.Вместо того, чтобы предпринимать какие-либо прямые действия с find, как, например, вызов exec, мы вместо этого используем его для динамического построения командной строки с -printf.
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • После того, как find найдет нужные нам файлы, он непосредственно создаст и распечатает ( большинство ) команды, которая потребуется нам для обработки вашего переименования.%dir-depth, прикрепленный к началу каждой строки, поможет убедиться, что мы не пытаемся переименовать файл или каталог в дереве с родительским объектом, который еще не был переименован.find использует всевозможные методы оптимизации для обхода дерева вашей файловой системы, и не уверен, что он вернет нужные нам данные в безопасном для операций порядке.Вот почему мы затем ...
  • sort -general-numerical -zero-delimited
    • Мы сортируем все выходные данные find на основе %directory-depth так, чтобы пути, ближайшие котношения с $ {SRC} работают в первую очередь.Это позволяет избежать возможных ошибок, связанных с mv переносом файлов в несуществующие места, и минимизирует необходимость в рекурсивном цикле.( на самом деле, вам может быть трудно вообще найти петлю )
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • Я думаю, что это единственныйцикл во всем сценарии, и он зацикливается только на второй %Path, напечатанном для каждой строки, в случае, если он содержит более одного значения $ {OLD}, которое, возможно, потребуется заменить.Все другие решения, которые я себе представлял, включали в себя второй sed процесс, и хотя короткий цикл может быть нежелателен, он определенно опережает порождение и разветвление всего процесса.
    • Так что в основном sed делает здесь поискЗатем $ {sed_sep}, найдя его, сохраняет его и все встреченные символы до тех пор, пока не найдет $ {OLD}, который затем заменяет на $ {NEW}.Затем он возвращается к $ {sed_sep} и снова ищет $ {OLD}, если это встречается в строке более одного раза.Если он не найден, он печатает измененную строку в stdout (которую затем снова перехватывает) и завершает цикл.
    • Это избавляет от необходимости разбора всей строки и гарантирует, что первая половина командной строки mv, которая должна включать, конечно, $ {OLD}, включает ее, а вторая половина изменяетсястолько раз, сколько необходимо, чтобы стереть имя $ {OLD} из пути назначения mv.
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • Два вызова -execздесь без секунды fork.В первом, как мы видели, мы модифицируем команду mv, предоставленную функциональной командой find -printf, по мере необходимости, чтобы должным образом изменить все ссылки $ {OLD} на $ {NEW}, но вДля этого нам пришлось использовать несколько произвольных опорных точек, которые не должны быть включены в окончательный результат.Поэтому, как только sed завершит все, что ему нужно, мы даем указание стереть свои контрольные точки из буфера хранения, прежде чем передать его дальше.

И СЕЙЧАС НАЗАД В ТЕЧЕНИЕ

read получит команду, которая выглядит следующим образомэто:

% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000

Это read превратится в ${msg} в ${sh_io}, что можно проверить по желанию вне функции.

Cool.

-Майк

...