Рекурсивно переименовывать файлы с помощью find и sed
Я хочу пройти через кучу каталогов и переименовать все файлы, которые заканчиваются в _test.rb, чтобы закончить в _spec.РБ вместо этого. Это то, что я никогда не понимал, как делать с Башем, поэтому на этот раз я подумал, что приложу некоторые усилия, чтобы его прибить. Я до сих пор придумал короткий, хотя, мои лучшие усилия:
find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` ;
NB: есть дополнительное Эхо после exec, так что команда печатается вместо запуска, пока я ее тестирую.
когда я запускаю его выход для каждого совпадающего имени файла:
mv original original
т. е. замена на sed была потеряна. В чем тут хитрость?
18 ответов:
это происходит потому, что
sed
получает строку{}
как входной сигнал, как можно проверить с:find . -exec echo `echo "{}" | sed 's/./foo/g'` \;
, который печатает
foofoo
для каждого файла в каталоге, рекурсивно. Причина такого поведения заключается в том, что конвейер выполняется один раз оболочкой, когда она разворачивает всю команду.нет никакого способа процитировать
sed
трубопровод таким образом, чтоfind
будет выполнять его для каждого файла, так какfind
не выполняет команды через оболочку и не имеет понятия о трубопроводах или обратных цитатах. Руководство GNU findutils объясняет, как выполнить аналогичную задачу, поместив конвейер в отдельный сценарий оболочки:#!/bin/sh echo "" | sed 's/_test.rb$/_spec.rb/'
(там может быть какой-то извращенный способ использования
sh -c
и тонна цитат, чтобы сделать все это в одной команде, но я не собираюсь пытаться.)
чтобы решить это как можно более близко к оригиналу проблема будет, наверное, использование команды xargs "аргументы в командной строке" вариант:
find . -name *_test.rb | sed -e "p;s/test/spec/" | xargs -n2 mv
он находит файлы в текущем рабочем каталоге рекурсивно, повторяет исходное имя файла (
p
) и затем измененное имя (s/test/spec/
) и кормит всеmv
пар (xargs -n2
). Помните, что в этом случае сам путь не должен содержать строкуtest
.
вы можете рассмотреть другой способ, как
for file in $(find . -name "*_test.rb") do echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/` done
вы можете сделать это без 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
Вы упомянули, что вы используете
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
(the
globstar
опция shell гарантирует, что bash найдет все соответствующие*_test.rb
файлы, независимо от того, насколько глубоко они вложены в иерархию каталогов... используйтеhelp shopt
чтобы узнать, как установить опцию)
самый простой способ:
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/
для этого вам не нужно
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}
удаляет самый короткий соответствующий шаблон "строка" из$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
если у вас есть Ruby (1.9+)
ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
в ответе рамтама, который мне нравится, часть find работает нормально, но остальная часть не работает, если путь имеет пробелы. Я не слишком знаком с sed, но я смог изменить этот ответ:
find . -name "*_test.rb" | perl -pe 's/^((.*_)test.rb)$/"" "spec.rb"/' | xargs -n2 mv
мне действительно нужно было изменить это, потому что в моем случае использования последняя команда больше похожа на
find . -name "olddir" | perl -pe 's/^((.*)olddir)$/"" "new directory"/' | xargs -n2 mv
у меня нет сердца, чтобы сделать все это снова, но я написал это в ответ Командная Строка Найти Sed Exec. Там спрашивающий хотел знать, как переместить все дерево, возможно, исключая каталог или два, и переименовать все файлы и каталоги, содержащие строку "старые" вместо того, чтобы содержать "новый".
кроме того описанием как с кропотливой детализации ниже, этот метод может быть уникальный в том, что он включает в себя встроенную отладку. Он в основном ничего не делает вообще, как написано, кроме компиляции и сохранения в переменную всех команд, которые он считает, что он должен сделать, чтобы выполнить запрошенную работу.
это тоже явно избегает петель как можно больше. Кроме того,
sed
рекурсивный поиск более чем одного совпадения pattern насколько я знаю, другой рекурсии нет.и последнее, это совершенно
null
с разделителями-он не срабатывает на любой символ в любом имени файла, кромеnull
. Я не думаю, что тебе стоит.кстати, это действительно быстро. Смотри:
% _mvnfind() { mv -n "" "" && cd "" > read -r SED <<SED > :;s|\(.*/[^/]*\)||;t;:;s|\(.*\)||;t;s|^[0-9]*[\t]\(mv.*\)||p > SED > find . -name "**" -printf "%d\tmv %P %P0" | > sort -zg | sed -nz ${SED} | read -r > echo <<EOF > Prepared commands saved in variable: > To view do: printf | tr "0" "\n" > To run do: sh <<EORUN > $(printf | tr "0" "\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 "0" "\n" \ > | wc - ; echo ${sh_io} | tr "0" "\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
- все дерево перемещается с самого начала. Это избавит вас от головной боли, поверьте мне. Все остальное, что вы хотите сделать - переименование - это просто метаданные файловой системы. Если бы вы, например, перемещали это с одного диска на другой, или через границы файловой системы любого рода, вам лучше сделать это сразу с одной командой. Это также безопаснее. Обратите внимание на
-noclobber
параметр установлен дляmv
; как написано, Эта функция не будет ставить${SRC_DIR}
где a${TGT_DIR}
уже существует.read -R SED <<HEREDOC
- я нашел все команды sed здесь, чтобы сэкономить на спасении от неприятностей и прочитать их в переменную для подачи в sed ниже. Объяснение ниже.
find . -name ${OLD} -printf
- мы начинаем на основе
%directory-depth
так что сначала обрабатываются пути, ближайшие по отношению к ${SRC}. Это позволяет избежать возможных ошибок, связанных сmv
ing файлы в несуществующих местах, и это сводит к минимуму необходимость для рекурсивного цикла. (на самом деле, вам может быть трудно найти петлю вообще)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
. В первом, как мы уже видели, мы модифицируем предоставленныйfind
' s-printf
команда функции по мере необходимости правильно изменить все ссылки ${OLD} на ${NEW}, но для этого нам пришлось использовать некоторые произвольные опорные точки, которые не должны быть включены в окончательный вывод. Так что один разsed
завершает все, что ему нужно сделать, мы поручаем ему стереть свои опорные точки из буфера удержания, прежде чем передавать его.И ТЕПЕРЬ МЫ СНОВА ВОКРУГ
read
получите команду, которая выглядит так:% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE 0
это
read
на${msg}
как${sh_io}
которое можно рассмотреть по желанию вне функция.прохладный.
-Майк
я смог обрабатывать имена файлов с пробелами, следуя примеры предложил onitake.
этой не перерыв, если путь содержит пробелы или строку
test
:find . -name "*_test.rb" -print0 | while read -d $'' file do echo mv "$file" "$(echo $file | sed s/test/spec/)" done
Это пример, который должен работать во всех случаях. Работает рекурсивно, нужна просто оболочка, а также поддержка имен файлов с пробелами.
find spec -name "*_test.rb" -print0 | while read -d $'' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done
вот что сработало для меня, когда имена файлов имели пробелы в них. Пример ниже рекурсивно переименовывает все .дар-файлы .zip-файлы:
find . -name "*.dar" -exec bash -c 'mv "" "`echo \"\" | sed s/.dar/.zip/`"' {} \;
$ find spec -name "*_test.rb" spec/dir2/a_test.rb spec/dir1/a_test.rb $ find spec -name "*_test.rb" | xargs -n 1 /usr/bin/perl -e '($new=$ARGV[0]) =~ s/test/spec/; system(qq(mv),qq(-v), $ARGV[0], $new);' `spec/dir2/a_test.rb' -> `spec/dir2/a_spec.rb' `spec/dir1/a_test.rb' -> `spec/dir1/a_spec.rb' $ find spec -name "*_spec.rb" spec/dir2/b_spec.rb spec/dir2/a_spec.rb spec/dir1/a_spec.rb spec/dir1/c_spec.rb
ваш вопрос, похоже, касается sed, но для достижения вашей цели рекурсивного переименования я бы предложил следующее, бесстыдно вырванное из другого ответа, который я дал здесь:рекурсивное переименование в bash
#!/bin/bash IFS=$'\n' function RecurseDirs { for f in "$@" do newf=echo "${f}" | sed -e 's/^(.*_)test.rb$/spec.rb/g' echo "${f}" "${newf}" mv "${f}" "${newf}" f="${newf}" if [[ -d "${f}" ]]; then cd "${f}" RecurseDirs $(ls -1 ".") fi done cd .. } RecurseDirs .
более безопасный способ переименования с помощью find utils и sed тип регулярного выражения:
mkdir ~/practice cd ~/practice touch classic.txt.txt touch folk.txt.txt
удалить ".формат txt.txt " расширение следующим образом -
cd ~/practice find . -name "*txt" -execdir sh -c 'mv "" `echo "" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} \;
Если вы используете + вместо ; для работы в пакетном режиме, приведенная выше команда переименует только первый соответствующий файл, но не весь список совпадений файлов по "find".
find . -name "*txt" -execdir sh -c 'mv "" `echo "" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} +
вот хороший oneliner, который делает трюк. Sed не может справиться с этим правильно, особенно если несколько переменных передаются xargs с-n 2. Замена bash будет легко справляться с этим, например:
find ./spec -type f -name "*_test.rb" -print0 | xargs -0 -I {} sh -c 'export file={}; mv $file ${file/_test.rb/_spec.rb}'
Adding-type-f ограничит операции перемещения только файлами, - print 0 будет обрабатывать пустые места в путях.