Рекурсивно переименовывать файлы с помощью 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 73

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

Я нахожу этот короче

find . -name '*_test.rb' -exec bash -c 'echo mv  ${0/test.rb/spec.rb}' {} \;

вы можете сделать это без 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}. Это позволяет избежать возможных ошибок, связанных с mving файлы в несуществующих местах, и это сводит к минимуму необходимость для рекурсивного цикла. (на самом деле, вам может быть трудно найти петлю вообще)
  • 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 будет обрабатывать пустые места в путях.