Comment puis-je remplacer une chaîne dans un fichier?

753

Le remplacement de chaînes dans des fichiers en fonction de certains critères de recherche est une tâche très courante. Comment puis-je

  • remplacer la chaîne foopar bardans tous les fichiers du répertoire actuel?
  • faire de même récursivement pour les sous-répertoires?
  • remplacer uniquement si le nom du fichier correspond à une autre chaîne?
  • remplacer uniquement si la chaîne est trouvée dans un certain contexte?
  • remplacer si la chaîne est sur un certain numéro de ligne?
  • remplacer plusieurs chaînes avec le même remplacement
  • remplacer plusieurs chaînes avec différents remplacements
terdon
la source
2
Ceci est censé être une question canonique sur ce sujet (voir cette méta discussion ), n'hésitez pas à modifier ma réponse ci-dessous ou à ajouter la vôtre.
terdon

Réponses:

1009

1. Remplacer toutes les occurrences d’une chaîne par une autre dans tous les fichiers du répertoire en cours:

Il s’agit des cas où vous savez que le répertoire ne contient que des fichiers normaux et que vous souhaitez traiter tous les fichiers non cachés. Si ce n'est pas le cas, utilisez les approches de 2.

Toutes les sedsolutions dans cette réponse supposent GNU sed. Si vous utilisez FreeBSD ou OS / X, remplacez -ipar -i ''. Notez également que l'utilisation du -icommutateur avec toute version de seda des implications sur la sécurité du système de fichiers et est déconseillée dans les scripts que vous envisagez de distribuer de quelque manière que ce soit.

  • Non récursif, fichiers dans ce répertoire uniquement:

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

    ( perlcelui-ci échouera pour les noms de fichiers se terminant par |ou espace) ).

  • Fichiers récursifs ordinaires ( y compris ceux cachés ) dans ce sous-répertoire et dans tous les sous-répertoires

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

    Si vous utilisez zsh:

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

    (peut échouer si la liste est trop grande, voir zargspour contourner le problème).

    Bash ne peut pas rechercher directement les fichiers normaux, une boucle est nécessaire (les accolades évitent de définir les options globalement):

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

    Les fichiers sont sélectionnés lorsqu'il s'agit de fichiers réels (-f) et qu'ils sont accessibles en écriture (-w).

2. Ne remplacez que si le nom du fichier correspond à une autre chaîne / a une extension spécifique / est d'un certain type, etc.:

  • Non récursifs, les fichiers de ce répertoire uniquement:

    sed -i -- 's/foo/bar/g' *baz*    ## all files whose name contains baz
    sed -i -- 's/foo/bar/g' *.baz    ## files ending in .baz
    
  • Fichiers récursifs et réguliers dans ce sous-répertoire et dans tous les sous-répertoires

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

    Si vous utilisez bash (les accolades évitent de définir les options globalement):

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

    Si vous utilisez zsh:

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

    Le --signe sedque plus aucun drapeau ne sera donné dans la ligne de commande. Ceci est utile pour se protéger contre les noms de fichiers commençant par -.

  • Si un fichier est d'un certain type, par exemple, exécutable (voir man findpour plus d'options):

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

    zsh:

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

