Answers:
免责声明: 此方法只是Ruby功能的幼稚展示,而不是用于替换文件中字符串的生产级解决方案。它容易出现各种故障情况,例如崩溃,中断或磁盘已满时数据丢失。该代码不适合用于备份所有数据的快速一次性脚本之外的任何内容。因此,请勿将此代码复制到程序中。
这是一个简短的方法。
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.write(file_name, text.gsub(/regexp/, "replace")
实际上,Ruby确实具有就地编辑功能。像Perl一样,你可以说
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
这会将代码用双引号引起来,应用于当前目录中所有名称以“ .txt”结尾的文件。编辑文件的备份副本将以“ .bak”扩展名创建(我认为是“ foobar.txt.bak”)。
注意:这似乎不适用于多行搜索。对于这些,您必须使用另一种不太漂亮的方法,使用正则表达式周围的包装脚本。
<main>': undefined method
GSUB”的主要:对象(NoMethodError)
-i
编辑到位。.bak
是用于备份文件的扩展名(可选)。-p
就像while gets; <script>; puts $_; end
。($_
是最后读取的行,但是您可以为它分配类似的内容echo aa | ruby -p -e '$_.upcase!'
。)
请记住,执行此操作时,文件系统可能空间不足,并且您可能会创建一个零长度的文件。如果您要执行诸如写出/ etc / passwd文件作为系统配置管理的一部分的操作,这将是灾难性的。
请注意,像接受的答案一样,就地文件编辑将始终截断文件并顺序写出新文件。在并发阅读器始终会看到文件被截断的情况下,总会存在竞争状况。如果在写入过程中由于某种原因(ctrl-c,OOM杀手,系统崩溃,电源中断等)中止了该进程,则被截断的文件也将被留下,这可能是灾难性的。这是开发人员必须考虑的那种数据丢失情况,因为它会发生。因此,我认为可接受的答案很可能不是可接受的答案。至少要写入一个临时文件,然后将文件移动/重命名到该位置,就像该答案末尾的“简单”解决方案一样。
您需要使用以下算法:
读取旧文件并写出新文件。(在将整个文件保存到内存中时需要小心)。
明确关闭新的临时文件,在这里您可能会引发异常,因为文件缓冲区由于没有空间而无法写入磁盘。(如果需要,可以捕获并清理临时文件,但是此时您需要重新抛出一些东西或失败相当困难。
修复了新文件的文件权限和模式。
重命名新文件并将其放置到位。
使用ext3文件系统,可以确保为写入文件而将元数据写入文件的位置不会被文件系统重新排列,也不会在写入新文件的数据缓冲区之前写入,因此这应该成功还是失败。还对ext4文件系统进行了修补,以支持这种行为。如果您非常偏执,则应fdatasync()
在将文件移到适当位置之前先调用系统调用作为步骤3.5。
无论哪种语言,这都是最佳做法。在调用close()
不会引发异常的语言(Perl或C)中,您必须显式检查的返回,close()
如果失败则引发异常。
上面的建议只是将文件拖入内存,对其进行处理并将其写出到文件中,这样可以保证在整个文件系统上生成零长度的文件。您需要始终使用FileUtils.mv
将完全写入的临时文件移动到位。
最后要考虑的是临时文件的位置。如果在/ tmp中打开文件,则必须考虑一些问题:
如果/ tmp挂载在其他文件系统上,则在写出本可以部署到旧文件目标位置的文件之前,您可能会在空间上运行/ tmp。
可能更重要的是,当您尝试mv
跨设备挂载访问文件时,您将透明地转换为cp
行为。将打开旧文件,将保留并重新打开旧文件的inode并复制文件内容。这很可能不是您想要的,并且如果您尝试编辑正在运行的文件的内容,则可能会遇到“文本文件繁忙”错误。这也违反了使用文件系统mv
命令的目的,并且您可能只用部分写入的文件在空间不足的情况下运行目标文件系统。
这也与Ruby的实现无关。系统mv
和cp
命令的行为类似。
更可取的是在与旧文件相同的目录中打开一个Tempfile。这样可以确保不会出现跨设备移动的问题。在mv
本身应该永远不会失败,你应该总是让一个完整的,未截断文件。在写出Tempfile期间应遇到任何故障,例如设备空间不足,权限错误等。
在目标目录中创建临时文件的方法的唯一缺点是:
这是一些实现完整算法的代码(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:在所有情况下,都应使用TL; DR至少代替可接受的答案,以确保更新是原子的,并且并发阅读器不会看到截断的文件。如上文所述,在此处将Tempfile与已编辑文件创建在同一目录中非常重要,这样可以避免跨设备mv操作在/ tmp安装在其他设备上时转换为cp操作。调用fdatasync是一个额外的妄想症层,但是它将导致性能下降,因此在本示例中我将其省略,因为它并不常见。
确实没有一种就地编辑文件的方法。当可以使用它时(例如,如果文件不太大),通常会执行以下操作:将文件读入内存(File.read
),对读取的字符串(String#gsub
)进行替换,然后将更改后的字符串写回到文件(File.open
,File#write
)。
如果文件是足够大的,要成为不可行的,你需要做的,就是阅读以块的文件(如果模式要替换不会跨越多行,然后一个大块通常意味着一条线-您可以使用File.foreach
来逐行读取文件),然后对每个块执行替换操作并将其附加到临时文件中。遍历源文件后,请关闭它并使用FileUtils.mv
临时文件覆盖它。
另一种方法是在Ruby内部使用就地编辑(而不是从命令行):
#!/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'
为''
。
read
文件进行Slurp()更好。它具有可扩展性,并且应该非常快。
这对我有用:
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
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) }
这是吉姆(Jim)的那只班轮的替代品,这次是脚本
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
将其保存在脚本中,例如replace.rb
您从命令行开始
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]))}
File.read
需要根据stackoverflow.com/a/25189286/128421中的信息进行调整, 以解决为什么大文件打包不好的原因。另外,请File.open(filename, "w") { |file| file << content }
使用代替变化File.write(filename, content)
。