Как я могу заменить строку в файле (ах)?

751

Замена строк в файлах на основе определенных критериев поиска является очень распространенной задачей. Как я могу

  • заменить строку fooс barво всех файлов в текущем каталоге?
  • сделать то же самое рекурсивно для подкаталогов?
  • заменить только если имя файла совпадает с другой строкой?
  • заменить только если строка найдена в определенном контексте?
  • заменить, если строка находится на определенном номере строки?
  • заменить несколько строк одной и той же заменой
  • заменить несколько строк с различными заменами
Тердон
источник
2
Это должно быть каноническим Q & A по этой теме (см. Эту мета-дискуссию ), пожалуйста, не стесняйтесь редактировать мой ответ ниже или добавить свой собственный.
Terdon

Ответы:

1009

1. Замена всех вхождений одной строки на другую во всех файлах в текущем каталоге:

Это для случаев, когда вы знаете, что каталог содержит только обычные файлы и что вы хотите обработать все не скрытые файлы. Если это не так, используйте подходы в 2.

Все sedрешения в этом ответе предполагают GNU sed. Если вы используете FreeBSD или OS / X, замените -iна -i ''. Также обратите внимание, что использование -iкоммутатора с любой версией sedимеет определенные последствия для безопасности файловой системы и нежелательно в любом сценарии, который вы планируете распространять любым способом.

  • Не рекурсивные файлы только в этом каталоге:

    sed -i -- 's/foo/bar/g' *
    perl -i -pe 's/foo/bar/g' ./* 

    ( perlодин не удастся для имен файлов, заканчивающихся на |или пробел) ).

  • Рекурсивные, обычные файлы ( включая скрытые ) в этом и всех подкаталогах

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

    Если вы используете zsh:

    sed -i -- 's/foo/bar/g' **/*(D.)

    (может потерпеть неудачу, если список слишком большой, смотрите, zargsчтобы обойти).

    Bash не может напрямую проверять наличие обычных файлов, необходим цикл (фигурные скобки не задают параметры глобально):

    ( shopt -s globstar dotglob;
        for file in **; do
            if [[ -f $file ]] && [[ -w $file ]]; then
                sed -i -- 's/foo/bar/g' "$file"
            fi
        done
    )

    Файлы выбираются, когда они являются фактическими файлами (-f), и они доступны для записи (-w).

