如何搜索文件文本的模式,并将其替换为给定的值

我正在寻找一个脚本来搜索一个文件(或文件列表)的模式,如果找到了,就用给定的值替换该模式。

有什么想法吗?

133240 次浏览

免责声明: 这种方法只是对 Ruby 功能的一个简单说明,而不是用于替换文件中字符串的生产级解决方案。它容易出现各种故障情况,比如在崩溃、中断或磁盘已满的情况下数据丢失。除了备份所有数据的快速一次性脚本之外,这段代码不适合其他任何代码。因此,< strong > 不要把这段代码复制到你的程序中。

这里有一个快速简短的方法来做到这一点。

file_names = ['foo.txt', 'bar.txt']


file_names.each do |file_name|
text = File.read(file_name)
new_contents = text.gsub(/search_regexp/, "replacement string")


# To merely print the contents of the file, use:
puts new_contents


# To write changes to the file, use:
File.open(file_name, "w") {|file| file.puts new_contents }
end

实际上没有就地编辑文件的方法。当你可以不受影响的时候(即如果文件不太大) ,你通常会把文件读入内存(File.read) ,对读取字符串(String#gsub)执行替换操作,然后把修改后的字符串写回文件(File.openFile#write)。

如果文件大到不可行,那么你需要做的就是分块读取文件(如果你想要替换的模式不会跨越多行,那么一个分块通常意味着一行——你可以使用 File.foreach一行一行地读取一个文件) ,对于每个分块执行替换并将其附加到一个临时文件。迭代完源文件后,关闭它并使用 FileUtils.mv用临时文件覆盖它。

实际上,Ruby 确实有一个就地编辑特性

ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt

这将把双引号代码应用于工作目录中名称以“。短信”。编辑后的文件的备份副本将使用“。Bak”扩展(我想是“ foobar.txt.bak”)。

注意: 这似乎不适用于多行搜索。对于这些,您必须使用另一种不那么漂亮的方法,在正则表达式周围使用包装器脚本。

请记住,当您这样做时,文件系统可能空间不足,您可能会创建一个零长度的文件。如果你正在写/etc/passwd 文件作为系统组态管理的一部分,这将是灾难性的。

注意,就地文件编辑(如在接受的答案中)将总是截断文件并按顺序写出新文件。总是存在竞争条件,并发读取器将看到截断的文件。如果在写入过程中由于任何原因(ctrl-c、 OOM 杀手、系统崩溃、断电等)而中止进程,那么被截断的文件也将被遗留下来,这可能是灾难性的。这是一种数据丢失的场景,开发人员必须考虑,因为它会发生。出于这个原因,我认为公认的答案很可能不是公认的答案。至少写入一个临时文件并将文件移动/重命名到适当的位置,就像这个答案末尾的“简单”解决方案一样。

你需要使用一个算法:

  1. 读取旧文件并写出到新文件。(您需要小心将整个文件存入内存)。

  2. 显式关闭新的临时文件,这时可能会引发异常,因为没有空间,无法将文件缓冲区写入磁盘。(如果愿意,可以捕获这个文件并清理临时文件,但是此时您需要重新抛出某些内容或者相当困难地失败。

  3. 修复新文件上的文件权限和模式。

  4. 重命名新文件并将其放入适当位置。

使用 ext3文件系统,您可以确保在写入新文件的数据缓冲区之前,文件系统不会重新排列和写入将文件移到适当位置的元数据,因此这应该是成功或失败的。Ext4文件系统也进行了修补,以支持这种行为。如果您非常偏执,那么在将文件移动到适当位置之前,应该将 fdatasync()系统调用作为步骤3.5进行调用。

无论语言如何,这都是最佳实践。在调用 close()不抛出异常(Perl 或 C)的语言中,必须显式检查 close()的返回,如果失败则抛出异常。

上面的建议只需将文件存储到内存中,操作它并将它写出到文件中,这将保证在完整的文件系统上生成零长度的文件。您需要 一直都是使用 FileUtils.mv将一个完整编写的临时文件移到适当的位置。

最后要考虑的是临时文件的位置。如果您在/tmp 中打开一个文件,那么您必须考虑一些问题:

  • 如果/tmp 安装在不同的文件系统上,那么在写出本来可以部署到旧文件目标的文件之前,可能会运行/tmp。

  • 也许更重要的是,当您尝试跨设备挂载 mv文件时,您将透明地转换为 cp行为。旧文件将被打开,旧文件 inode 将被保留并重新打开,文件内容将被复制。这很可能不是您想要的,如果您试图编辑正在运行的文件的内容,则可能会遇到“文本文件繁忙”错误。这也违背了使用文件系统 mv命令的目的,您可能只使用部分编写的文件运行目标文件系统。

    这也与 Ruby 的实现无关,系统 mvcp命令的行为类似。

更可取的做法是在与旧文件相同的目录中打开一个 Tempfile。这样可以确保不会出现跨设备移动问题。mv本身应该永远不会失败,并且应该始终获得一个完整的、未截断的文件。在写出 Tempfile 时,应该遇到任何故障,如设备空间不足、权限错误等。

在目标目录中创建 Tempfile 的唯一缺点是:

  • 有时您可能无法在那里打开一个 Tempfile,例如,如果您试图在/proc 中“编辑”一个文件。由于这个原因,如果在目标目录中打开文件失败,可能需要后退并尝试/tmp。
  • 为了保存完整的旧文件和新文件,目标分区上必须有足够的空间。然而,如果你没有足够的空间来容纳两个副本,那么你可能会缺少磁盘空间,并且写一个被截断的文件的实际风险要高得多,所以我认为这是一个非常糟糕的权衡,除了一些非常狭窄的(和监视良好的)边缘情况。

下面是一些实现完整算法的代码(Windows 代码是未测试和未完成的) :

#!/usr/bin/env ruby


require 'tempfile'


def file_edit(filename, regexp, replacement)
tempdir = File.dirname(filename)
tempprefix = File.basename(filename)
tempprefix.prepend('.') unless RUBY_PLATFORM =~ /mswin|mingw|windows/
tempfile =
begin
Tempfile.new(tempprefix, tempdir)
rescue
Tempfile.new(tempprefix)
end
File.open(filename).each do |line|
tempfile.puts line.gsub(regexp, replacement)
end
tempfile.fdatasync unless RUBY_PLATFORM =~ /mswin|mingw|windows/
tempfile.close
unless RUBY_PLATFORM =~ /mswin|mingw|windows/
stat = File.stat(filename)
FileUtils.chown stat.uid, stat.gid, tempfile.path
FileUtils.chmod stat.mode, tempfile.path
else
# FIXME: apply perms on windows
end
FileUtils.mv tempfile.path, filename
end


file_edit('/tmp/foo', /foo/, "baz")

这里有一个稍微紧凑一点的版本,它不会考虑所有可能的边缘情况(如果你在 Unix 上,并且不关心写入/proc) :

#!/usr/bin/env ruby


require 'tempfile'


def file_edit(filename, regexp, replacement)
Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
File.open(filename).each do |line|
tempfile.puts line.gsub(regexp, replacement)
end
tempfile.fdatasync
tempfile.close
stat = File.stat(filename)
FileUtils.chown stat.uid, stat.gid, tempfile.path
FileUtils.chmod stat.mode, tempfile.path
FileUtils.mv tempfile.path, filename
end
end


file_edit('/tmp/foo', /foo/, "baz")

真正简单的用例,当您不关心文件系统权限时(要么您不以 root 身份运行,要么您以 root 身份运行,而文件是 root 拥有的) :

#!/usr/bin/env ruby


require 'tempfile'


def file_edit(filename, regexp, replacement)
Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
File.open(filename).each do |line|
tempfile.puts line.gsub(regexp, replacement)
end
tempfile.close
FileUtils.mv tempfile.path, filename
end
end


file_edit('/tmp/foo', /foo/, "baz")

TL; DR : 在所有情况下,至少应该使用这个值代替已接受的答案,以确保更新是原子的,并发读取器不会看到被截断的文件。如前所述,在与编辑文件相同的目录中创建 Tempfile 非常重要,以避免在/tmp 挂载在不同设备上时将跨设备 mv 操作转换为 cp 操作。调用 fdata 异步是多疑的一个附加层,但是它会导致性能下降,所以我在这个例子中省略了它,因为它并不常见。

require 'trollop'


opts = Trollop::options do
opt :output, "Output file", :type => String
opt :input, "Input file", :type => String
opt :ss, "String to search", :type => String
opt :rs, "String to replace", :type => String
end


text = File.read(opts.input)
text.gsub!(opts.ss, opts.rs)
File.open(opts.output, 'w') { |f| f.write(text) }

这对我有用:

filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }

下面是在给定目录的所有文件中查找/替换的解决方案。基本上,我采用 sepp2k 提供的答案并对其进行了扩展。

# First set the files to search/replace in
files = Dir.glob("/PATH/*")


# Then set the variables for find/replace
@original_string_or_regex = /REGEX/
@replacement_string = "STRING"


files.each do |file_name|
text = File.read(file_name)
replace = text.gsub!(@original_string_or_regex, @replacement_string)
File.open(file_name, "w") { |file| file.puts replace }
end

另一种方法是在 Ruby 内部使用 inplace 编辑(不是从命令行) :

#!/usr/bin/ruby


def inplace_edit(file, bak, &block)
old_stdout = $stdout
argf = ARGF.clone


argf.argv.replace [file]
argf.inplace_mode = bak
argf.each_line do |line|
yield line
end
argf.close


$stdout = old_stdout
end


inplace_edit 'test.txt', '.bak' do |line|
line = line.gsub(/search1/,"replace1")
line = line.gsub(/search2/,"replace2")
print line unless line.match(/something/)
end

如果不想创建备份,那么将 '.bak'更改为 ''

这里是 Jim 的一行代码的一个替代品,这次是在一个脚本中

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}

保存在脚本中(如:

从命令行开始

replace.rb *.txt <string_to_replace> <replacement>

* . txt 可以替换为其他选项或某些文件名或路径

这样我就可以解释发生了什么,但仍然可以执行

# ARGV is an array of the arguments passed to the script.
ARGV[0..-3].each do |f| # enumerate the arguments of this script from the first to the last (-1) minus 2
File.write(f,  # open the argument (= filename) for writing
File.read(f) # open the argument (= filename) for reading
.gsub(ARGV[-2],ARGV[-1])) # and replace all occurances of the beforelast with the last argument (string)
end

编辑: 如果要使用正则表达式,请使用以下代码 显然,这只是为了处理相对较小的文本文件,没有千兆字节的怪物

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}

如果需要跨行进行替换,那么使用 ruby -pi -e将不起作用,因为 p一次处理一行。相反,我建议使用以下方法,尽管使用多 GB 文件可能会失败:

ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"

它在引号后面寻找空格(可能包括新行) ,在这种情况下,它会去掉空格。%q(')只是引用引号字符的一种花哨的方式。

我正在使用 Tty-file宝石

除了替换之外,它还包括 append、 prepend (在文件中给定的文本/正则表达式上)、 diff 等。