Як зробити рекурсивну знаходження / заміну рядка на awk або sed?


675

Як знайти та замінити кожне виникнення:

subdomainA.example.com

з

subdomainB.example.com

у кожному текстовому файлі під /home/www/деревом каталогів рекурсивно?


93
Порада: Не виконайте наведеного нижче в дереві оформлення замовлення svn ... це замінить чарівні файли папок .svn.
Дж. Полфер

7
о мій боже, це саме те, що я тільки що зробив. Але це спрацювало і, здається, не завдало ніякої шкоди. Що найгірше, що могло статися?
J. Katzwinkel

5
@ J.Katzwinkel: як мінімум, це може пошкодити контрольні суми, що може пошкодити ваше сховище.
ninjagecko

3
Короткий підказ для всіх людей, які використовують sed: Він додасть нові файли у ваші файли. Якщо ви не хочете їх, спочатку знайдіть заміну, яка нічого не відповідає, і виконайте це git. Тоді зробіть справжній. Потім інтерактивно перезавантажте та видаліть перше.
funroll

5
Ви можете виключити каталог, наприклад , як мерзотник, з результатів, використовуючи -path ./.git -prune -oв find . -path ./.git -prune -o -type f -name '*matchThisText*' -print0перед тим конвеєру до xargs
devinbost

Відповіді:


850
find /home/www \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i 's/subdomainA\.example\.com/subdomainB.example.com/g'

-print0говорить findнадрукувати кожен з результатів, розділених нульовим символом, а не новим рядком. У тому випадку, коли у вашому каталозі є файли з новими рядками в іменах, це все ще дозволяє xargsпрацювати над правильними іменами.

\( -type d -name .git -prune \)це вираз, який повністю пропускає всі названі каталоги .git. Ви можете легко розширити його, якщо ви використовуєте SVN або маєте інші папки, які хочете зберегти - просто збігайтесь із кількома іменами. Це приблизно еквівалентно -not -path .git, але більш ефективно, тому що замість перевірки кожного файлу в каталозі він цілком пропускає його. -oПісля нього вимагається , тому як на -pruneсамому ділі працює.

Для отримання додаткової інформації див man find.


132
На OSX ви можете зіткнутися з sed: 1: "...": invalid command code .проблемою. Здається, що опція -i очікує розширення та розбору 's/../...'команди. Рішення: передати розширення '' до -i варіант типу sed -i '' 's/....
Роберт Луйо

6
Примітка: якщо ви використовуєте це над каталогом і цікавитеся, чому svn stне відображаються зміни, це тому, що ви також змінили файли в каталогах .svn! Використовуйте find . -maxdepth 1 -type f -print0 | xargs -0 sed -i 's/toreplace/replaced/g'замість цього.
ACK_stoverflow

57
Крім того, будьте обережні, якщо ви знаходитесь в git repo. Я подумав, що я розумний, перевіривши це на чіткій гілці, щоб я міг повернутися, якщо це щось погано, але натомість пошкодив мій індекс git.
Ciryon

13
Використовуйте це, grep -r 'hello' -l --null . | xargs -0 sed -i 's#hello#world#g'щоб уникнути редагування непов'язаних файлів (sed може змінити кодування файлу).
caiguanhao

6
"а натомість пошкодив мій індекс git." Не надто хвилюйтеся з цього приводу, що ви можете просто зробити, find .git ... | ... 'sed -i s/(the opposite from before)/g'щоб виправити свій індекс git
Massey101,

259

Примітка . Не запускайте цю команду у папці, що включає репост git - зміни в .git можуть пошкодити ваш індекс git.

find /home/www/ -type f -exec \
    sed -i 's/subdomainA\.example\.com/subdomainB.example.com/g' {} +

У порівнянні з іншими відповідями тут, це простіше, ніж більшість і використовує sed замість perl, саме про це і задавали оригінальне запитання.


50
Зауважте, що якщо ви використовуєте BSD sed (включаючи Mac OS X), вам потрібно надати явний порожній аргумент рядка для -iпараметра sed . тобто: sed -i '' 's/original/replacement/g'
Натан Крейк

