根据某些搜索条件替换文件中的字符串是非常常见的任务。如何在当前目录的所有文件中
将字符串foo替换为bar
递归地对子目录执行相同的操作?匹配另一个字符串吗?
仅当在特定上下文中找到该字符串时才替换吗?
如果该字符串位于某个行号上就替换吗?
用相同的替换项替换多个字符串
用不同的替换项替换多个字符串


评论

这旨在作为该主题的规范问答(请参阅此元讨论),请随时在下面编辑我的答案或添加您自己的答案。

很棒的grep -rl(然后通过管道传输到sed)在这里回答:unix.stackexchange.com/questions/472476 / ...

#1 楼

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/' file



sed中,使用\( \)保存括号中的内容,然后可以使用进行访问。此主题有很多变体,要了解有关此类正则表达式的更多信息,请参见此处。


仅当在输入的3d列(字段)上找到foo时,才用bar替换foo文件(假设用空格分隔的字段):
 gawk -i inplace '{gsub(/foo/,"baz",); print}' file



(需要gawk 4.1.0或更高版本)。


对于不同的字段只需使用$N,其中N是感兴趣字段的编号。对于不同的字段分隔符(在此示例中为:),请使用:
 gawk -i inplace -F':' '{gsub(/foo/,"baz",);print}' file



使用perl的另一种解决方案: awk解决方案将影响文件中的间距(删除开头和结尾的空格,并将空格序列转换为匹配的行中的一个空格字符)。对于不同的字段,请使用perl,其中$F[N-1]是您想要的字段编号,对于不同的字段分隔符,请使用(N将输出字段分隔符设置为$"=":"):
    perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo 



仅在第4行用:替换foo
    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 



4。多种替换操作:用不同的字符串替换


您可以组合使用bar命令:将用sed替换sed 's/foo/bar/g; s/bar/baz/g')。


或Perl命令
 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



如果有大量模式,则为更容易在foo脚本文件中保存模式及其替换:
 sed -i 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file



或者,如果您有太多的模式对不可行,可以从文件中读取模式对(每行两个空格分隔的模式$ pattern和$ replacement): br />
对于一长串的模式和大型数据文件,这将非常慢,因此您可能希望读取模式并从中创建baz脚本。以下假设<<!> space <!>>定界符分隔了文件sed中一行一行出现的MATCH <<!> space <!>> REPLACE对的列表:
 perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file



以上格式在很大程度上是任意的,例如,在MATCH或REPLACE中都不允许使用<<!>空格<!>>。该方法非常通用:基本上,如果您可以创建一个看起来像sed脚本的输出流,则可以通过将patterns.txt的脚本文件指定为sed stdin将该源流作为sed脚本。


您可以以类似的方式组合和连接多个脚本:
 #! /usr/bin/sed -f
 s/foo/bar/g
 s/baz/zab/g



POSIX sed会将所有脚本按出现在屏幕上的顺序连接在一起。命令行。所有这些都不需要以- ewline结尾。


sed可以以相同的方式工作:
 while read -r pattern replacement; do   
     sed -i "s/$pattern/$replacement/" file
 done < patterns.txt



固定工作-strings作为模式,转义正则表达式元字符是一个好习惯。您可以轻松地做到这一点:
 sed 's| *\([^ ]*\) *\([^ ]*\).*|s///g|' <patterns.txt |
 sed -f- ./editfile >outfile



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


\ngrepfoo中的任何一个替换为bar
 SOME_PIPELINE |
 sed -e'#some expression script'  \
     -f./script_file -f-          \
     -e'#more inline expressions' \
 ./actual_edit_file >./outfile




 sed -e'#generate a pattern list' <in |
 grep -f- ./grepped_file




评论


@StéphaneChazelas感谢您的编辑,确实确实修复了一些问题。但是,请不要删除与bash相关的信息。并非每个人都使用zsh。一定要添加zsh信息,但是没有理由删除bash内容。另外,我知道使用shell进行文本处理不是理想的,但是在某些情况下需要使用shell。我在原始脚本的更好版本中进行了编辑,它将创建一个sed脚本,而不是实际使用shell循环进行解析。例如,如果您有数百对模式,这将很有用。