2. Заменить, только если имя файла совпадает с другой строкой / имеет конкретное расширение / имеет определенный тип и т.д .:

  • Не рекурсивные файлы только в этом каталоге:

    sed -i -- 's/foo/bar/g' *baz*    ## all files whose name contains baz
    sed -i -- 's/foo/bar/g' *.baz    ## files ending in .baz
  • Рекурсивные, обычные файлы в этом и всех подкаталогах

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

    Если вы используете bash (в скобках избегайте глобальных настроек):

    ( shopt -s globstar dotglob
        sed -i -- 's/foo/bar/g' **baz*
        sed -i -- 's/foo/bar/g' **.baz
    )

    Если вы используете zsh:

    sed -i -- 's/foo/bar/g' **/*baz*(D.)
    sed -i -- 's/foo/bar/g' **/*.baz(D.)

    В --Подачи сказать , sedчто больше флагов не будет дано в командной строке. Это полезно для защиты от имен файлов, начинающихся с -.

  • Если файл имеет определенный тип, например, исполняемый (см. man findДополнительные параметры):

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

    zsh:

    sed -i -- 's/foo/bar/g' **/*(D*)

3. Заменить, только если строка найдена в определенном контексте

  • Замените fooна barтолько, если есть bazпозже в той же строке:

    sed -i 's/foo\(.*baz\)/bar\1/' file

    В sed, используя \( \)сохраняет все , что в скобках , а затем вы можете получить к нему доступ \1. Есть много вариантов этой темы, чтобы узнать больше о таких регулярных выражениях, смотрите здесь .

  • Заменить fooс barтолько если fooнаходится на 3 - й колонке (поле) входного файла (при условии , разделенные пробелами поля):

    gawk -i inplace '{gsub(/foo/,"baz",$3); print}' file

    (требуется gawk4.1.0 или новее).

  • Для другого поля просто используйте $Nгде Nномер поля интереса. Для другого разделителя полей ( :в этом примере) используйте:

    gawk -i inplace -F':' '{gsub(/foo/,"baz",$3);print}' file

    Другое решение с использованием perl:

    perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo 

    ПРИМЕЧАНИЕ: оба решения awkи perlрешения будут влиять на интервалы в файле (удаляйте начальные и конечные пробелы и преобразуйте последовательности пробелов в один пробел в этих совпадающих строках). Для другого поля используйте $F[N-1]где Nвы хотите номер поля, а для другого использования разделителя полей ( $"=":"устанавливает выходной разделитель полей :):

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
  • Заменить fooс barтолько на 4 - й строке:

    sed -i '4s/foo/bar/g' file
    gawk -i inplace 'NR==4{gsub(/foo/,"baz")};1' file
    perl -i -pe 's/foo/bar/g if $.==4' file

4. Несколько операций замены: заменить на разные строки

  • Вы можете комбинировать sedкоманды:

    sed -i 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file

    Помните, что порядок имеет значение ( sed 's/foo/bar/g; s/bar/baz/g'будет заменен fooна baz).

  • или Perl команды

    perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
  • Если у вас есть большое количество шаблонов, проще сохранить ваши шаблоны и их замены в sedфайле сценария:

    #! /usr/bin/sed -f
    s/foo/bar/g
    s/baz/zab/g
  • Или, если у вас слишком много пар шаблонов, чтобы описанное выше было возможно, вы можете прочитать пары шаблонов из файла (два шаблона с разделением пробелами, $ pattern и $ replace, на строку):

    while read -r pattern replacement; do   
        sed -i "s/$pattern/$replacement/" file
    done < patterns.txt
  • Это будет довольно медленно для длинных списков шаблонов и больших файлов данных, поэтому вы можете захотеть прочитать шаблоны и sedвместо них создать скрипт. Далее предполагается, что разделитель <пробел> разделяет список пар MATCH <пробел> ЗАМЕНА, встречающихся в файле по одной на строку patterns.txt:

    sed 's| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|' <patterns.txt |
    sed -f- ./editfile >outfile

    Приведенный выше формат в основном произвольный и, например, не допускает использование <пробела> в MATCH или REPLACE . Хотя метод очень общий: в основном, если вы можете создать выходной поток, который выглядит как sedскрипт, то вы можете использовать этот поток как sedскрипт, указав sedфайл скрипта как -stdin.

  • Вы можете комбинировать и объединять несколько скриптов аналогичным образом:

    SOME_PIPELINE |
    sed -e'#some expression script'  \
        -f./script_file -f-          \
        -e'#more inline expressions' \
    ./actual_edit_file >./outfile

    POSIX sedобъединит все сценарии в один в порядке их появления в командной строке. Ни один из них не должен заканчиваться на \newline.

  • grep может работать так же:

    sed -e'#generate a pattern list' <in |
    grep -f- ./grepped_file
  • При работе с фиксированными строками в качестве шаблонов рекомендуется избегать метасимволов регулярных выражений . Вы можете сделать это довольно легко:

    sed 's/[]$&^*\./[]/\\&/g
         s| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|
    ' <patterns.txt |
    sed -f- ./editfile >outfile

5. Несколько операций замены: заменить несколько шаблонов одной строкой

  • Заменить все foo, barили bazсfoobar

    sed -Ei 's/foo|bar|baz/foobar/g' file
  • или же

    perl -i -pe 's/foo|bar|baz/foobar/g' file
тердон
источник
2
@ StéphaneChazelas спасибо за редактирование, оно действительно исправило несколько вещей. Однако, пожалуйста, не удаляйте информацию, относящуюся к bash. Не все используют zsh. Конечно, добавьте zshинформацию, но нет причин удалять bash. Также я знаю, что использование оболочки для обработки текста не идеально, но есть случаи, когда это необходимо. Я отредактировал более качественную версию моего оригинального скрипта, которая будет создавать sedскрипт вместо фактического использования цикла оболочки для анализа. Это может быть полезно, например, если у вас есть несколько сотен пар паттернов.
Terdon
2
@terdon, ваш bash один неверный. bash до 4.3 будет следовать символическим ссылкам при спуске. Кроме того, bash не имеет эквивалента для (.)классификатора globbing, поэтому здесь его нельзя использовать. (вам не хватает некоторых - также). Цикл for является неправильным (отсутствует -r) и означает несколько проходов в файлах и не добавляет никаких преимуществ по сравнению со сценарием sed.
Стефан Шазелас
7
@terdon Что означает --после sed -iи перед командой замены?
Компьютерщик
5
@ Гик, это POSIX. Это означает конец опций и позволяет передавать аргументы, начиная с -. Его использование гарантирует, что команды будут работать с файлами с такими именами, как -foo. Без этого -fбудет разбираться как вариант.
Тердон
1
Будьте очень осторожны при выполнении некоторых рекурсивных команд в git-репозиториях. Например, решения, представленные в разделе 1 этого ответа, фактически изменят внутренние файлы git в .gitкаталоге и фактически испортят вашу проверку. Лучше работать в / на определенных каталогах по имени.
Пистос
75

Хороший т е пл acement инструмента Linux является RPL , которая первоначально была написана для проекта Debian, так что он доступен с apt-get install rplлюбым Debian производного дистрибутива, и может быть для других, но в противном случае вы можете скачать tar.gzфайл в SourgeForge .

Простейший пример использования:

 $ rpl old_string new_string test.txt

Обратите внимание, что если строка содержит пробелы, она должна быть заключена в кавычки. По умолчанию rplзаботятся о заглавных буквах, но не о полных словах , но вы можете изменить эти значения по умолчанию с помощью параметров -i(игнорировать регистр) и -w(целые слова). Вы также можете указать несколько файлов :

 $ rpl -i -w "old string" "new string" test.txt test2.txt

Или даже укажите extensions ( -x) для поиска или даже рекурсивный поиск ( -R) в каталоге:

 $ rpl -x .html -x .txt -R old_string new_string test*

Вы также можете искать / заменять в интерактивном режиме с -pопцией (подсказка):

Выходные данные показывают количество замененных файлов / строк и тип поиска (регистр в / чувствительном, целые / частичные слова), но он может быть беззвучным с опцией -q( тихий режим ) или даже более подробно, перечисляя номера строк, которые содержат совпадения каждого файла и каталога с опцией -v( подробный режим ).

Другие варианты, которые стоит помнить , являются -e(честь е Scapes) , которые позволяют regular expressions, так что вы можете осуществлять поиск и вкладки ( \t), новые строки ( \n) и т.д.. Даже вы можете использовать -fдля принудительного разрешения (конечно, только когда у пользователя есть права на запись) и -dсохранить время модификации`).

Наконец, если вы не уверены, что именно сделает, используйте -s( режим имитации ).

Fran
источник
2
Так намного лучше в обратной связи и простоте, чем сед. Я просто хотел бы, чтобы он позволял работать с именами файлов, и тогда все было бы идеально, как есть.
Kzqai
1
мне нравится -s (режим имитации) :-)
erm3nda
25

Как сделать поиск и заменить несколько файлов предлагает:

Вы также можете использовать find и sed, но я обнаружил, что эта маленькая строчка perl прекрасно работает.

perl -pi -w -e 's/search/replace/g;' *.php
  • -e означает выполнить следующую строку кода.
  • -i означает редактировать на месте
  • -пишем предупреждения
  • -p зацикливает входной файл, печатая каждую строку после того, как к нему применен скрипт.

Мои лучшие результаты получены от использования Perl и grep (чтобы убедиться, что файл имеет выражение поиска)

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )
Алехандро Саламанка Мазуэло
источник
13

Вы можете использовать Vim в режиме Ex:

заменить строку ALF на BRA во всех файлах в текущем каталоге?

for CHA in *
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

сделать то же самое рекурсивно для подкаталогов?

find -type f -exec ex -sc '%s/ALF/BRA/g' -cx {} ';'

заменить только если имя файла совпадает с другой строкой?

for CHA in *.txt
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

заменить только если строка найдена в определенном контексте?

ex -sc 'g/DEL/s/ALF/BRA/g' -cx file

заменить, если строка находится на определенном номере строки?

ex -sc '2s/ALF/BRA/g' -cx file

заменить несколько строк одной и той же заменой

ex -sc '%s/\vALF|ECH/BRA/g' -cx file

заменить несколько строк с различными заменами

ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file
Стивен Пенни
источник
13

Я использовал это:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. Список всех файлов, которые содержат old_string.

  2. Замените новую строку в результате пробелами (чтобы можно было передавать список файлов sed.

  3. Запустите sedэти файлы, чтобы заменить старую строку новой.

Обновление: приведенный выше результат не удастся для имен файлов, которые содержат пробелы. Вместо этого используйте:

grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'

o_o_o--
источник
Обратите внимание, что это не удастся, если любое из ваших имен файлов содержит пробелы, символы табуляции или перевода строки. Использование grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'заставит его иметь дело с произвольными именами файлов.
Terdon
Спасибо, парни. добавил обновление и оставил старый код, потому что это интересная оговорка, которая может быть полезна для тех, кто не знает о таком поведении.
o_o_o--
6

С точки зрения пользователя, это хороший и простой инструмент Unix, который отлично справляется со своей задачей qsubst. Например,

% qsubst foo bar *.c *.h

заменит fooс barво всех моих файлах C. Приятной особенностью является то, что qsubstбудет выполнять запрос-замену , то есть он будет показывать мне каждое вхождение fooи спрашивать, хочу ли я заменить его или нет. [Вы можете заменить безоговорочно (не спрашивая) с -goопцией, и есть другие варианты, например, -wесли вы хотите заменить, только fooкогда это целое слово.]

Как это получить: qsubstбыл изобретен дер Маусом (из McGill) и опубликован на comp.unix.sources 11 (7) в августе 1987 года. Существуют обновленные версии. Например, версия NetBSD qsubst.c,v 1.8 2004/11/01компилируется и отлично работает на моем Mac.

PHS
источник
2

Мне нужно было что - то , что бы обеспечить всухую вариант и будет работать рекурсивно с Glob, и после попытки сделать это с awkи sedя отказался и вместо этого сделал это в питона.

Скрипт ищет рекурсивно все файлы , соответствующие шаблон Глоба (например --glob="*.html") для регулярных выражений и заменяет регулярное выражение замены:

find_replace.py [--dir=my_folder] \
    --search-regex=<search_regex> \
    --replace-regex=<replace_regex> \
    --glob=[glob_pattern] \
    --dry-run

Каждый длинный вариант , такие как --search-regexесть соответствующий короткий вариант, то есть -s. Запустите с, -hчтобы увидеть все варианты.

Например, это перевернет все даты с 2017-12-31на 31-12-2017:

python replace.py --glob=myfile.txt \
    --search-regex="(\d{4})-(\d{2})-(\d{2})" \
    --replace-regex="\3-\2-\1" \
    --dry-run --verbose
import os
import fnmatch
import sys
import shutil
import re

import argparse

def find_replace(cfg):
    search_pattern = re.compile(cfg.search_regex)

    if cfg.dry_run:
        print('THIS IS A DRY RUN -- NO FILES WILL BE CHANGED!')

    for path, dirs, files in os.walk(os.path.abspath(cfg.dir)):
        for filename in fnmatch.filter(files, cfg.glob):

            if cfg.print_parent_folder:
                pardir = os.path.normpath(os.path.join(path, '..'))
                pardir = os.path.split(pardir)[-1]
                print('[%s]' % pardir)
            filepath = os.path.join(path, filename)

            # backup original file
            if cfg.create_backup:
                backup_path = filepath + '.bak'

                while os.path.exists(backup_path):
                    backup_path += '.bak'
                print('DBG: creating backup', backup_path)
                shutil.copyfile(filepath, backup_path)

            with open(filepath) as f:
                old_text = f.read()

            all_matches = search_pattern.findall(old_text)

            if all_matches:

                print('Found {} matches in file {}'.format(len(all_matches), filename))

                new_text = search_pattern.sub(cfg.replace_regex, old_text)

                if not cfg.dry_run:
                    with open(filepath, "w") as f:
                        print('DBG: replacing in file', filepath)
                        f.write(new_text)
                else:
                    for idx, matches in enumerate(all_matches):
                        print("Match #{}: {}".format(idx, matches))

                    print("NEW TEXT:\n{}".format(new_text))

            elif cfg.verbose:
                print('File {} does not contain search regex "{}"'.format(filename, cfg.search_regex))


if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='''DESCRIPTION:
    Find and replace recursively from the given folder using regular expressions''',
                                     formatter_class=argparse.RawDescriptionHelpFormatter,
                                     epilog='''USAGE:
    {0} -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern]

    '''.format(os.path.basename(sys.argv[0])))

    parser.add_argument('--dir', '-d',
                        help='folder to search in; by default current folder',
                        default='.')

    parser.add_argument('--search-regex', '-s',
                        help='search regex',
                        required=True)

    parser.add_argument('--replace-regex', '-r',
                        help='replacement regex',
                        required=True)

    parser.add_argument('--glob', '-g',
                        help='glob pattern, i.e. *.html',
                        default="*.*")

    parser.add_argument('--dry-run', '-dr',
                        action='store_true',
                        help="don't replace anything just show what is going to be done",
                        default=False)

    parser.add_argument('--create-backup', '-b',
                        action='store_true',
                        help='Create backup files',
                        default=False)

    parser.add_argument('--verbose', '-v',
                        action='store_true',
                        help="Show files which don't match the search regex",
                        default=False)

    parser.add_argument('--print-parent-folder', '-p',
                        action='store_true',
                        help="Show the parent info for debug",
                        default=False)

    config = parser.parse_args(sys.argv[1:])

    find_replace(config)

Here является обновленной версией скрипта, которая выделяет условия поиска и замены разными цветами.

ccpizza
источник
1
Я не понимаю, почему вы сделали что-то такое сложное. Для рекурсии используйте либо globstarопцию bash (или эквивалент вашей оболочки) и **globs, либо find. Для пробного запуска просто используйте sed. Если вы не используете -iопцию, она не будет вносить никаких изменений. Для резервного копирования используйте sed -i.bak(или perl -i .bak); для файлов, которые не совпадают, используйте grep PATTERN file || echo file. И почему в мире вам нужно, чтобы python расширял глобус, а не позволял оболочке это делать? Почему script.py --glob=foo*вместо просто script.py foo*?
Тердон
1
Мои причины очень просты: (1) прежде всего, простота отладки; (2) использование только одного хорошо документированного инструмента с поддерживающим сообществом (3) не зная sedи awkхорошо и не желая тратить дополнительное время на их освоение, (4) удобочитаемость, (5) это решение также будет работать на не-posix системах (не то, что мне нужно, но кто-то еще может).
ccpizza
1

ripgrep (имя команды rg) - grepинструмент, но также поддерживает поиск и замену.

$ cat ip.txt
dark blue and light blue
light orange
blue sky
$ # by default, line number is displayed if output destination is stdout
$ # by default, only lines that matched the given pattern is displayed
$ # 'blue' is search pattern and -r 'red' is replacement string
$ rg 'blue' -r 'red' ip.txt
1:dark red and light red
3:red sky

$ # --passthru option is useful to print all lines, whether or not it matched
$ # -N will disable line number prefix
$ # this command is similar to: sed 's/blue/red/g' ip.txt
$ rg --passthru -N 'blue' -r 'red' ip.txt
dark red and light red
light orange
red sky


rg не поддерживает опцию на месте, поэтому вам придется сделать это самостоятельно

$ # -N isn't needed here as output destination is a file
$ rg --passthru 'blue' -r 'red' ip.txt > tmp.txt && mv tmp.txt ip.txt
$ cat ip.txt
dark red and light red
light orange
red sky


См. Документацию по регулярным выражениям Rust для ознакомления с синтаксисом и функциями регулярного выражения -PПереключатель позволит PCRE2 аромат. rgподдерживает Unicode по умолчанию.

$ # non-greedy quantifier is supported
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat

$ # unicode support
$ echo 'fox:αλεπού,eagle:αετός' | rg '\p{L}+' -r '($0)'
(fox):(αλεπού),(eagle):(αετός)

$ # set operator example, remove all punctuation characters except . ! and ?
$ para='"Hi", there! How *are* you? All fine here.'
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Hi there! How are you? All fine here.

$ # use -P if you need even more advanced features
$ echo 'car bat cod map' | rg -P '(bat|map)(*SKIP)(*F)|\w+' -r '[$0]'
[car] bat [cod] map


Например grep, эта -Fопция позволит сопоставить фиксированные строки, что, по моему мнению, также sedдолжно быть реализовано.

$ printf '2.3/[4]*6\nfoo\n5.3-[4]*9\n' | rg --passthru -F '[4]*' -r '2'
2.3/26
foo
5.3-29


Еще одна удобная опция, -Uкоторая позволяет многострочное соответствие

$ # (?s) flag will allow . to match newline characters as well
$ printf '42\nHi there\nHave a Nice Day' | rg --passthru -U '(?s)the.*ice' -r ''
42
Hi  Day


rg может обрабатывать файлы в стиле DOS

$ # same as: sed -E 's/\w+(\r?)$/123\1/'
$ printf 'hi there\r\ngood day\r\n' | rg --passthru --crlf '\w+$' -r '123'
hi 123
good 123


Еще одним преимуществом rgявляется то, что он может быть быстрее, чемsed

$ # for small files, initial processing time of rg is a large component
$ time echo 'aba' | sed 's/a/b/g' > f1
real    0m0.002s
$ time echo 'aba' | rg --passthru 'a' -r 'b' > f2
real    0m0.007s

$ # for larger files, rg is likely to be faster
$ # 6.2M sample ASCII file
$ wget https://norvig.com/big.txt    
$ time LC_ALL=C sed 's/\bcat\b/dog/g' big.txt > f1
real    0m0.060s
$ time rg --passthru '\bcat\b' -r 'dog' big.txt > f2
real    0m0.048s
$ diff -s f1 f2
Files f1 and f2 are identical

$ time LC_ALL=C sed -E 's/\b(\w+)(\s+\1)+\b/\1/g' big.txt > f1
real    0m0.725s
$ time rg --no-pcre2-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real    0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical
Sundeep
источник