如何替换文件中的字符串?

ter*_*don 879 sed awk perl text-processing

根据某些搜索条件替换文件中的字符串是一项非常常见的任务。我怎样才能

  • 替换字符串foobar在当前目录下的所有文件?
  • 对子目录递归执行相同的操作吗?
  • 仅当文件名匹配另一个字符串时才替换?
  • 仅当在特定上下文中找到字符串时才替换?
  • 替换字符串是否在某个行号上?
  • 用相同的替换替换多个字符串
  • 用不同的替换替换多个字符串

ter*_*don 1194

1. 将当前目录中所有文件中所有出现的一个字符串替换为另一个:

这些适用于您 知道目录仅包含常规文件并且您想要处理所有非隐藏文件的情况。如果不是这种情况,请使用 2 中的方法。

sed此答案中的所有解决方案都假定 GNU sed。如果使用 FreeBSD 或 macOS,请替换-i-i ''. 另请注意,将-iswitch 与任何版本一起使用sed都有一定的文件系统安全隐患,并且在您计划以任何方式分发的任何脚本中都是不可取的。

perl对于以|或空格结尾的文件名,将失败))。

  • 此目录和所有子目录中的递归常规文件(包括隐藏文件)

     find . -type f -exec sed -i 's/foo/bar/g' {} +
    
    Run Code Online (Sandbox Code Playgroud)

    如果您使用的是 zsh:

     sed -i -- 's/foo/bar/g' **/*(D.)
    
    Run Code Online (Sandbox Code Playgroud)

    (如果列表太大,可能会失败,请参阅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
     )
    
    Run Code Online (Sandbox Code Playgroud)

    当文件是实际文件 (-f) 并且它们是可写的 (-w) 时,就会选择这些文件。

2. 仅当文件名匹配另一个字符串/具有特定扩展名/属于某种类型等时才替换:

--用来告诉sed没有更多的旗帜将在命令行中给出。这对于防止以 开头的文件名很有用-

  • 如果文件是某种类型的,例如,可执行文件(man find有关更多选项,请参阅):

    find . -type f -executable -exec sed -i 's/foo/bar/g' {} +
    
    Run Code Online (Sandbox Code Playgroud)

zsh

    sed -i -- 's/foo/bar/g' **/*(D*)
Run Code Online (Sandbox Code Playgroud)

3. 仅在特定上下文中找到字符串时才替换

在 中sed, using\( \)保存括号中的任何内容,然后您可以使用\1. 此主题有许多变体,要了解有关此类正则表达式的更多信息,请参阅此处

  • 替换foobar只有当foo在输入文件的三维列(字段)被发现(假设空白分隔字段):

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

(需要gawk4.1.0 或更新版本)。

  • 对于不同的字段,只需使用$NwhereN是感兴趣的字段的编号。对于不同的字段分隔符(:在本例中)使用:

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

使用的另一种解决方案perl

    perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo 
Run Code Online (Sandbox Code Playgroud)