2
@JohnZwinck Моя помилка, пропущено +. Як не дивно, рішення Нікіти для мене працює швидше.
Сем

6
@AoeAoe: +значно зменшує кількість sedпороджених процесів. Це більш ефективно.
Джон Цвінк

4
Як я можу це безпечно зробити у папці з git repo?
Хатшепсут

20
Це безпечно виконати на папці , що містить Git репо , якщо виключити репо з результатів знайти: find . -not -path '*/\.git*' -type f ....
Дейл Андерсон

210

Для мене найпростіший спосіб

grep -rl oldtext . | xargs sed -i 's/oldtext/newtext/g'

1
@Anatoly: лише одне питання: як я можу виключити бінарні файли (файли виконуваних файлів) ?
користувач2284570

3
@ user2284570 Використовуйте прапорці -Iабо --binary-file=without-matchgrep.
Zéychin

34
Це особливо добре працює, коли вам потрібно виключити каталоги, як, наприклад, з .svn. Наприклад:grep -rl oldtext . --exclude-dir=.svn | xargs sed -i 's/oldtext/newtext/g'
фіат

11
brew install gnu-sedі використовуйте gsedOSX, щоб уникнути болю в світі.
П я

1
Хлопці , будь ласка , увагу, якщо ваш проект мерзотник версірован, використовуйте замість цього: git grep -rl oldtext . | xargs sed -i 's/oldtext/newtext/g'. зовсім не приємно f * ck up your .gitdir
Paolo

61

Всі хитрощі майже однакові, але мені подобається цей:

