Как сделать рекурсивный поиск / замену строки с помощью awk или sed?


679

Как мне найти и заменить каждое вхождение:

subdomainA.example.com

с

subdomainB.example.com

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


93
Совет: не делайте ниже в дереве проверки svn ... это перезапишет волшебные файлы папки .svn.
Дж. Полфер

7
Боже мой, это именно то, что я только что сделал. Но это сработало и, похоже, не принесло никакого вреда. Что может случиться худшее?
J. Katzwinkel

5
@ J.Katzwinkel: по крайней мере, это может повредить контрольные суммы, которые могут повредить ваш репозиторий.
ниндзягецко

3
Подсказка для всех людей, использующих sed: он добавит последние строки в ваши файлы. Если вы не хотите их, сначала выполните поиск-замену, которая не будет ничего соответствовать, и передайте это git. Тогда сделай настоящий. Затем интерактивно перезагружаем и удаляем первый.
funroll

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

Ответы:


852
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-репо. Я думал, что был умен, протестировав это на чистой ветке, чтобы я мог вернуться, если он сделал что-то плохое, но вместо этого испортил мой git index.
Цирйон

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

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

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), вам нужно будет указать явную пустую строку arg для -iопции sed . то есть: sed -i '' 's/original/replacement/g'
Натан Крейк

2
@JohnZwinck Моя ошибка, пропустил +. Как ни странно, решение Никиты работает быстрее для меня.
Сэм

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

4
Как я могу безопасно сделать это в папке с git-репо?
Хатшепсут

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

214

Самый простой способ для меня это

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

1
@Anatoly: только один вопрос: как я могу исключить двоичные файлы (исполняемые файлы) ?
user2284570

3
@ user2284570 Используйте флаги -Iили --binary-file=without-matchgrep.
Зейчин

34
Это работает особенно хорошо, когда вам нужно исключить каталоги, как с .svn. Например:grep -rl oldtext . --exclude-dir=.svn | xargs sed -i 's/oldtext/newtext/g'
Phyatt

11
brew install gnu-sedи использовать gsedна OSX, чтобы избежать боли.
П и я

1
Ребята , пожалуйста , внимание, если ваш проект мерзавец версирован, используйте вместо этого: git grep -rl oldtext . | xargs sed -i 's/oldtext/newtext/g'. вообще нехорошо .gitдурачиться
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: Нет: исключить исполняемые файлы (но включают сценарии оболочки) .
user2284570

8
@ I159 Разве этот ответ не идентичен ответу Джона Цвинка ?
Восстановите Монику, пожалуйста,

1
@ user2284570 Понятие «двоичный файл» не совсем четко определено. Вы можете использовать fileкоманду, чтобы попытаться определить тип каждого файла, но случайные изменения в его выводе могут быть немного озадачивающими. Опция -I(aka --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
Есть: from "man find": указанная команда запускается один раз для каждого соответствующего файла. То есть, если в / home / www есть 2000 файлов, то 'find ... -exec ...' приведет к 2000 вызовам perl; тогда как "найти ... | xargs ... 'будет вызывать perl только один или два раза (при условии, что ARG_MAX составляет около 32 КБ, а средняя длина имени файла - 20).
Занятый русский

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

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

4
@EmployedRussian, 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...)заключается в том, что оболочка не может обработать имена файлов с пробелами или другими метасимволами оболочки. Если вы знаете, что это не проблема, такой подход хорош; но у нас слишком много вопросов, когда люди не были предупреждены об этой проблеме или не поняли предупреждение.
tripleee


16

Один хороший приятель в качестве дополнительного. Используя git grep.

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

3
Хорошая идея, если вы работаете в git-репо, так как вы не рискуете переписать .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" }Использование, например, чтобы заменить слово на "меч": 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'

Mac:

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.
tripleee

@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построить одну команду для многих имен файлов.

На findстранице man: «Командная строка создается так же, как 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: только один вопрос: как я могу исключить двоичные файлы (исполняемые файлы) ?
user2284570

ack-grep сделает это автоматически для вас.
Henno

@Henno: это включает в себя сценарии оболочки?
user2284570

Да. Вот полный список типов файлов, которые он поддерживает: beyondgrep.com/documentation
Henno

6

Прямой метод, если вам нужно исключить directoryies ( --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это no-op, если нет совпадений, так что grepв действительности это не нужно; хотя это полезная оптимизация, позволяющая избежать перезаписи файлов, которые не содержат совпадений, если у вас их много или вы хотите без необходимости обновлять отметки даты на файлах.
tripleee

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

Надеюсь, что это поможет вам !!!


Работает на MacOs без проблем! Все sedоснованные команды не работали, когда были включены двоичные файлы даже с особыми настройками OSX.
Jankapunkt

Осторожно ... это взорвется, если findв именах файлов будет пробел! Гораздо безопаснее использовать while read: stackoverflow.com/a/9612560/1938956
Сорен Бьорнстад

4

Попробуй это:

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

1
Привет @RikHic, хороший совет - думал о чем-то вроде этого; к сожалению, вышеописанное форматирование не совсем получилось :) Так что я попробую с предварительным тэгом (не работает) - так что с экранированием обратных галочек, то: sed -i 's/subdomainA/subdomainB/g'` grep -ril 'subdomainA' /home/www/*` - это выглядит не слишком хорошо, но должно пережить копипасту :) ура!
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;'
J.Hpour

3

Если вы не возражаете против использования vimвместе с инструментами grepили find, вы можете следить за ответом, данным пользователем Gert в этой ссылке -> Как выполнить замену текста в большой иерархии папок?,

Вот сделка:

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

  • (необязательно), если вы хотите сделать предварительное резервное копирование этих файлов в централизованном каталоге, возможно, вы также можете использовать это: cp -iv $(grep 'string' 'pathname' -Rl) 'centralized-directory-pathname'

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

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

2

Немного старой школы, но это работало на OS 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 , я не хотел создавать резервную копию исходных файлов (как объяснено в Редактировании на месте с помощью 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, которые встречаются только в некоторых версиях Plan 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, а не bash, как отмечено OP.

Ограничения команд 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 как отмечено OP

Это имеет некоторые неприятные проблемы с цитированием и чтением строк с for.
tripleee

1

Если вы хотите использовать это без полного уничтожения вашего SVN-репозитория, вы можете указать 'find' игнорировать все скрытые файлы, выполнив:

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

Скобки кажутся излишними. Ранее у этого была ошибка форматирования, которая делала его непригодным для использования (рендеринг Markdown сожрал бы некоторые символы из регулярного выражения).
tripleee

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цикле.
Павел

А? У вас все еще есть forпетля; Если какое-либо возвращаемое имя файла содержит пробелы, оно не будет работать правильно, потому что оболочка токенизирует forсписок аргументов. Но затем вы используете переменную имени файла без кавычек внутри цикла, поэтому вместо этого она сломается, если вы исправите это. Исправление этих оставшихся ошибок сделает ваш ответ идентичным ответу @ MadMan2064.
tripleee

@tripleee Да, это правда, я пропустил это.
Павел

1

Для замены всех вхождений в git-репозитории вы можете использовать:

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

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


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

1
Не используется awk/ sed, но Perl распространен (кроме встраиваемых / систем только с busybox).
Певик

1

изменить несколько файлов (и сохранить резервную копию как *.bak):

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

возьмет все файлы в каталоге и заменит |x на «Perl pie» (просто как пирог)


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

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