Як я можу замінити рядок у файлі (файлах)?


751

Заміна рядків у файлах на основі певних критеріїв пошуку - дуже поширене завдання. Як я можу

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

2
Це покликане стати канонічним запитанням на цю тему (див. Цю мета-дискусію ). Будь ласка, відредагуйте мою відповідь нижче або додайте свою власну.
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воно знайдене в 3d-колонці (полі) вхідного файлу (якщо вважати поля, розділені пробілом):

    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
    
  • Або якщо у вас занадто багато пар шаблонів для вищезгаданого, щоб це було здійсненним, ви можете прочитати пари шаблонів із файлу (два шаблони, розділені пробілом, $ шаблон та $ заміна на рядок):

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

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

    Вищевказаний формат значною мірою довільний і, наприклад, не допускає <пробілу> ні в MATCH, ні В ЗАМІНІ . Спосіб, однак, дуже загальний: в основному, якщо ви можете створити вихідний потік, схожий на 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лінії перегляду.

  • 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інформацію, але немає ніяких причин видаляти башти. Також я знаю, що використання оболонки для обробки тексту не є ідеальним, але є випадки, коли це потрібно. Я відредагував кращу версію свого оригінального сценарію, який створить sedсценарій замість того, щоб насправді використовувати цикл оболонки для розбору. Це може бути корисно, якщо у вас є, наприклад, кілька сотень пар візерунків.
тердон

2
@terdon, твій баш невірний. удар до 4.3 буде слідувати посиланнями під час спадання. Крім того, bash не має еквівалента для (.)класифікатора глобуса, тому його не можна використовувати тут. (ви також пропускаєте деякі). Цикл for є неправильним (відсутній -r) і означає робити кілька проходів у файлах і не додає ніякої користі над сценарієм sed.
Стефан Шазелас

7
@terdon Що означає --після sed -iта перед командою заміщення?
Geek

5
@Geek, це POSIX річ. Це означає кінець параметрів і дозволяє передавати аргументи, починаючи з -. Використання його гарантує, що команди будуть працювати над файлами з такими іменами -foo. Без цього, the -fbised би розбирався як варіант.
terdon

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

Або навіть вкажіть розширення ( -x) для пошуку або навіть пошуку рекурсивно ( -R) у каталозі:

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

Ви також можете шукати / замінювати в інтерактивному режимі з -pопцією (підказка):

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

Інші варіанти, які варто пам’ятати, - це -e( шанси електронної події), які дозволяють regular expressions, тому ви можете шукати також вкладки ( \t), нові рядки ( \n) тощо. Навіть ви можете використовувати -fдля примусового дозволу (звичайно, лише тоді, коли у користувача є дозволи на запис) та -dдля збереження часу модифікації`).

Нарешті, якщо ви не впевнені, що саме зробите, скористайтеся режимом-s ( імітувати ).


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

1
Мені подобається -s (імітувати режим) :-)
erm3nda

25

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

Ви також можете використовувати 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' )

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'


Зверніть увагу, що це не вдасться, якщо будь-яке з ваших імен файлів містить пробіли, вкладки або нові рядки. Використання 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був винайдений der Mouse (від McGill) і розміщений у comp.unix.sources 11 (7) у серпні 1987 р. Існують оновлені версії. Наприклад, версія NetBSD qsubst.c,v 1.8 2004/11/01збирається та ідеально працює на моєму mac.


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 це оновлена ​​версія сценарію, яка висвітлює пошукові терміни та заміни різними кольорами.


1
Я не розумію, чому ви зробили б щось таке складне. Для рекурсії використовуйте globstarопцію bash (або еквівалент вашої оболонки) та **глобус або find. Для сухого ходу просто використовуйте sed. Якщо ви не використовуєте цю -iопцію, вона не внесе жодних змін. Для резервного використання sed -i.bak(або perl -i .bak); для файлів, які не відповідають, використовуйте grep PATTERN file || echo file. І чому б у світі ви мали б пітон розширити глобус, а не давати оболонці робити це? Чому script.py --glob=foo*замість просто script.py foo*?
тердон

1
Мої причини дуже прості: (1) перш за все, простота налагодження; (2) використовуючи лише один добре задокументований інструмент із підтримуючою спільнотою (3), яка не знає sedта awkдобре, і не бажає вкладати додатковий час на оволодіння ними, (4) читабельність, (5) це рішення також буде працювати на системах, що не належать до пошти (не те, що мені це потрібно, але може хтось інший).
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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.