3. Ne remplacez que si la chaîne est trouvée dans un certain contexte

  • Remplacez foopar barseulement s'il y a un bazdernier sur la même ligne:

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

    Dans sed, utilisez \( \)enregistre tout ce qui est entre parenthèses et vous pouvez y accéder avec \1. Il existe de nombreuses variantes de ce thème. Pour en savoir plus sur ces expressions régulières, voir ici .

  • Remplacer foopar barseulement si fooest trouvé sur la colonne 3d (champ) du fichier d'entrée (en supposant que les champs sont séparés par des espaces):

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

    (nécessite gawk4.1.0 ou plus récent).

  • Pour un champ différent, utilisez simplement $NNest le numéro du champ d’intérêt. Pour un séparateur de champ différent ( :dans cet exemple), utilisez:

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

    Une autre solution utilisant perl:

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

    REMARQUE: les solutions awket perlaffecteront les espaces dans le fichier (supprimez les espaces de début et de fin et convertissez les séquences d'éléments en un caractère d'espacement dans les lignes correspondantes). Pour un champ différent, utilisez $F[N-1]Nest le numéro de champ souhaité et pour un séparateur de champ différent, utilisez ( $"=":"définit le séparateur de champ en sortie sur :):

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
  • Remplacez foopar barseulement sur la 4ème ligne:

    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. Plusieurs opérations de remplacement: remplacez par différentes chaînes

  • Vous pouvez combiner des sedcommandes:

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

    Sachez que l'ordre compte ( sed 's/foo/bar/g; s/bar/baz/g'sera substitué foopar baz).

  • ou commandes Perl

    perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
  • Si vous avez un grand nombre de patterns, il est plus facile de sauvegarder vos patterns et leurs remplacements dans un sedfichier de script:

    #! /usr/bin/sed -f
    s/foo/bar/g
    s/baz/zab/g
  • Ou, si vous avez trop de paires de modèles pour que ce qui précède soit réalisable, vous pouvez lire les paires de modèles à partir d'un fichier (deux modèles séparés par des espaces, $ pattern et $ replacement, par ligne):

    while read -r pattern replacement; do   
        sed -i "s/$pattern/$replacement/" file
    done < patterns.txt
  • Cela sera assez lent pour les longues listes de modèles et les fichiers de données volumineux. Vous voudrez peut-être lire les modèles et en créer un sedscript à la place. Ce qui suit suppose qu'un délimiteur <space> sépare une liste de paires MATCH <space> REPLACE se produisant une par ligne dans le fichier patterns.txt:

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

    Le format ci-dessus est largement arbitraire et, par exemple, ne permet pas un <espace> dans MATCH ou REPLACE . La méthode est cependant très générale: si vous pouvez créer un flux de sortie qui ressemble à un sedscript, vous pouvez le générer en tant que sedscript en spécifiant sedle fichier de script -stdin.

  • Vous pouvez combiner et concaténer plusieurs scripts de la même manière:

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

    Un POSIX sedconcaténera tous les scripts en un seul dans l’ordre dans lequel ils apparaissent sur la ligne de commande. Aucune de ces \nchoses ne doit se terminer par une ewline.

  • grep peut fonctionner de la même manière:

    sed -e'#generate a pattern list' <in |
    grep -f- ./grepped_file
  • Lorsque vous utilisez des modèles de chaînes fixes, il est recommandé d'échapper aux métacaractères d' expression régulière . Vous pouvez le faire assez facilement:

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

5. Plusieurs opérations de remplacement: remplacez plusieurs modèles avec la même chaîne

  • Remplacez l' une des foo, barou bazavecfoobar

    sed -Ei 's/foo|bar|baz/foobar/g' file
  • ou

    perl -i -pe 's/foo|bar|baz/foobar/g' file
terdon
la source
2
@ StéphaneChazelas merci pour le montage, il a en effet corrigé plusieurs problèmes. Cependant, veuillez ne pas supprimer les informations pertinentes pour bash. Tout le monde n'utilise pas zsh. Bien sûr, ajoutez des zshinformations, mais il n’ya aucune raison de supprimer les éléments bash. De plus, je sais que l’utilisation du shell pour le traitement de texte n’est pas idéale, mais il existe des cas où cela est nécessaire. J'ai édité dans une meilleure version de mon script original qui créera un sedscript au lieu d'utiliser réellement la boucle du shell pour analyser. Cela peut être utile si vous avez plusieurs centaines de paires de motifs par exemple.
terdon
2
@terdon, votre bash est incorrect. bash avant la 4.3 suivra les liens symboliques lors de la descente. De plus, bash n'a pas d'équivalent pour le (.)qualificatif globbing, il ne peut donc pas être utilisé ici. (vous en manquez - aussi). La boucle for est incorrecte (-r) et signifie qu'il faut effectuer plusieurs passes dans les fichiers sans aucun avantage par rapport à un script sed.
Stéphane Chazelas
7
@terdon Que signifie --après sed -iet avant la commande de substitution?
Geek
5
@ Geek c'est une chose de POSIX. Cela signifie la fin des options et vous permet de passer des arguments en commençant par -. Son utilisation garantit que les commandes fonctionneront sur des fichiers portant des noms tels que -foo. Sans cela, le -fserait analysé comme une option.
terdon
1
Soyez très prudent en exécutant certaines des commandes récursives dans les dépôts git. Par exemple, les solutions fournies à la section 1 de cette réponse modifieront les fichiers git internes d’un .gitrépertoire et gâcheront votre commande. Mieux vaut opérer dans / sur des répertoires spécifiques par leur nom.
Pistos
75

Une bonne r e pl acement outil Linux est RPL , qui a été écrite à l' origine pour le projet Debian, il est disponible avec apt-get install rpldans toute distro dérivée de Debian, et peut - être pour d' autres, mais sinon , vous pouvez télécharger le tar.gzfichier SourgeForge .

Exemple d'utilisation le plus simple:

 $ rpl old_string new_string test.txt

Notez que si la chaîne contient des espaces, elle doit être placée entre guillemets. Par défaut , rplprendre soin des lettres majuscules , mais pas de mots complets , mais vous pouvez modifier ces valeurs par défaut avec des options -i(ignorer la casse) et -w(mots entiers). Vous pouvez également spécifier plusieurs fichiers :

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

Ou même spécifier les extensions ( -x) pour rechercher ou même rechercher récursivement ( -R) dans le répertoire:

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

Vous pouvez également rechercher / remplacer en mode interactif avec -pl'option (invite):

La sortie indique le nombre de fichiers / chaîne remplacés et le type de recherche (casse dans / sensible, mots entiers / partiels), mais il peut être silencieux avec l’ option -q( mode silencieux ), ou même plus détaillée, énumérant les numéros de ligne contenant correspondances de chaque fichier et répertoire avec option -v( mode détaillé ).

D' autres options qui sont à retenir sont -e( l' honneur e hampes) qui permettent regular expressions, vous pouvez rechercher également des onglets ( \t), de nouvelles lignes ( \n), etc. Même vous pouvez utiliser -fpour forcer les autorisations (bien sûr, uniquement lorsque l'utilisateur dispose d'autorisations en écriture) et -dpour conserver les temps de modification`).

Enfin, si vous ne savez pas exactement ce que vous ferez, utilisez le mode-s ( simuler ).

Fran
la source
2
Tellement meilleur au feedback et à la simplicité que sed. Je souhaite juste que cela permette d'agir sur les noms de fichiers, et ensuite ce serait parfait tel quel.
Kzqai
1
j'aime les -s (mode simulation) :-)
erm3nda
25

Comment faire une recherche et remplacer sur plusieurs fichiers suggère:

Vous pouvez également utiliser find et sed, mais je trouve que cette petite ligne de Perl fonctionne bien.

perl -pi -w -e 's/search/replace/g;' *.php
  • -e signifie exécuter la ligne de code suivante.
  • -i signifie éditer sur place
  • -w écrire des avertissements
  • -p boucle sur le fichier d'entrée, en imprimant chaque ligne après que le script lui a été appliqué.

Mes meilleurs résultats proviennent de l'utilisation de perl et de grep (pour garantir que le fichier contient l'expression de recherche)

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )
Alejandro Salamanca Mazuelo
la source
13

Vous pouvez utiliser Vim en mode Ex:

remplacer la chaîne ALF par BRA dans tous les fichiers du répertoire actuel?

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

faire de même récursivement pour les sous-répertoires?

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

remplacer uniquement si le nom du fichier correspond à une autre chaîne?

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

remplacer uniquement si la chaîne est trouvée dans un certain contexte?

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

remplacer si la chaîne est sur un certain numéro de ligne?

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

remplacer plusieurs chaînes avec le même remplacement

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

remplacer plusieurs chaînes avec différents remplacements

ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file
Steven Penny
la source
13

J'ai utilisé ceci:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. Répertorie tous les fichiers qui contiennent old_string.

  2. Remplacez nouvelle ligne dans le résultat par des espaces (afin que la liste des fichiers puisse être alimentée) sed.

  3. Exécuter sedsur ces fichiers pour remplacer l’ancienne chaîne par la nouvelle.

Mise à jour: le résultat ci-dessus échouera pour les noms de fichiers contenant des espaces. Au lieu de cela, utilisez:

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

o_o_o--
la source
Notez que cela échouera si l'un de vos noms de fichier contient des espaces, des tabulations ou des nouvelles lignes. L'utilisation grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'le fera traiter avec des noms de fichiers arbitraires.
terdon
Merci les gars. ajout de la mise à jour et abandon de l’ancien code car c’est une mise en garde intéressante qui pourrait être utile à une personne ignorant ce comportement.
o_o_o--
6

Du point de vue de l'utilisateur, c'est un outil Unix simple et agréable qui fait le travail à la perfection qsubst. Par exemple,

% qsubst foo bar *.c *.h

remplacera foopar bardans tous mes fichiers C. Une fonctionnalité intéressante consiste à qsubstfaire une requête de remplacement , c'est -à- dire qu'elle me montrera chaque occurrence de fooet me demandera si je veux la remplacer ou non. [Vous pouvez remplacer sans condition (pas demander) avec -gooption, et il existe d'autres options, par exemple, -wsi vous souhaitez uniquement remplacer foolorsqu'il s'agit d'un mot entier.]

Comment l'obtenir: a qsubstété inventé par der Mouse (de McGill) et envoyé à comp.unix.sources 11 (7) en août 1987. Des versions mises à jour existent. Par exemple, la version de NetBSD est qsubst.c,v 1.8 2004/11/01compilée et fonctionne parfaitement sur mon mac.

phs
la source
2

J'avais besoin de quelque chose qui fournirait une option sèche et fonctionnerait de manière récursive avec un glob, et après avoir essayé de le faire avec awk, sedj'ai abandonné et je l'ai fait à la place en python.

Le script recherche récursivement tous les fichiers correspondant à un motif global (par exemple --glob="*.html") et les remplace par les expressions régulières de remplacement:

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

Chaque option longue telle qu’elle --search-regexa une option courte correspondante, c.-à-d -s. Exécuter avec -hpour voir toutes les options.

Par exemple, cela retournera toutes les dates de 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 est une version mise à jour du script qui met en évidence les termes de recherche et les remplacements avec des couleurs différentes.

ccpizza
la source
1
Je ne comprends pas pourquoi vous feriez quelque chose d'aussi complexe. Pour la récursion, utilisez l' globstaroption et les **globs ou les options de bash (ou l'équivalent de votre shell) find. Pour un essai à sec, utilisez simplement sed. À moins que vous n'utilisiez cette -ioption, aucune modification ne sera apportée. Pour une sauvegarde, utilisez sed -i.bak(ou perl -i .bak); pour les fichiers qui ne correspondent pas, utilisez grep PATTERN file || echo file. Et pourquoi dans le monde voudriez-vous que Python étende le glob au lieu de laisser le shell le faire? Pourquoi script.py --glob=foo*au lieu de juste script.py foo*?
terdon
1
Mon pourquoi est très simple: (1) avant tout, facilité de débogage; (2) en utilisant un seul outil bien documenté avec une communauté de soutien (3) ne sachant pas sedet awkbien et être peu disposés à investir du temps supplémentaire sur les maîtriser, (4) la lisibilité, (5) cette solution travaillera également sur les systèmes non-posix (pas que j'en ai besoin, mais quelqu'un d'autre pourrait)
ccpizza
1

ripgrep (nom de la commande rg) est un grepoutil, mais prend également en charge la recherche et le remplacement.

$ 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 ne prend pas en charge l'option sur place, vous devrez donc le faire vous-même

$ # -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


Consultez la documentation de Rust regex pour connaître la syntaxe et les fonctionnalités des expressions régulières. Le -Pcommutateur activera la version PCRE2 . rgprend en charge Unicode par défaut.

$ # 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


De même grep, l' -Foption permettra aux chaînes fixes de correspondre, une option pratique que je pense seddevoir également implémenter.

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


Une autre option pratique consiste à -Uactiver la correspondance multiligne.

$ # (?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 peut aussi gérer des fichiers de type 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


Un autre avantage rgest qu'il est susceptible d'être plus rapide quesed

$ # 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
la source