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

Вопрос или проблема

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

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

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

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

Все решения с использованием sed в этом ответе предполагают использование GNU sed. Если вы используете FreeBSD или macOS, замените -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*    ## все файлы, имя которых содержит baz
    sed -i -- 's/foo/bar/g' *.baz    ## файлы, заканчивающиеся на .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' {} +
    

3. Заменять только в случае нахождения строки в определенном контексте

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

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

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

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

    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 только на четвертой строке:

    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 и $replacement, на строку):

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

    sed 's| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|' 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 объединит все скрипты в один в порядке, в котором они появляются в командной строке. Ни у одного из них не нужно заканчиваться символом \n ewline.

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

    sed -e'#generate a pattern list' 
  • При работе с фикcированными строками в качестве паттернов полезно экранировать метасимволы регулярного выражения. Сделать это можно довольно легко:

    sed 's/[]$&^*\./[]/\\&/g
         s| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|
    ' 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
    

6. Замена путей файлов в нескольких файлах

  • Другое использование с разным разделителем:

    sed -i 's|path/to/foo|path/to/bar|g' *
    
  • или

    perl -i -pe 's|path/to/foo|path/to/bar|g' ./*
    

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

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

$ 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 (учет эскейп-последовательностей), которые позволяют использовать регулярные выражения, так что вы можете искать табуляции (\t), новые строки (\n) и т.д. Вы можете использовать -f, чтобы принудительно изменить права (конечно, только если у пользователя есть права на запись) и -d для сохранения времени изменения).

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

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

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

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

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

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )

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

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

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'

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

% qsubst foo bar *.c *.h

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

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

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

$ cat ip.txt
темно-синий и светло-синий
светло-оранжевый
синее небо
$ # по умолчанию, номер строки будет отображаться, если выходное предназначение stdout
$ # по умолчанию, только строки, соответствующие заданному паттерну, будут отображаться
$ # 'синий' — это шаблон поиска и -r 'красный' — это строка замены
$ rg 'синий' -r 'красный' ip.txt
1:темно-красный и светло-красный
3:красное небо

$ # --passthru опция полезна для печати всех строк, соответствуют ли они паттерну или нет
$ # -N отключит префикс номера строки
$ # эта команда аналогична: sed 's/blue/red/g' ip.txt
$ rg --passthru -N 'синий' -r 'красный' ip.txt
темно-красный и светло-красный
светло-оранжевый
красное небо

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

$ # -N не нужен здесь, так как выходное предназначение — это файл
$ rg --passthru 'синий' -r 'красный' ip.txt > tmp.txt && mv tmp.txt ip.txt
$ cat ip.txt
темно-красный и светло-красный
светло-оранжевый
красное небо

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

$ # нежадный квантификатор поддерживается
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat

$ # поддержка юникода
$ echo 'fox:αλεπού,eagle:αετός' | rg '\p{L}+' -r '($0)'
(fox):(αλεπού),(eagle):(αετός)

$ # пример с оператором множества, удаление всех знаков препинания, кроме . ! и ?
$ para=""Привет", как ты! Как *ты*? Все в порядке здесь."
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Привет как ты! Как ты? Все в порядке здесь.

$ # используйте -P, если вам нужны более продвинутые функции
$ 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) позволит . увидеть символы новой строки
$ printf '42\nПривет как\nХороший день' | rg --passthru -U '(?s)the.*ice' -r ''
42
Привет  День

rg также может обрабатывать файлы в формате dos

$ # то же самое, как: sed -E 's/\w+(\r?)$/123\1/'
$ printf 'привет тебе\r\nхороший день\r\n' | rg --passthru --crlf '\w+$' -r '123'
привет 123
хороший 123

Еще одно преимущество rg заключается в том, что он, вероятно, будет быстрее чем sed

$ # для маленьких файлов начальное время обработки rg является большой составляющей
$ time echo 'aba' | sed 's/a/b/g' > f1
real    0m0.002s
$ time echo 'aba' | rg --passthru 'a' -r 'b' > f2
real    0m0.007s

$ # для больших файлов rg скорее всего быстрее
$ # 6.2M пример файла ASCII
$ 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-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real    0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical

Мне нужно было что-то, что бы предоставляло опцию "проверочный запуск" (dry-run) и работало рекурсивно с шаблоном, и после попыток сделать это с awk и sed, я сдался и сделал это на Python.

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

find_replace.py [--dir=my_folder] \
    --search-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)

            блокировать оригинальный файл
            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  -r  -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)

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

Обновление июль 2022: У меня есть действительно надежный обёрточный инструмент, rgr, который означает "RipGrep Replace" и который оборачивает невероятно быстрый инструмент RipGrep (rg), который вы должны использовать вместо этого. См. пост здесь. Мой обертка поддерживает все опции rg, добавляя -R для настоящих текстовых замен на диске.


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

replace_str Bash функция

Обновление: приведенный ниже код был улучшен и теперь является частью моего eRCaGuy_dotfiles проекта как "find_and_replace.sh" здесь. <— Я рекомендую использовать этот инструмент теперь.

Использование:

gs_replace_str "regex_search_pattern" "replacement_string" "file_path"

Функция Bash:

# Использование: `gs_replace_str "regex_search_pattern" "replacement_string" "file_path"`
gs_replace_str() {
    REGEX_SEARCH="$1"
    REPLACEMENT_STR="$2"
    FILENAME="$3"

    num_lines_matched=$(grep -c -E "$REGEX_SEARCH" "$FILENAME")
    # Подсчет количества совпадений, НЕ строк (grep -c считает строки),
    # на случай, если на одной строке несколько совпадений; см.:
    # https://superuser.com/questions/339522/counting-total-number-of-matches-with-grep-instead-of-just-how-many-lines-match/339523#339523
    num_matches=$(grep -o -E "$REGEX_SEARCH" "$FILENAME" | wc -l)

    # Если num_matches > 0
    if [ "$num_matches" -gt 0 ]; then
        echo -e "\n${num_matches} найденных совпадений на ${num_lines_matched} строках в файле"\
                "\"${FILENAME}\":"
        # Теперь показываем эти точные совпадения с их соответствующими строками 'n' в файле
        grep -n --color=always -E "$REGEX_SEARCH" "$FILENAME"
        # Теперь реально ДЕЛАЕМ замену строк в файлах 'i'n place с помощью 's'tream 'ed'itor sed!
        sed -i "s|${REGEX_SEARCH}|${REPLACEMENT_STR}|g" "$FILENAME"
    fi
}

Поместите это в ваш файл ~/.bashrc, например. Закройте и откройте ваш терминал, а затем используйте его.

Пример:

Заменить do на bo, так чтобы "doing" стал "boing" (я знаю, мы должны исправлять орфографические ошибки, а не создавать их 🙂 ):

$ gs_replace_str "do" "bo" test_folder/test2.txt

9 найденные совпадения на 6 строках в файле "test_folder/test2.txt":
1:hey how are you doing today
2:hey how are you doing today
3:hey how are you doing today
4:hey how are you doing today  hey how are you doing today  hey how are you doing today  hey how are you doing today
5:hey how are you doing today
6:hey how are you doing today?
$SHLVL:3

Скриншот вывода, чтобы показать, как выделяются совпавшие тексты красным:

enter image description here

Ссылки:

  1. https://superuser.com/questions/339522/counting-total-number-of-matches-with-grep-instead-of-just-how-many-lines-match/339523#339523
  2. https://stackoverflow.com/questions/12144158/how-to-check-if-sed-has-changed-a-file/61238414#61238414

Использование Raku (ранее известного как Perl_6)


  • Одиночная замена (идентична Perl):
~$ raku -pe 's/foo/bar/;' file > new_file
  • Глобальная замена ( :g перемещается в начало s/// оператора ) :
~$ raku -pe 's:g/foo/bar/;' file > new_file
  • Как rg, Raku не имеет опции in-place, так что сделайте это самостоятельно для отдельных файлов:
~$ raku -pe 's:g/foo/bar/;' input.txt > tmp.txt && mv tmp.txt input.txt

По всему каталогу (не рекурсивно):

  • Теперь самое интересное — начиная с $*CWD текущий каталог, S/// 'большая S' заменить выбранные файлы .txt и записать эти новые файлы в ../Tmp0 (новосозданный 'сестринский' каталог):
~$ raku -e '
   # ниже создать дочерний каталог "Tmp0":
   my $Tmp0 = $*CWD.parent.add: "Tmp0/";
   unless $Tmp0.d { mkdir $Tmp0 or die $! };

   # ниже выбрать файлы ".txt" в $*CWD и выполнить итерацию по ним:
   for $*CWD.dir( test => / \.txt / ) -> $fpath {

       # ниже для каждого файла S/// модифицировать и сохранить в $mod-file:
       my $mod-file = $fpath.slurp.map({ S:g/foo/bar/ });

       # ниже записать $mod-file в новый каталог "Tmp0":
       spurt( IO::Path($Tmp0 ~ $fpath.basename ~ ".mod"), $mod-file, createonly => True )
   };'

  • Дополните круг, заменив оригинальный файл (начинать из того же $*CWD, как выше):
~$ raku -e '
   # ниже создать путь к каталогу "Tmp0", выдать ошибку, если он не существует:
   my $Tmp0 = $*CWD.parent.add: "Tmp0/";
   die "Каталог $Tmp0 не существует!" unless $Tmp0.d;

   # ниже выбрать файлы ".txt" в $*CWD и выполнить итерацию по ним:
   for $*CWD.dir( test => / \.txt / ) -> $fpath {

       # ниже удалить оригинальный файл, если существует файл ".mod":
       if (IO::Path($Tmp0 ~ $fpath.basename ~ ".mod")  ~~ :e & :f) {
           unlink($fpath) &&  \

           # если выше удалось, скопируйте ".mod" файл в оригинальный каталог, убрав ".mod" из имени:
           copy(IO::Path($Tmp0 ~ $fpath.basename ~ ".mod"), $fpath, createonly => True) &&  \

           # если выше удалось, удалить ".mod" файл (каталог "Tmp0" все еще будет существовать):
           unlink( IO::Path($Tmp0 ~ $fpath.basename ~ ".mod") );
       };
   };'

  • Очевидно, что скрипт копирования и скрипт замены выше могут быть объединены в один (и сокращены), но в таком виде они дают вам возможность проверить недавно созданные файлы (с помощью vimdiff или аналогичного), прежде чем удалять оригиналы. Предупреждение: Убедитесь, что вы начинаете с того же $*CWD для обоих скриптов копирования/замены!
  • Вышеуказанное — довольно простой подход, и он не погружается в подкаталоги. Для кода, показывающего, как выполнять рекурсию в каталоге, см. код в нижней части dir() ссылки ниже (первая ссылка).

Почему использовать Raku, вместо отличных ответов, уже опубликованных?

  1. Во-первых, приведенный выше код довольно компактный, и его можно ввести в командной строке (предложения приветствуются, чтобы сделать его компактнее).
  2. Во-вторых, приведенный выше код не требует загрузки внешних модулей.
  3. В-третьих, директории с запутанными именами файлов могут быть легко обследованы/изменены, так как метод dir() Raku может указывать имена файлов через Regex. Вы оцените возможность указания сразу (например) нечувствительных к регистру нескольких расширений: dir(test => /:i \. [ html | htmx | shtml | htm | xhtml ] $/).
  4. Вы можете указывать имена файлов на лету, например добавляя тэг .mod в конец вновь созданного/измененного файла. Это помогает вам отслеживать свой прогресс, не прибегая к вызову rename в конце работы.
  5. Raku обеспечивает высокоуровневую поддержку Юникода, что позволяет надежно выполнять замены Юникода. У вас есть выбор между NFC-нормализацией вашего Юникода или реконструкцией входных байт с использованием кодировки, называемой UTF8-C8. См. подробности по Юникоду ниже.
  6. Наконец, вышеупомянутый код может легко выполняться из автономного скрипта. Для портативности используйте код с динамической переменной $*CWD, переходя в интересующий каталог. В противном случае Raku предоставляет метод indir(), который позволяет вам переопределить $*CWD и выполнить модификационный скрипт из (практически) любого места в вашей файловой системе. См. indir() ссылку ниже для подробностей.

Примечание, так как файлы slurp() — читаются в память сразу, вы можете столкнуться с ограничениями памяти для чрезвычайно больших файлов. Это можно обойти, открыв файловый дескриптор и добавляя модифицированные строки поочередно (однако вы потеряете возможность выполнять многострочные замены). См. https://docs.raku.org/language/io-guide для деталей.

https://docs.raku.org/routine/dir
https://docs.raku.org/routine/indir
https://docs.raku.org/language/unicode#UTF8-C8
https://unix.stackexchange.com/a/676629/227738
https://unix.stackexchange.com/a/749581/227738

Моя обертка RipGrep Replace, rgr, теперь мой основной инструмент поиска и замены на диске и заменитель grep, точка. Он невероятно надежный, быстрый и тщательный. В отличие от grep, он обрабатывает полный набор функций синтаксиса регулярных выражений. Он в 0.348s/0.136s = ~3 раза быстрее чем git grep и в 0.806s/0.136s = ~6 раз быстрее чем GNU grep (см. тесты скорости RipGrep здесь), и rgr поддерживает поиск и замену на диске!

Моя rgr обертка вокруг невероятно быстрого RipGrep (rg) инструмента дает ему возможность выполнять замену текста на диске через новый параметр -R. Я называю свою обертку rgr, для "RipGrep Replace", так как он будет выполнять функцию поиска и замены на вашем диске через добавленный -R параметр.

См. инструкции по установке в начале rg_replace.sh, чтобы вы могли использовать его как rgr.

Для получения дополнительной информации о нем, смотрите мои комментарии здесь и здесь.

Примеры использования, как показано в меню помощи, доступные через rgr -h или rgr --help, приведены здесь:

ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:

    rgr foo -r boo
        Выполните dry run, чтобы заменить все экземпляры 'foo' на 'boo' в этой папке и ниже.
    rgr foo -R boo
        ФАКТИЧЕСКИ ЗАМЕНИТЕ НА ДИСКЕ все экземпляры 'foo' на 'boo' в этой папке и ниже.
    rgr foo -R boo file1.c file2.c file3.c
        То же, что и выше, но только в этих 3 файлах.
    rgr foo -R boo -g '*.txt'
        Используйте фильтр glob, чтобы заменить на вашем диске все экземпляры 'foo' на 'boo' только в .txt файлах, в этой папке и ниже. Узнайте больше о функции RipGrep glob здесь:
        https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#manual-filtering-globs
    rgr foo -R boo --stats
        Замените на вашем диске все экземпляры 'foo' на 'boo', покажите подробную статистику.

Посмотрите меню rgr --help сами для полного описания.

Ответ или решение

Замена строк в файлах часто является неотъемлемой частью работы ИТ-специалистов, будь то изменение конфигураций, обновление кода или просто редакция текстовых документов. В зависимости от ситуации вы можете воспользоваться различными инструментами и подходами для выполнения этой задачи. Давайте рассмотрим несколько сценариев и соответствующих методов.

Теория

Замена строк в файлах основывается на концепции поиска и замены текстовых данных. Это можно выполнять как на отдельных файлах, так и рекурсивно и по всей файловой структуре. Средства, которые помогают выполнять подобные задачи, включают командные утилиты, такие как sed и perl, а также более современное ПО, например, ripgrep и его дополнения. Инструмент должен соответствовать вашим задачам: поддерживать регулярные выражения, быть производительным и безопасным для использования на разных файловых системах.

Пример

Рассмотрим задачу замены строки "foo" на "bar" в текущей директории. Вариант с использованием sed может выглядеть так:

sed -i 's/foo/bar/g' *.txt

Этот пример выполняет поиск и замену в каждом .txt файле в текущей директории. Если необходимо выполнить эти действия рекурсивно, find в паре с sed поможет:

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

Аналогично, но более современный инструмент ripgrep в сочетании с модифицированным скриптом может предложить замену с фильтрацией:

rgr foo -R bar --glob '*.txt'

Применение

Правильная замена строк требует учета целого ряда факторов, включая имя файла, контекст строки, номер строки и другие. Рассмотрим некоторые из них.

  1. Замена по критерию имени файла:
    Если необходимо заменить строку только в файлах с определенной частью имени, это можно сделать с помощью комбинации find и расширенных шаблонов поиска:

    find . -type f -name "*baz*" -exec sed -i 's/foo/bar/g' {} +
  2. Замена в определенном контексте:
    Возможно, вам нужно заменить строку только тогда, когда она окружена особыми условиями. Например, sed может помочь в замене, если через некоторое количество символов встречается слово baz:

    sed -i 's/foo\(.*baz\)/bar\1/g' файл
  3. Замена на определенной строке:
    Если необходимо заменить строку на определенном номере строки, воспользуйтесь sed с указанием строкового диапазона:

    sed -i '4s/foo/bar/g' файл
  4. Множественная замена:
    sed позволяет проводить несколько замен в одном запуске, используя следующие команды подряд:

    sed -i 's/foo/bar/g; s/baz/zab/g; s/qux/xyz/g' файл
  5. Использование Python для сложных операций:
    Если требуется более сложная замена с сохранением структуры файла (например, реляционная замена), Python может быть более подходящим средством:

    import re
    
    with open('file.txt', 'r') as file:
       content = file.read()
    
    content = re.sub(r'foo', 'bar', content)
    
    with open('file.txt', 'w') as file:
       file.write(content)

Заключение

Каждый из вышеприведенных примеров может быть адаптирован в зависимости от ваших требований. Лучшая практика — экспериментировать с тестовыми файлами, чтобы убедиться в корректности замены. Кроме того, регулярное создание резервных копий и использование опции --dry-run, если она доступна, сэкономит вам время и защитит данные. Выбор подходящего инструмента и метода замены может существенно повысить вашу продуктивность и минимизировать риски ошибок.

Оцените материал
Добавить комментарий

Капча загружается...