find <mydir> -type f -exec sed -i 's/<string1>/<string2>/g' {} +
  • find <mydir>: шукати в каталозі.

  • -type f:

    Файл типу: звичайний файл

  • -exec command {} +:

    Цей варіант дії -exec виконує вказану команду для вибраних файлів, але командний рядок будується додаванням кожного вибраного імені файлу в кінці; загальна кількість викликів команди буде значно меншою, ніж кількість відповідних файлів. Командний рядок побудований приблизно так само, як xargs будує свої командні рядки. У команді дозволений лише один екземпляр `{}. Команда виконується в початковому каталозі.


@ user2284570 з -exec? Спробуйте встановити шлях до виконуваного файлу замість імені інструмента.
I159

@ I159: Ні: виключати виконувані бінарні файли (але включати сценарії оболонки) .
користувач2284570

8
@ I159 Чи не відповідає ця відповідь Джону Цвінку ?
Відновіть Моніку. Будь ласка,

1
@ user2284570 Концепція "двійкового файлу" не зовсім чітко визначена. Ви можете скористатися fileкомандою, щоб спробувати визначити тип кожного файлу, але випадкові зміни його виходу можуть бути дещо дивовижними. Варіант -I(ака --mime) дещо допомагає, або --mime-typeякщо у вас є це. Як саме відреагувати цей акуратний одношаровий проміжок, на жаль, це не виходить для цього крихітного вікна коментарів. Можливо, поставте окреме запитання, якщо вам потрібна допомога? (Можливо, додайте тут коментар із посиланням на нього.)
tripleee

1
найчистіша відповідь! подяка товариш
jukerok

39
cd /home/www && find . -type f -print0 |
  xargs -0 perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g'

2
Мені цікаво, чи є причина використовувати -print0і xargsзамість -execабо -execdir?
Філіп

4
Є: з "man find": Вказана команда виконується один раз для кожного відповідного файлу. Тобто, якщо в / home / www є 2000 файлів, то "find ... -exec ..." призведе до 2000 викликів perl; тоді як "знайти ... | xargs ... 'буде викликати perl лише один або два рази (якщо припустимо, що ARG_MAX становить близько 32 К і середня довжина імені файлу 20).
Працевлаштований росіянином

2
@Ecess Russian: саме тому ви б і використовували find -exec command {} +- це уникає зайвих викликів команди, як xargs, але без окремого процесу.
Джон Цвінк

2
На якій платформі? Рішення xargs є портативним, "магічні" виклики "find ... -exec", які не викликають підпроцес для кожного знайденого файлу, не є.
працевлаштований росіянин

4
@ElderRussian, find -exec ... {} +визначено POSIX з 2006 р.
Чарльз Даффі

34

Для мене найпростіше запам'ятати рішення https://stackoverflow.com/a/2113224/565525 , тобто:

sed -i '' -e 's/subdomainA/subdomainB/g' $(find /home/www/ -type f)

ПРИМІТКА : -i ''вирішує проблему з OSXsed: 1: "...": invalid command code .

ПРИМІТКА . Якщо у вас обробляється занадто багато файлів, ви отримаєте Argument list too long. Вирішення способу - використання find -execабо xargsрозчин, описаний вище.


4
workaroundПовинно бути кращим синтаксис у всіх випадках.
Відновіть Моніку. Будь ласка,

1
Проблема підстановки команд $(find...)полягає в тому, що немає можливості оболонці обробляти імена файлів з пробілами або іншими метахарактерами оболонки в них. Якщо ви знаєте, що це не проблема, такий підхід чудово; але у нас є занадто багато питань, коли людей не попереджали про це питання або не розуміли попередження.
трійка

30

Для всіх, хто користується пошуком срібла ( ag)

ag SearchString -l0 | xargs -0 sed -i 's/SearchString/Replacement/g'

Оскільки ag ігнорує файли / папки git / hg / svn за замовчуванням, це безпечно запускати всередині сховища.


16

Один приємний oneliner як додатковий. Використання git grep.

git grep -lz 'subdomainA.example.com' | xargs -0 perl -i'' -pE "s/subdomainA.example.com/subdomainB.example.com/g"

3
Гарна ідея, якщо ви працюєте всередині git repo, оскільки ви не ризикуєте перезаписати .git / content (як повідомляється в коментарях до іншої відповіді).
mahemoff

1
Дякую, я використовую це як функцію bash refactor() { echo "Replacing $1 by $2 in all files in this git repository." git grep -lz $1| xargs -0 perl -i'' -pE "s/$1/$2/g" }Usage, наприклад, щоб замінити 'word' на 'меч': refactor word swordпотім перевірте, з чим це робилося git diff.
Пол Рудьо

16

Щоб скоротити файли для рекурсивного sedпроходження, ви можете grepдля рядкового екземпляра:

grep -rl <oldstring> /path/to/folder | xargs sed -i s^<oldstring>^<newstring>^g

Якщо ви запустите, man grepви помітите, що ви можете також визначити --exlude-dir="*.git"прапор, якщо ви хочете пропустити пошук через .git каталоги, уникаючи проблем з індексом git, як інші ввічливо вказали.

Ведучий до:

grep -rl --exclude-dir="*.git" <oldstring> /path/to/folder | xargs sed -i s^<oldstring>^<newstring>^g

13

Цей сумісний із сховищами git та трохи простішим:

Linux:

git grep -l 'original_text' | xargs sed -i 's/original_text/new_text/g'

Мак:

git grep -l 'original_text' | xargs sed -i '' -e 's/original_text/new_text/g'

(Завдяки http://blog.jasonmeridth.com/posts/use-git-grep-to-replace-strings-in-files-in-your-git-repository/ )


Мудріше використовувати git-grep«s -zваріант разом з xargs -0.
gniourf_gniourf

git grepочевидно, має сенс лише у gitрепо. Загальна заміна була б grep -r.
трійка

@gniourf_gniourf Ви можете пояснити?
Петро Пеллер

2
@PetrPeller: з -z, git-grepрозділить вихідні поля нульовими байтами замість нових рядків; і з -0, xargsбуде читати введення, розділене нульовими байтами, а не пробілами (і не робити дивні речі з лапок). Так що, якщо ви не хочете, щоб команда перерви , якщо імена файлів містять пропуски, лапки та інші кумедні персонажі, команда: git grep -z -l 'original_text' | xargs -0 sed ....
gniourf_gniourf

10
find /home/www/ -type f -exec perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g' {} +

find /home/www/ -type f буде перераховано всі файли в / home / www / (та його підкаталогах). Прапор "-exec" повідомляє find для виконання наступної команди на кожному знайденому файлі.

perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g' {} +

це команда, запущена у файлах (багато за один раз). Отримані {}замінюються іменами файлів. В +кінці команди вказується findпобудувати одну команду для багатьох імен.

На сторінці findman: "Командний рядок побудований приблизно так само, як xargs будує свої командні рядки."

Таким чином, можна досягти своєї мети (і обробляти назви файлів, що містять пробіли) без використання xargs -0, або -print0.


8

Мені просто це було потрібно і не був задоволений швидкістю наявних прикладів. Тому я придумав своє:

cd /var/www && ack-grep -l --print0 subdomainA.example.com | xargs -0 perl -i.bak -pe 's/subdomainA\.example\.com/subdomainB.example.com/g'

Ack-grep дуже ефективний у пошуку відповідних файлів. Ця команда замінила близько 145 000 файлів вітерцем, тоді як інші зайняли стільки часу, що я не міг дочекатися їх закінчення.


Приємно, але grep -ril 'subdomainA' *ніде не так швидко, як grep -Hr 'subdomainA' * | cut -d: -f1.
trusktr

@Henno: лише одне питання: як я можу виключити бінарні файли (файли виконуваних файлів) ?
користувач2284570

ack-grep робить це автоматично для вас.
Генно

@Henno: Чи включають сценарії оболонки?
користувач2284570

Так. Ось повний перелік типів файлів, які він підтримує : yondgrep.com/documentation
Henno

6

Метод прямого переходу, якщо вам потрібно виключити каталоги ( --exclude-dir=.svn), а також можуть бути імена файлів з пробілами (використовуючи 0Byte з grep -Zіxargs -0

grep -rlZ oldtext . --exclude-dir=.svn | xargs -0 sed -i 's/oldtext/newtext/g'

6

Найпростіший спосіб заміни ( усі файли, каталог, рекурсивний )

find . -type f -not -path '*/\.*' -exec sed -i 's/foo/bar/g' {} +

Примітка: Іноді вам може знадобитися ігнорувати деякі приховані файли, тобто .gitви можете використовувати вищезгадану команду.

Якщо ви хочете включити приховані файли,

find . -type f  -exec sed -i 's/foo/bar/g' {} +

В обох випадках рядок fooбуде замінено новоюbar


5

grep -lr 'subdomainA.example.com' | while read file; do sed -i "s/subdomainA.example.com/subdomainB.example.com/g" "$file"; done

Я думаю, що більшість людей не знають, що вони можуть передавати щось у файл "під час читання", і це дозволяє уникнути цих неприємних аргументів -print0, одночасно попередньо переглядаючи пробіли у назви файлів.

Подальше додавання echoдо sed дозволяє вам побачити, які файли будуть змінені, перш ніж це зробити.


Причина -print0корисна в тому, що він обробляє випадки, які while readпросто не вдається обробити - новий рядок є дійсним символом в імені файлу Unix, тому для того, щоб ваш код був повністю надійним, він також повинен впоратися з такими іменами файлів. (Крім того, ви хочете read -rуникнути настирливої ​​поведінки POSIX read.)
tripleee

Крім того, sedце не-оп, якщо немає відповідностей, тож grepнасправді це не потрібно; хоча це корисна оптимізація для того, щоб уникнути перезапису файлів, які не містять відповідностей, якщо таких у вас багато, або ви хочете уникати оновлення штампів дат на файли.
трійка

5

Ви можете використовувати awk, щоб вирішити це, як показано нижче,

for file in `find /home/www -type f`
do
   awk '{gsub(/subdomainA.example.com/,"subdomainB.example.com"); print $0;}' $file > ./tempFile && mv ./tempFile $file;
done

сподіваюся, що це допоможе вам !!!


Працює на MacO без будь-яких проблем! Усі sedбазовані команди не вдалися, коли бінарні файли були включені навіть із специфічними параметрами osx.
Jankapunkt

Обережно ... це підірветься, якщо в будь-якому з findповернених файлів пробіл у їх іменах! Це набагато безпечніше використовувати while read: stackoverflow.com/a/9612560/1938956
Сорен Bjørnstad

4

Спробуйте це:

sed -i 's/subdomainA/subdomainB/g' `grep -ril 'subdomainA' *`

1
Привіт @ RikHic, приємна порада - думав про щось подібне; на жаль, форматування вище не виявилося правильним :) Тож я спробую заздалегідь позначити попередній тег (не працює) - тож із втеченими задніми даними: sed -i 's/subdomainA/subdomainB/g'` grep -ril 'subdomainA' /home/www/*` - це все ще виглядає не надто добре, але слід вижити copypaste :) Привіт!
sdaau

4
#!/usr/local/bin/bash -x

find * /home/www -type f | while read files
do

sedtest=$(sed -n '/^/,/$/p' "${files}" | sed -n '/subdomainA/p')

    if [ "${sedtest}" ]
    then
    sed s'/subdomainA/subdomainB/'g "${files}" > "${files}".tmp
    mv "${files}".tmp "${files}"
    fi

done

4

Відповідно до цієї публікації в блозі:

find . -type f | xargs perl -pi -e 's/oldtext/newtext/g;'

Як уникнути косого /?. Наприклад, я хочу замінити IP-адреси: xxx.xxx.xxx.xxxдляxxx.xxx.xxx.xxx/folder
Pathros

Ви можете втекти /з \. Наприклад:find . -type f | xargs perl -pi -e 's/xxx.xxx.xxx.xxx\/folder/newtext/g;'
Дж. Хпур

3

Якщо ви не проти використовувати vimразом з grepабоfind інструментів, ви можете стежити за відповідь , даний користувачем Герта в цьому посиланню -> Як зробити заміну тексту у великій ієрархії папок? .

Ось угода:

  • рекурсивно візьміть за рядок, який потрібно замінити певним шляхом, і візьміть лише повний шлях відповідного файлу. (це було б$(grep 'string' 'pathname' -Rl) .

  • (необов’язково), якщо ви хочете зробити попередню резервну копію цих файлів у централізованому каталозі, можливо, ви можете також скористатися цим: cp -iv $(grep 'string' 'pathname' -Rl) 'centralized-directory-pathname'

  • після цього ви можете за бажанням відредагувати / замінити, vimдотримуючись схеми, подібної схемі, наданій у наведеному посиланні:

    • :bufdo %s#string#replacement#gc | update

2

Трохи стара школа, але це працювало на ОС X.

Існує кілька хитрощів:

• Редагуватиме файли лише з розширенням .slsпід поточним каталогом

. потрібно уникати, щоб sedне оцінювати їх як "будь-якого персонажа"

,використовується якsed роздільник замість звичайного/

Також зауважте, що це відредагувати шаблон Jinja для передачі variableточки на шляхуimport (але це поза темою).

По-перше, переконайтеся, що ваша команда sed робить те, що ви хочете (це буде друкувати лише зміни в stdout, це не змінить файли):

for file in $(find . -name *.sls -type f); do echo -e "\n$file: "; sed 's,foo\.bar,foo/bar/\"+baz+\"/,g' $file; done

Відредагуйте команду sed за необхідності, як тільки ви будете готові внести зміни:

for file in $(find . -name *.sls -type f); do echo -e "\n$file: "; sed -i '' 's,foo\.bar,foo/bar/\"+baz+\"/,g' $file; done

Зверніть увагу на -i ''команду sed , я не хотів створювати резервну копію вихідних файлів (як пояснено в In-place edits with sed на OS X або в коментарі Роберта Луйо на цій сторінці).

Щасливі люди, що сидять!


2

просто щоб уникнути змін також

  • NearlysubdomainA.example.com
  • subdomainA.example.comp.other

але все ж

  • subdomainA.example.com.IsIt.good

(можливо, це не добре в ідеї, що лежить в корені домену)

find /home/www/ -type f -exec sed -i 's/\bsubdomainA\.example\.com\b/\1subdomainB.example.com\2/g' {} \;

2

Я просто використовую вершини:

find . -name '*.[c|cc|cp|cpp|m|mm|h]' -print0 |  xargs -0 tops -verbose  replace "verify_noerr(<b args>)" with "__Verify_noErr(<args>)" \
replace "check(<b args>)" with "__Check(<args>)" 

плюс один для `` *. [c | cc | cp | cpp | m | mm | h] ''
FractalSpace

2

Ось версія, яка має бути загальнішою за більшість; Наприклад, це не вимагає find(використовуючи duзамість цього). Це вимагає xargs, які є лише в деяких версіях плану 9 (наприклад, 9front).

 du -a | awk -F' '  '{ print $2 }' | xargs sed -i -e 's/subdomainA\.example\.com/subdomainB.example.com/g'

Якщо ви хочете додати фільтри, такі як розширення файлів, використовуйте grep:

 du -a | grep "\.scala$" | awk -F' '  '{ print $2 }' | xargs sed -i -e 's/subdomainA\.example\.com/subdomainB.example.com/g'

1

Для Qshell (qsh) на IBMi, не баш, як позначено ОП.

Обмеження команд qsh:

  • find не має опції -print0
  • xargs не має опції -0
  • sed не має опції -i

Таким чином, рішення в qsh:

    PATH='your/path/here'
    SEARCH=\'subdomainA.example.com\'
    REPLACE=\'subdomainB.example.com\'

    for file in $( find ${PATH} -P -type f ); do

            TEMP_FILE=${file}.${RANDOM}.temp_file

            if [ ! -e ${TEMP_FILE} ]; then
                    touch -C 819 ${TEMP_FILE}

                    sed -e 's/'$SEARCH'/'$REPLACE'/g' \
                    < ${file} > ${TEMP_FILE}

                    mv ${TEMP_FILE} ${file}
            fi
    done

Застереження:

  • Рішення виключає обробку помилок
  • Не Bash, як позначено ОП

У цьому виникають певні проблеми з цитуванням, а також з читанням рядків for.
трійчатка

1

Якщо ви хочете використовувати це, не знищуючи повністю ваш сховище SVN, ви можете сказати "знайти", щоб ігнорувати всі приховані файли, виконавши:

find . \( ! -regex '.*/\..*' \) -type f -print0 | xargs -0 sed -i 's/subdomainA.example.com/subdomainB.example.com/g'

