web-dev-qa-db-fra.com

Comment puis-je remplacer une chaîne dans un ou plusieurs fichiers?

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 foo par bar dans tous les fichiers du répertoire courant?
  • faire de même récursivement pour les sous-répertoires?
  • remplacer uniquement si le nom de fichier correspond à une autre chaîne?
  • remplacer uniquement si la chaîne se trouve dans un certain contexte?
  • remplacer si la chaîne est sur un certain numéro de ligne?
  • remplacer plusieurs chaînes par le même remplacement
  • remplacer plusieurs chaînes par des remplacements différents
791
terdon

1. Remplacement de toutes les occurrences d'une chaîne par une autre dans tous les fichiers du répertoire courant:

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

Toutes les solutions sed dans cette réponse supposent GNU sed. Si vous utilisez FreeBSD ou OS/X, remplacez -i par -i ''. Notez également que l'utilisation du commutateur -i avec n'importe quelle version de sed a certains systèmes de fichiers implications pour la sécurité et est déconseillée dans tout script que vous prévoyez de distribuer dans en tous cas.

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

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

    (celui Perl échouera pour les noms de fichiers se terminant par | ou espace) ).

  • Fichiers récursifs et réguliers ( y compris les fichiers cachés ) dans ce sous-répertoire et dans tous ses 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 zargs pour contourner).

    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'ils sont des fichiers réels (-f) et qu'ils sont accessibles en écriture (-w).

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

  • Non récursif, fichiers dans 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 ses 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 -- sert à dire à sed qu'aucun autre 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 find pour plus d'options):

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

    zsh:

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