– terdon♦
2015年1月16日15:10

@terdon,您的bash错误。 4.3之前的bash下降时将遵循符号链接。 bash也没有等效于(。)globlob限定符,因此不能在此处使用。 (您也缺少一些)。 for循环不正确(缺少-r),意味着在文件中进行了多次传递,并且与sed脚本相比没有任何好处。

–StéphaneChazelas
15年1月16日在15:16

@terdon在sed -i之后和替代命令之前指示什么?

–极客
2015年9月28日在11:29



@Geek是POSIX的东西。它表示选项的结尾,并允许您传递以-开头的参数。使用它可以确保命令可以在名称为-foo的文件上使用。没有它,-f将被解析为一个选项。

– terdon♦
2015年9月28日在11:42



在git仓库中执行一些递归命令时要非常小心。例如,此答案的第1部分中提供的解决方案实际上将修改.git目录中的内部git文件,并实际上使您的结帐混乱。最好按名称在特定目录内/上操作。

–手枪
16-4-19的14:44

#2 楼

rpl是一个很好的替代Linux工具,它最初是为Debian项目编写的,因此它可以在任何Debian派生发行版中与apt-get install rpl一起使用,也可以与其他工具一起使用,但是可以从SourceForge下载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*

也可以搜索/ q在带有-p(提示)选项的交互模式下:
输出显示替换的文件/字符串的数量和搜索的类型(区分大小写/区分大小写),但是使用-q(安静模式)选项,甚至更详细,使用-v(详细模式)选项列出包含每个文件和目录匹配项的行号。
其他值得记住的选项是-e(荣誉转义符),它允许regular expressions ,因此您还可以搜索标签(\t),换行(\n)等。您可以使用-f来强制授予权限(当然,仅当用户具有写权限时),并且可以使用-d保留修改时间。)
最后,如果您不确定会发生什么,请使用-s(模拟模式) )。

评论


在反馈和简单性方面比sed好得多。我只是希望它允许对文件名起作用,然后再按原样进行。

– Kzqai
16 Dec 23'在17:12

我喜欢-s(模拟模式):-)

– m3nda
18年6月10日在11:08

比sed好得多。发誓

– Marc Compere
8月3日17:26

非常感谢。 sed对于简单的替换很好,但对于更复杂和更长的字符串却很糟糕

–yeah22
9月15日19:02

对于macOS,可从MacPorts获得rpl。

– Murray
9月23日20:49

#3 楼

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


还可以使用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' )


#4 楼

您可以在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 {} ';'




replace



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


#5 楼

我使用了以下命令:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'



列出所有包含old_string的文件。在结果中用空格替换换行符(以便文件列表可以被送入sed
在这些文件上运行sed,用new替换旧字符串。

更新:以上结果将对包含空格的文件名失败,而应使用:

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♦
15-10-26在17:07



多谢你们。添加了更新并保留了旧代码,这是一个有趣的警告,对不知道此行为的人可能有用。

–o_o_o--
15-10-26在20:59



#6 楼

从用户的角度来看,可以很好地完成此工作的漂亮而简单的Unix工具是qsubst。例如,

% qsubst foo bar *.c *.h


将在我所有的C文件中将foo替换为barqsubst的一个很好的功能是执行查询替换,即它将向我显示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上编译并完美运行。

#7 楼

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 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 '(grep)' (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 '[-F]' [car] bat [cod] map 一样,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 

$ # same as: sed -E 's/\w+(\r?)$/123/' $ printf 'hi there\r\ngood day\r\n' | rg --passthru --crlf '\w+$' -r '123' hi 123 good 123 也可以处理dos样式的文件
 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+)+\b//g' big.txt > f1 real 0m0.725s $ time rg --no-unicode --passthru -wP '(\w+)(\s+)+' -r '' big.txt > f2 real 0m0.093s $ diff -s f1 f2 Files f1 and f2 are identical 快。
 q4312079q 


#8 楼

我需要一些可以提供空运行选项并可以与全局递归协同工作的东西,并尝试使用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-31

 =“ lang-bash prettyprint-override”> 31-12-2017 


 python replace.py --glob=myfile.txt \
    --search-regex="(\d{4})-(\d{2})-(\d{2})" \
    --replace-regex="--" \
    --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) 是该版本的更新版本脚本,以不同的颜色突出显示搜索词和替换词。


