Рекурсивно переименовывать файлы с помощью find и sed

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

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

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

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

mv original original

то есть. замена sed была потеряна. Что за трюк?

Ответ 1

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

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

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

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

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

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

Ответ 2

Чтобы решить проблему, наиболее близкую исходной проблеме, вероятно, будет использоваться опция 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.

Ответ 3

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

for file in $(find . -name "*_test.rb")
do 
  echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done

Ответ 4

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

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

Ответ 5

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

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

${var%%suffix} strips suffix от значения var.

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

for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done

Ответ 6

Вы упомянули, что используете 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 с одним слоем следующим образом:

$ rename _test _spec **/*_test.rb

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

Ответ 7

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

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 байт RAM для работы):

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

Ответ 8

если у вас есть Ruby (1.9 +)

ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'

Ответ 9

В ответе ramtam, который мне нравится, часть поиска работает нормально, но в остальном нет, если путь имеет пробелы. Я не очень хорошо знаком с sed, но я смог изменить этот ответ на:

find . -name "*_test.rb" | perl -pe 's/^((.*_)test.rb)$/"\1" "\2spec.rb"/' | xargs -n2 mv

Мне действительно нужно было изменить это, потому что в моем случае конечная команда больше похожа на

find . -name "olddir" | perl -pe 's/^((.*)olddir)$/"\1" "\2new directory"/' | xargs -n2 mv

Ответ 10

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

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

Он также явно избегает циклов как можно больше. Насколько я знаю, кроме рекурсивного поиска 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}, где a ${TGT_DIR} уже существует.
  • read -R SED <<HEREDOC
    • Я нашел здесь все команды sed, чтобы сэкономить на избегающих стычках и прочитать их в переменной, которая будет подаваться в sed ниже. Пояснение ниже.
  • find . -name ${OLD} -printf
    • Начнем процесс find. С find мы ищем только все, что нужно переименовать, потому что мы уже выполнили все операции "место-в-месте" 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 ing в несуществующие местоположения и минимизирует необходимость для рекурсивного цикла. (на самом деле вам может быть трудно найти петлю вообще)
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • Я думаю, что это единственный цикл во всем script, и он перебирает только второй %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}, который может быть исследован вне функции.

Круто.

-Mike

Ответ 11

Мне удалось обрабатывать имена файлов с пробелами, следуя примерам, предложенным onitake.

Этот не ломается, если путь содержит пробелы или строку test:

find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
    echo mv "$file" "$(echo $file | sed s/test/spec/)"
done

Ответ 12

Это пример, который должен работать во всех случаях. Работает recursiveley, Нужна просто оболочка и поддерживает имена файлов с пробелами.

find spec -name "*_test.rb" -print0 | while read -d $'\0' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done

Ответ 13

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

find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.zip/`"' {} \;

Ответ 14

Для этого вам не нужно 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

Ответ 15

$ 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

Ответ 16

Ваш вопрос, кажется, о sed, но для достижения вашей цели рекурсивного переименования я предлагаю следующее, бесстыдно разорванное из другого ответа, который я дал здесь: рекурсивное переименование в bash

#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "[email protected]"
do
  newf=echo "${f}" | sed -e 's/^(.*_)test.rb$/\1spec.rb/g'
    echo "${f}" "${newf}"
    mv "${f}" "${newf}"
    f="${newf}"
  if [[ -d "${f}" ]]; then
    cd "${f}"
    RecurseDirs $(ls -1 ".")
  fi
done
cd ..
}
RecurseDirs .

Ответ 17

Более безопасный способ переименования с помощью 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 "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} \;

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

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} +

Ответ 18

Здесь хороший 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}'

Добавление -type -f ограничивает операции перемещения только файлами, -print 0 будет обрабатывать пустые пространства в путях.