注意:awkperl解决方案都会影响文件中的间距(删除前导和尾随空格,并将空格序列转换为匹配行中的一个空格字符)。对于不同的字段,使用$F[N-1]whereN是您想要的字段编号,并使用不同的字段分隔符($"=":"将输出字段分隔符设置为:):

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
Run Code Online (Sandbox Code Playgroud)

4. 多种替换操作:替换为不同的字符串

请注意,顺序很重要(sed 's/foo/bar/g; s/bar/baz/g'将替换foobaz)。

  • 或 Perl 命令

     perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果您有大量模式,将您的模式及其替换保存在sed脚本文件中会更容易:

     #! /usr/bin/sed -f
     s/foo/bar/g
     s/baz/zab/g
    
    Run Code Online (Sandbox Code Playgroud)
  • 或者,如果您有太多的模式对使上述操作不可行,您可以从文件中读取模式对(两个空格分隔的模式,每行 $pattern 和 $replacement):

     while read -r pattern replacement; do   
         sed -i "s/$pattern/$replacement/" file
     done < patterns.txt
    
    Run Code Online (Sandbox Code Playgroud)
  • 对于长的模式列表和大型数据文件,这将非常慢,因此您可能想要读取模式并sed从它们创建脚本。下面假设<<!>space<!>>分隔符分隔MATCH<<!>space<!>>REPLACE对的列表,文件中每行出现一个patterns.txt

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

上面的格式在很大程度上是任意的,例如,在MATCHREPLACE中都不允许有<<!>space<!>>。不过,该方法非常通用:基本上,如果您可以创建一个看起来像脚本的输出流,那么您可以通过将的脚本文件指定为stdin来将该流作为脚本源。sedsedsed-

  • 您可以以类似的方式组合和连接多个脚本:

     SOME_PIPELINE |
     sed -e'#some expression script'  \
         -f./script_file -f-          \
         -e'#more inline expressions' \
     ./actual_edit_file >./outfile
    
    Run Code Online (Sandbox Code Playgroud)

POSIXsed将按照它们在命令行中出现的顺序将所有脚本连接成一个。这些都不需要以\n猫线结束。

  • grep 可以以同样的方式工作:

     sed -e'#generate a pattern list' <in |
     grep -f- ./grepped_file
    
    Run Code Online (Sandbox Code Playgroud)
  • 使用固定字符串作为模式时,转义正则表达式元字符是一种很好的做法。你可以很容易地做到这一点:

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

5.多次替换操作:用同一个字符串替换多个模式

  • @terdon 在 `sed -i` 之后和替换命令之前的 `--` 表示什么? (7认同)
  • @Geek 那是 POSIX 的事情。它表示选项的结束,并允许您传递以“-”开头的参数。使用它可以确保命令可以处理名称类似于 `-foo` 的文件。没有它,`-f` 将被解析为一个选项。 (7认同)
  • @StéphaneChazelas 感谢您的编辑,它确实解决了一些问题。但是,请不要删除与 bash 相关的信息。不是每个人都使用 `zsh`。无论如何添加`zsh`信息,但没有理由删除bash内容。另外,我知道使用 shell 进行文本处理并不理想,但在某些情况下需要它。我编辑了一个更好的原始脚本版本,它将创建一个 `sed` 脚本,而不是实际使用 shell 循环来解析。例如,如果您有数百对模式,这会很有用。 (3认同)
  • 在 git 存储库中执行一些递归命令时要非常小心。例如,本答案第 1 部分中提供的解决方案实际上会修改 `.git` 目录中的内部 git 文件,实际上会弄乱您的结帐。最好按名称在特定目录内/上操作。 (3认同)
  • @terdon,您的 bash 不正确。4.3 之前的 bash 在降序时将遵循符号链接。此外,bash 没有与`(.)` globbing 限定符等效的内容,因此不能在此处使用。(你也错过了一些 - )。for 循环不正确(缺少 -r)并且意味着在文件中进行多次传递并且没有增加 sed 脚本的好处。 (2认同)
  • 只是想我会提到 **repren**,这是另一种方法。它是我自己的 Python 工具,因此必须与 pip 一起安装,但涵盖了大多数这些场景以及您无法使用 sed 和 perl 复制的其他几个场景(如试运行、统计数据和多个同时替换):https:// github.com/jlevy/repren (2认同)
  • 我建议在此处的递归选项中添加一个部分以排除“.hg”、“.git”和“.svn”目录。这种事情只有在为时已晚的时候才会想到。 (2认同)

Fra*_*ran 98

一个好的[R é PL acement Linux的工具是RPL,最初为Debian项目写的,所以它可与apt-get install rpl任何Debian的发行版导出,并且可以为别人,否则你可以下载tar.gz从文件SourceForge上

最简单的使用示例:

 $ rpl old_string new_string test.txt
Run Code Online (Sandbox Code Playgroud)

请注意,如果字符串包含空格,则应将其括在引号中。默认情况下rpl处理大写字母但不处理完整单词,但您可以使用选项-i(ignore case) 和-w(whole words)更改这些默认值。您还可以指定多个文件

 $ rpl -i -w "old string" "new string" test.txt test2.txt
Run Code Online (Sandbox Code Playgroud)

甚至指定分机-x)来搜索,甚至搜索递归-R)目录:

 $ rpl -x .html -x .txt -R old_string new_string test*
Run Code Online (Sandbox Code Playgroud)

您还可以使用(提示)选项在交互模式下搜索/替换-p

输出显示替换的文件/字符串的数量和搜索类型(大小写/敏感,整个/部分单词),但它可以使用-q安静模式)选项静音,或者更详细,列出包含的行号每个文件和目录与-v详细模式)选项的匹配。