评论


我不明白您为什么要做这么复杂的事情。要进行递归,请使用bash的(或与您的外壳等效的)globstar选项和** globs或find。对于空运行,只需使用sed。除非您使用-i选项,否则它将不会进行任何更改。对于备份,请使用sed -i.bak(或perl -i .bak);对于不匹配的文件,请使用grep PATTERN文件||回声文件。而且为什么在世界上您会用python扩展glob而不是让shell这样做呢?为什么是script.py --glob = foo *而不是script.py foo *?

– terdon♦
17年11月23日在9:34



我的原因很简单:(1)首先,易于调试; (2)仅在支持社区的情况下使用单一的文档齐全的工具(3)不熟悉sed和awk,不愿意花费更多的时间来掌握它们,(4)可读性,(5)此解决方案也可用于非posix系统(不是我需要的,而是其他人)。

–ccpizza
17年11月23日在12:59



#9 楼

这里我用grep说,如果它要(在最后,所以我可以算发改线,以及更换的次数,输出)更改一个文件,然后我用sed实际更改该文件。请注意以下Bash函数最末端的一行sed用法:


replace_str Bash函数

用法:

gs_replace_str "regex_search_pattern" "replacement_string" "file_path"


bash函数:

# Usage: `gs_replace_str "regex_search_pattern" "replacement_string" "file_path"`
gs_replace_str() {
    REGEX_SEARCH=""
    REPLACEMENT_STR=""
    FILENAME=""

    num_lines_matched=$(grep -c -E "$REGEX_SEARCH" "$FILENAME")
    # Count number of matches, NOT lines (`grep -c` counts lines), 
    # in case there are multiple matches per line; see: 
    # https://superuser.com/questions/339522/counting-total-number-of-matches-with-grep-instead-of-just-how-many-lines-match/339523#339523
    num_matches=$(grep -o -E "$REGEX_SEARCH" "$FILENAME" | wc -l)

    # If num_matches > 0
    if [ "$num_matches" -gt 0 ]; then
        echo -e "\n${num_matches} matches found on ${num_lines_matched} lines in file"\
                "\"${FILENAME}\":"
        # Now show these exact matches with their corresponding line 'n'umbers in the file
        grep -n --color=always -E "$REGEX_SEARCH" "$FILENAME"
        # Now actually DO the string replacing on the files 'i'n place using the `sed` 
        # 's'tream 'ed'itor!
        sed -i "s|${REGEX_SEARCH}|${REPLACEMENT_STR}|g" "$FILENAME"
    fi
}


例如,将其放置在〜/ .bashrc文件中。关闭并重新打开终端,然后使用它。

示例:

do替换为bo,以使“正在执行”变为“正在执行”(我知道,我们应该修复拼写错误错误未创建它们:)):

$ gs_replace_str "do" "bo" test_folder/test2.txt 

9 matches found on 6 lines in file "test_folder/test2.txt":
1:hey how are you doing today
2:hey how are you doing today
3:hey how are you doing today
4:hey how are you doing today  hey how are you doing today  hey how are you doing today  hey how are you doing today
5:hey how are you doing today
6:hey how are you doing today?
$SHLVL:3 


输出屏幕截图:



参考文献:<登记/>

https://superuser.com/questions/339522/counting-total-number-of-matches-with-grep-instead-of-just-how-many-lines-match / 339523#339523
https://stackoverflow.com/questions/12144158/how-to-check-if-sed-has-changed-a-file/61238414#61238414