Рекурсивно перейменовуйте файли за допомогою find та sed


85

Я хочу переглядати купу каталогів і замінювати всі файли, які закінчуються на _test.rb, на _spec.rb. Це те, чого я ніколи не розумів, як робити баш, тому цього разу я подумав, що доклав би зусиль, щоб його прибити. Я поки що прийшов короткий, хоча, мої найкращі зусилля:

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, ось воно: знайдіть специфікаційне ім'я "* _test.rb" -exec sh -c 'echo mv "$ 1" "$ (echo" $ 1 "| sed s / test.rb \ $ / spec.rb /) "'_ {} \;
opsb

1
@opsb для чого це чорт? чудове рішення - але мені більше подобається ramtam відповісти :)
iRaS

На здоров’я! Врятував мені багато головних болів. Для повноти ось як я передаю це сценарію: find. -назва "файл" -exec sh /path/to/script.sh {} \;
Свен М.

128

Щоб вирішити це способом, найбільш близьким до вихідної проблеми, можливо, було б використовувати параметр 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та ксаргами -0:find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Еван Пурхізер

Найкраще рішення. Набагато швидше, ніж find -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
для файлу в $ (знайти. -назву "* _test.rb"); робити echo mv $ файл echo $file | sed s/_test.rb$/_spec.rb/; готово - це однокласний, чи не так?
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команду, яка виконує команди через оболонку і не розуміє зворотного тику .
Wayne Conrad

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 prestani шкодити'

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 . Там запитувач хотів знати, як перемістити ціле дерево, можливо, за винятком каталогу або двох, і перейменувати всі файли та каталоги, що містять рядок "СТАРИЙ", а не "НОВИЙ" .

Окрім опису того, як із кропіткою багатослівністю нижче, цей метод може бути унікальним тим, що включає вбудовану налагодження. По суті, він взагалі нічого не робить, як написано, крім компіляції та збереження у змінну всіх команд, які, на її переконання, повинен робити для виконання запитуваної роботи.

Він також явно уникає циклів , наскільки це можливо. Окрім 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', -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

Ось хороший 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 оброблятиме порожні пробіли у шляхах.



Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.