Дужки здаються зайвими. Раніше в ньому була помилка форматування, яка зробила її непридатною (візуалізація Markdown з'їсть деякі символи з регулярного виразу).
трійчатка

1

Використання комбінації grepтаsed

for pp in $(grep -Rl looking_for_string)
do
    sed -i 's/looking_for_string/something_other/g' "${pp}"
done

@tripleee Я трохи це змінив. У цьому випадку виведіть для grep -Rl patternфайлів, створених командою список файлів, де є шаблон. Файли не читаються в forциклі.
Pawel

Так? У вас ще є forпетля; якщо якесь повернене ім'я файлу містить пробіл, воно не працюватиме правильно, оскільки оболонка токенізує forсписок аргументів. Але тоді ви використовуєте змінну імені файлу без лапок всередині циклу, тому вона замість цього перерветься, якщо ви виправили це. Виправлення цих решти помилок зробить вашу ідентичну відповіді @ MadMan2064.
трійчатка

@tripleee так, це правда, я пропустив це.
Pawel

1

Для заміни всіх подій у сховищі git ви можете використовувати:

git ls-files -z | xargs -0 sed -i 's/subdomainA\.example\.com/subdomainB.example.com/g'

Див. Список файлів у локальному git repo? для інших опцій перелічити всі файли у сховищі. В -zопції говорить мерзотник розділити імена файлів з нульовою байт, який гарантує , що xargs(з опцією -0) може відокремити імена файлів, навіть якщо вони містять прогалини або етажерку.


1
perl -p -i -e 's/oldthing/new_thingy/g' `grep -ril oldthing *`

1
Не використовується awk/ sed, але perl є загальним (за винятком вбудованих систем / лише із зайнятим ящиком).
pevik

1

щоб змінити кілька файлів (і зберегти резервну копію як *.bak):

perl -p -i -e "s/\|/x/g" *

візьме всі файли в каталозі та замінить |на x, що називається "пиріг Perl" (простий як пиріг)


Не рекурсивні через каталоги.
PKHunter

до неї можна подати трубу, що робить її дуже регульованою, в тому числі за допомогою каталогів. josephscott.org/archives/2005/08/… та unix.stackexchange.com/questions/101415/…
Stenemo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.