3. Remplacez uniquement si la chaîne est trouvée dans un certain contexte

  • Remplacez foo par bar uniquement s'il y a baz plus tard sur la même ligne:

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

    Dans sed, l'utilisation de \( \) enregistre tout ce qui est entre parenthèses et vous pouvez ensuite 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 .

  • Remplacez foo par bar uniquement si foo se trouve dans la colonne (champ) 3d du fichier d'entrée (en supposant des champs séparés par des espaces):

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

    (nécessite gawk 4.1.0 ou plus récent).

  • Pour un champ différent, utilisez simplement $NN est le numéro du champ d'intérêt. Pour un séparateur de champs 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 awk et Perl affecteront l'espacement dans le fichier (supprimez les blancs de début et de fin et convertissez les séquences de blancs en un seul espace dans les lignes qui correspondent). Pour un champ différent, utilisez $F[N-1]N est le numéro de champ souhaité et pour un autre séparateur de champ, utilisez (le $"=":" définit le séparateur de champ de sortie sur : ):

    Perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
    
  • Remplacez foo par bar uniquement 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. Opérations de remplacement multiples: remplacer par différentes chaînes

  • Vous pouvez combiner les commandes sed:

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

    N'oubliez pas que l'ordre est important (sed 's/foo/bar/g; s/bar/baz/g' remplacera foo par baz).

  • ou commandes Perl

    Perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
    
  • Si vous disposez d'un grand nombre de modèles, il est plus facile d'enregistrer vos modèles et leurs remplacements dans un fichier de script sed:

    #! /usr/bin/sed -f
    s/foo/bar/g
    s/baz/zab/g
    
  • Ou, si vous avez trop de paires de motifs pour que cela soit possible, vous pouvez lire des paires de motifs à partir d'un fichier (deux motifs séparés par des espaces, $ motif et $ remplacement, par ligne):

    while read -r pattern replacement; do   
        sed -i "s/$pattern/$replacement/" file
    done < patterns.txt
    
  • Cela sera assez lent pour de longues listes de modèles et de gros fichiers de données, vous voudrez peut-être lire les modèles et créer un script sed à partir d'eux. Ce qui suit suppose qu'un délimiteur <space> sépare une liste de paires MATCH <space> REMPLACER 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 en grande partie arbitraire et, par exemple, ne permet pas un <space> dans l'un des deux [~ # ~] correspond à [~ # ~] ou [~ # ~] remplacer [~ # ~]. La méthode est cependant très générale: en gros, si vous pouvez créer un flux de sortie qui ressemble à un script sed, vous pouvez alors le source en tant que script sed en spécifiant sed en tant que -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 sed va concaténer tous les scripts en un dans l'ordre où ils apparaissent sur la ligne de commande. Aucun de ceux-ci ne doit se terminer par une \newline.

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

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

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

5. Opérations de remplacement multiples: remplacez plusieurs modèles par la même chaîne

  • Remplacez l'un des foo, bar ou baz par foobar

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

    Perl -i -pe 's/foo|bar|baz/foobar/g' file
    
1061
terdon

Un bon r e pl outil Linux est rpl, qui a été écrit à l'origine pour le projet Debian, il est donc disponible avec apt-get install rpl dans n'importe quelle distribution dérivée de Debian, et peut l'être pour d'autres, mais sinon vous pouvez télécharger le fichier tar.gz dans 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 rpl attention aux lettres majuscules mais pas aux mots complets , mais vous pouvez modifier ces valeurs par défaut avec les 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 l'option -p (Invite):

La sortie affiche le nombre de fichiers/chaîne remplacés et le type de recherche (casse/sensible, mots entiers/partiels), mais elle peut être silencieuse avec le -q ( mode silencieux ), ou encore plus détaillé, répertoriant les numéros de ligne qui contiennent des correspondances de chaque fichier et répertoire avec -v ( mode détaillé ) option.

Les autres options qui méritent d'être rappelées sont -e (Honneur e scapes) qui autorisent regular expressions, Vous pouvez donc rechercher également tabulations (\t), nouvelles lignes (\n), etc. Même vous pouvez utiliser -f Pour forcer les autorisations (bien sûr, uniquement lorsque l'utilisateur a des autorisations d'écriture) et -d pour conserver les temps de modification ").

Enfin, si vous n'êtes pas sûr de ce qui fera exactement, utilisez le mode de simulation -s ().

79
Fran

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, imprimant chaque ligne après que le script lui a été appliqué.

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

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

J'ai utilisé ceci:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. Liste tous les fichiers contenant old_string.

  2. Remplacez le retour à la ligne par les espaces (afin que la liste des fichiers puisse être alimentée dans sed.

  3. Exécutez sed sur ces fichiers pour remplacer l'ancienne chaîne par une nouvelle.

pdate: Le résultat ci-dessus échouera sur les noms de fichiers contenant des espaces blancs. Utilisez plutôt:

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

15
o_o_o--

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 de 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 se trouve 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 par le même remplacement

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

remplacer plusieurs chaînes par des remplacements différents

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

Du point de vue d'un utilisateur, un outil Unix simple et agréable qui fait parfaitement le travail est qsubst . Par exemple,

% qsubst foo bar *.c *.h

remplacera foo par bar dans tous mes fichiers C. Une fonctionnalité intéressante est que qsubst fera un remplacement de requête , c'est-à-dire qu'il me montrera chaque occurrence de foo et demande si je veux le remplacer ou non. [Vous pouvez remplacer inconditionnellement (sans demander) par -go option, et il existe d'autres options, par exemple, -w si vous souhaitez uniquement remplacer foo lorsqu'il s'agit d'un mot entier.]

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

7
phs

ripgrep (nom de la commande rg) est un outil grep, 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


Voir documentation de Rust regex pour la syntaxe et les fonctionnalités des expressions régulières. Le commutateur -P Active la saveur PCRE2 . rg prend 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


Comme grep, l'option -F Permettra de faire correspondre les chaînes fixes, une option pratique que je pense que sed devrait é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 est -U Qui permet 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 également gérer les fichiers de style 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 de rg est qu'il sera probablement plus rapide que 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

J'avais besoin de quelque chose qui fournirait une option de fonctionnement à sec et fonctionnerait récursivement avec un glob, et après avoir essayé de le faire avec awk et sed j'ai abandonné et l'ai fait à la place en python.

Le script recherche récursivement tous les fichiers correspondant à un modèle global (par exemple --glob="*.html") pour une expression régulière et remplace par l'expression régulière 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 que --search-regex a une option courte correspondante, c'est-à-dire -s. Courir avec -h pour voir toutes les options.

Par exemple, cela inversera 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 différentes couleurs.

3
ccpizza