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


87

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

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

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

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

mv original original

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


Кстати, я знаю, что есть команда переименования, но мне бы очень хотелось выяснить, как это сделать с помощью sed, чтобы в будущем я мог выполнять более мощные команды.
opsb


Ответы:


32

Это происходит потому, что 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и множество кавычек, чтобы сделать все это в одной команде, но я не собираюсь пробовать.)


27
Для тех, кто интересуется неправильным использованием sh -c, вот он: find spec -name "* _test.rb" -exec sh -c 'echo mv "$ 1" "$ (echo" $ 1 "| sed s / test.rb \ $ / spec.rb /) "'_ {} \;
opsb

1
@opsb какого черта это _? отличное решение - но мне нравится рамтам отвечать больше :)
iRaS

Ура! Избавил меня от многих головных болей. Для полноты картины я передаю его скрипту следующим образом: find. -name "file" -exec sh /path/to/script.sh {} \;
Sven M.

131

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


9
К сожалению, у этого есть проблемы с пустым пространством. Таким образом, использование с папками, в названии которых есть пробелы, приведет к поломке его на xargs (подтвердите с помощью -p для подробного / интерактивного режима)
cde

1
Это именно то, что я искал. Жалко для проблемы с пустым пространством (хотя я не тестировал). Но для моих текущих нужд он идеален. Я бы посоветовал сначала протестировать его с помощью «echo» вместо «mv» в качестве параметра в «xargs».
Мишель Далл'Агата,

5
Если вам нужно иметь дело с пробелами в путях и вы используете GNU sed> = 4.2.2, вы можете использовать эту -zопцию вместе с находками -print0и xargs -0:find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Эван Пуркхизер,

Лучшее решение. Намного быстрее, чем найти -exec. Спасибо
Miguel A. Baldi Hörlle

Это не сработает, если testна одном пути есть несколько папок. sedпереименует только первый, и mvкоманда завершится No such file or directoryошибкой.
Кейси

22

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

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

Похоже, это хороший способ сделать это. Я действительно хочу взломать один лайнер, чтобы улучшить свои знания больше, чем что-либо еще.
opsb

2
для файла в $ (find. -name "* _test.rb"); сделать echo mv $ file echo $file | sed s/_test.rb$/_spec.rb/; done является однострочным, не так ли?
Bretticus

5
Это не сработает, если у вас есть имена файлов с пробелами. forразделит их на отдельные слова. Вы можете заставить его работать, указав цикл for разделять только символы новой строки. См. Примеры на cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html .
onitake

Я согласен с @onitake, хотя предпочел бы использовать -execопцию find.
ShellFish

18

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

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

Привет, я думаю, что ' _test.rb ' должен быть ' _test.rb' (двойные кавычки в одиночные кавычки). Могу я спросить, почему вы используете подчеркивание, чтобы подтолкнуть аргумент, который вы хотите разместить в $ 1, когда мне кажется, что это find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;работает ? Как быfind . -name '*_test.rb' -exec bash -c 'echo mv $1 ${1/test.rb/spec.rb}' iAmArgumentZero {} \;
agtb

Спасибо за ваши предложения, исправлено
csg

Спасибо, что прояснили это - я прокомментировал это только потому, что некоторое время размышлял о значении _, думая, что это может быть хитрое использование $ _ ('_' довольно сложно найти в документации!)
agtb

9

Вы можете сделать это без 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

это не работает ( sedтот), как объясняется принятым ответом.
Али

@Ali, действительно работает - сам тестировал, когда писал ответ. @ объяснение larsman не распространяется на for i in... ; do ... ; done, который выполняет команды с помощью оболочки и делает понять кавычку.
Уэйн Конрад

9

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

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

2

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

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

2

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

Большое спасибо! Это помогло мне легко рекурсивно удалить завершающий .gz из всех имен файлов. while IFS= read -r file; do mv $file ${file%.gz}; done < <(find -type f -name "*.gz")
Vinay Vissh

1
@CasualCoder приятно это читать :) Обратите внимание, можно прямо сказать find .... -exec mv .... Также будьте осторожны, $fileтак как он завершится неудачно, если содержит пробелы. Лучше использовать цитаты "$file".
fedorqui 'SO stop harming'

1

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

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

1

В ответе 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

1

У меня нет духа делать это снова и снова, но я написал это в ответ на 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 printfand 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
    • Начинаем 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загрузкой файлов в несуществующие места, и сводит к минимуму необходимость в рекурсивном цикле. ( на самом деле, вам может быть трудно найти цикл вообще )
  • 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«s -printfкоманды функции , как необходимо правильно изменить все ссылки $ {OLD} до $ {NEW}, но для того , чтобы сделать это , мы должны были использовать некоторые произвольные ориентиры, которые не следует включать в окончательный результат. Итак, как только sedзакончится все, что ему нужно сделать, мы даем ему указание стереть свои контрольные точки из буфера удержания перед тем, как передать его.

И теперь мы вернулись

read получит команду, которая выглядит так:

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

Это будет readего в ${msg}качестве ${sh_io}которой может быть рассмотрена по желанию за пределы функции.

Круто.

-Майк


1

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

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

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

1

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

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

0
$ 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, кроме как поместить логику в сценарий оболочки и вызвать это в exec. изначально не видел требования использовать sed
Damodharan R

0

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

#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
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 .

Как sedработает без побега, ()если не установить -rопцию?
mikeserv

0

Более безопасный способ переименования с помощью типа регулярного выражения 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:]]+$//'`' {} +

0

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



Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.