it-swarm-ru.tech

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

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

  • заменить строку foo на bar во всех файлах в текущем каталоге?
  • сделать то же самое рекурсивно для подкаталогов?
  • заменить только если имя файла совпадает с другой строкой?
  • заменить только если строка найдена в определенном контексте?
  • заменить, если строка находится на определенном номере строки?
  • заменить несколько строк одной и той же заменой
  • заменить несколько строк с различными заменами
791
terdon

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
    

    (требуется gawk 4.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
    

    Приведенный выше формат является в основном произвольным и, например, не допускает <пробел> в любом из [~ # ~] совпадений [~ # ~] или [~ # ~] заменить [~ # ~]. Однако этот метод очень общий: если вы можете создать выходной поток, который выглядит как сценарий 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
    
1061
terdon

Хороший инструмент r e pl 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

Или даже укажите расширения (-x) искать или даже искать рекурсивно (-R) в каталоге:

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

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

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

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

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

79
Fran

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

Вы также можете использовать 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' )
26
Alejandro Salamanca Mazuelo

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

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

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

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

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

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

15
o_o_o--

Вы можете использовать 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
15
Steven Penny

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

7
phs

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 regex для синтаксиса и функций регулярного выражения. Переключатель -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
3
Sundeep

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

script рекурсивно ищет все файлы, соответствующие шаблону glob (например, --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 - это обновленная версия скрипта, которая выделяет условия поиска и замены различными цветами.

3
ccpizza