it-swarm.cn

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

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

  • 在当前目录的所有文件中用foo替换字符串bar
  • 递归子目录是否一样?
  • 仅在文件名匹配另一个字符串时才替换?
  • 仅在特定上下文中找到字符串时才替换?
  • 如果字符串在特定的行号上,请替换?
  • 用相同的替换替换多个字符串
  • 用不同的替换项替换多个字符串
791
terdon

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_进行访问。此主题有很多变体,要了解有关此类正则表达式的更多信息,请参见 此处

  • 仅当在输入文件的3d列(字段)上找到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 
    _

    注意:awkPerl解决方案都将影响文件中的间距(删除开头和结尾的空格,并将空格序列转换为匹配的行中的一个空格字符)。对于其他字段,请使用_$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脚本。以下假设<space>定界符分隔了文件中的MATCH <space> REPLACE对的列表_patterns.txt_

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

    上面的格式在很大程度上是任意的,例如,在[〜#〜] match [〜#〜]或-中都不允许<space>[〜#〜] replace [〜#〜]。该方法非常通用:基本上,如果可以创建看起来像sed脚本的输出流,则可以通过将sed的脚本文件指定为_-_ stdin来将该流作为sed脚本来获取。

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

    _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.多次替换操作:用同一字符串替换多个模式

  • foobarbaz中的任何一个替换为foobar

    _sed -Ei 's/foo|bar|baz/foobar/g' file
    _
  • 要么

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

一个很好的 r e pl ace Linux工具是rpl,最初是为Debian项目编写的,因此它可以在任何从Debian派生的发行版中与apt-get install rpl一起使用,并且可能适用于其他版本,但否则,您可以在 SourgeForge 中下载tar.gz文件。

最简单的使用示例:

 $ 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(提示)选项以交互模式搜索/替换:

输出显示替换的文件/字符串的数量以及搜索的类型(区分大小写/全部/部分单词),但是可以使用-qquiet mode)选项保持静音,甚至更详细的列表行号,其中包含每个文件和目录的匹配项,并带有-v详细模式)选项。

其他值得记住的选项是允许使用-eregular expressions(荣誉e scapes),因此您还可以搜索制表符(\t),换行(\n)等。甚至您都可以使用-f来实现强制权限(当然,仅当用户具有写权限时)和-d可以保留修改时间。).

最后,如果不确定哪个会准确实现,请使用-s模拟模式)。

79
Fran

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

您也可以使用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' )
26

我用这个:

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'

15
o_o_o--

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

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

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
15
Steven Penny

从用户的角度来看,可以很好地完成工作的尼斯和简单Unix工具是 qsubst 。例如,

% qsubst foo bar *.c *.h

会在我所有的C文件中将foo替换为bar。一个不错的功能是qsubst将执行query-replace,即,它将向我显示每次出现的foo和问我是否要更换它。 [您可以使用-go选项无条件(无条件地)替换,还有其他选项,例如-w,如果您只想在整个单词中替换foo,则为该选项。]

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

7
phs

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


请参阅 正则表达式文档 了解正则表达式的语法和功能。 -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
3
Sundeep

我需要一些可以提供空运行选项的文件,并且可以使用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

每个长选项,例如--search-regex具有相应的简短选项,即-s。使用-h查看所有选项。

例如,这将翻转2017-12-3131-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 是脚本的更新版本,突出显示了搜索词和用不同颜色替换的内容。

3
ccpizza