其他值得记住的选项是-e(honor e scapes) 允许regular expressions,因此您还可以搜索选项卡 ( \t)、新行 ( \n) 等。您可以使用-f,以强制许可(当然,只有当用户有写权限),并-d保存修改times`)。

最后,如果您不确定会发生什么,请使用-s模拟模式)。

  • 在反馈和简单性方面比 sed 好得多。我只是希望它允许对文件名进行操作,然后它就完美了。 (2认同)

Ale*_*elo 28

如何搜索和替换多个文件建议:

您也可以使用 find 和 sed,但我发现这行 perl 效果很好。

perl -pi -w -e 's/search/replace/g;' *.php
Run Code Online (Sandbox Code Playgroud)
  • -e 表示执行以下代码行。
  • -i 表示就地编辑
  • -w 写警告
  • -p 循环输入文件,在脚本应用到它后打印每一行。

我最好的结果来自使用 perl 和 grep(以确保文件具有搜索表达式)

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )
Run Code Online (Sandbox Code Playgroud)


o_o*_*o-- 18

我用过这个:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
Run Code Online (Sandbox Code Playgroud)
  1. 列出所有包含old_string.

  2. 用空格替换结果中的换行符(以便文件列表可以输入到sed.

  3. sed在这些文件上运行以用新的替换旧的字符串。

更新:上述结果将在包含空格的文件名上失败。相反,使用:

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


Ste*_*nny 18

你可以在 Ex 模式下使用 Vim:

在当前目录的所有文件中用 BRA 替换字符串 ALF?

for CHA in *
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done
Run Code Online (Sandbox Code Playgroud)

对子目录递归执行相同的操作吗?

find -type f -exec ex -sc '%s/ALF/BRA/g' -cx {} ';'
Run Code Online (Sandbox Code Playgroud)

仅当文件名匹配另一个字符串时才替换?

for CHA in *.txt
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done
Run Code Online (Sandbox Code Playgroud)

仅当在特定上下文中找到字符串时才替换?

ex -sc 'g/DEL/s/ALF/BRA/g' -cx file
Run Code Online (Sandbox Code Playgroud)

替换字符串是否在某个行号上?

ex -sc '2s/ALF/BRA/g' -cx file
Run Code Online (Sandbox Code Playgroud)

用相同的替换替换多个字符串

ex -sc '%s/\vALF|ECH/BRA/g' -cx file
Run Code Online (Sandbox Code Playgroud)

用不同的替换替换多个字符串

ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file
Run Code Online (Sandbox Code Playgroud)


phs*_*phs 9

从用户的角度来看,一个完美而简单的 Unix 工具是qsubst. 例如,

% qsubst foo bar *.c *.h
Run Code Online (Sandbox Code Playgroud)

将替换foobar我所有的 C 文件。一个很好的功能是qsubst会执行查询替换,即,它会显示每次出现foo并询问我是否要替换它。[您可以无条件地(不询问)用-go选项替换,还有其他选项,例如,-w如果您只想替换foo整个单词。]

如何获得:qsubst由 der Mouse(来自 McGill)发明并于 1987 年 8 月发布到comp.unix.sources 11(7)。存在更新版本。例如,NetBSD 版本qsubst.c,v 1.8 2004/11/01在我的 mac 上可以完美地编译和运行。


Sun*_*eep 6

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

有关则表达式语法和功能,请参阅Rust regex 文档。该-P开关将启用PCRE2风格。rg默认支持 Unicode。

$ # non-greedy quantifier is supported
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat

$ # unicode support
$ echo 'fox:??????,eagle:?????' | rg '\p{L}+' -r '($0)'
(fox):(??????),(eagle):(?????)

$ # set operator example, remove all punctuation characters except . ! and ?
$ para='"Hi", there! How *are* you? All fine here.'
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Hi there! How are you? All fine here.

$ # use -P if you need even more advanced features
$ echo 'car bat cod map' | rg -P '(bat|map)(*SKIP)(*F)|\w+' -r '[$0]'
[car] bat [cod] map
Run Code Online (Sandbox Code Playgroud)

就像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
Run Code Online (Sandbox Code Playgroud)

另一个方便的选项是-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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

的另一个优点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-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real    0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical
Run Code Online (Sandbox Code Playgroud)


ccp*_*zza 5

我需要一些可以提供空运行选项的东西,并且可以递归地使用 glob 工作,在尝试使用它之后awksed我放弃了,而是在 python 中完成了它。

脚本以递归方式搜索匹配 glob 模式(例如--glob="*.html")的所有文件以查找正则表达式并替换为替换正则表达式:

find_replace.py [--dir=my_folder] \
    --search-regex=<search_regex> \
    --replace-regex=<replace_regex> \
    --glob=[glob_pattern] \
    --dry-run
Run Code Online (Sandbox Code Playgroud)

每个多头选项,例如,--search-regex都有一个相应的空头选项,即-s。运行-h以查看所有选项。

例如,这会将所有日期从2017-12-31to翻转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
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)

Here 是脚本的更新版本,它用不同的颜色突出显示搜索词和替换。

  • 我不明白你为什么要把事情弄得这么复杂。对于递归,请使用 bash(或您的 shell 的等效项)`globstar` 选项和 `**` globs 或 `find`。对于试运行,只需使用`sed`。除非你使用 `-i` 选项,它不会做任何改变。对于备份使用`sed -i.bak`(或`perl -i .bak`);对于不匹配的文件,使用`grep PATTERN file || 回声文件`. 为什么你要让 python 扩展 glob 而不是让 shell 来做呢?为什么是 `script.py --glob=foo*` 而不是 `script.py foo*`? (2认同)
  • 我的_为什么_很简单:(1) 首先,易于调试;(2) 仅使用具有支持性社区的单个记录良好的工具 (3) 不太了解 `sed` 和 `awk` 并且不愿意投入额外的时间来掌握它们,(4) 可读性,(5) 该解决方案也将在非 posix 系统上工作(不是我需要,但其他人可能需要)。 (2认同)