如何search一个模式的文件文本,并将其replace为给定的值
我正在寻找一个脚本来search一个模式文件(或文件列表),如果发现,用一个给定的值replace该模式。
思考?
这是一个简单的方法来做到这一点。
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
实际上,Ruby确实具有就地编辑function。 像Perl一样,你可以说
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
这会将双引号内的代码应用到当前目录中以“.txt”结尾的所有文件。 编辑文件的备份副本将会创build一个“.bak”扩展名(我认为是“foobar.txt.bak”)。
注意:这似乎不适用于多行search。 对于那些,你必须做另一个不太漂亮的方式,围绕正则expression式包装脚本。
请记住,当你这样做时,文件系统可能会空间不足,你可能会创build一个零长度的文件。 这是灾难性的,如果你正在写/ etc / passwd文件做系统configurationpipe理的一部分。
你需要使用一个algorithm:
-
读取旧文件并写入新文件。 (你需要小心将整个文件sl到内存中)。
-
显式closures新的临时文件,这是您可能会抛出exception,因为文件缓冲区不能被写入磁盘,因为没有空间。 (如果你愿意的话,抓住这个并且清理临时文件,但是你需要重新抛出一些东西或者相当难以在这一点上失败。
-
修复新文件上的文件权限和模式。
-
重命名新文件并将其放置到位。
使用ext3文件系统,可以保证写入文件的元数据不会被文件系统重新排列,并且在写入新文件的数据缓冲区之前写入,所以这应该成功或失败。 ext4文件系统也被修补以支持这种行为。 如果你是非常偏执的,你应该调用fdatasync()
系统调用步骤3.5,然后移动文件到位。
不pipe语言如何,这是最好的做法。 在调用close()
不会抛出exception(Perl或C)的语言中,必须显式检查close()
的返回值,如果失败则抛出exception。
上面的build议只是简单地将文件写入内存,操纵它并将其写入文件将保证在完整的文件系统上产生零长度的文件。 您需要始终使用FileUtils.mv
将完整的临时文件移动到位。
最后的考虑是临时文件的放置。 如果你在/ tmp中打开一个文件,那么你必须考虑一些问题:
- 如果将/ tmp安装在不同的文件系统上,则可能会在写出可以部署到旧文件目标的文件之前,运行/ tmp空间不足。
-
也许更重要的是,当你尝试通过设备挂载文件时,你会透明地转换为
cp
行为。 旧文件将被打开,旧文件inode将被保留并重新打开,文件内容将被复制。 这很可能不是您想要的,如果您尝试编辑正在运行的文件的内容,则可能会遇到“文本文件繁忙”错误。 这也损害了使用文件系统mv
命令的目的,并且可能只用部分写入的文件运行目标文件系统。这也与Ruby的实现无关。 系统
mv
和cp
命令的行为相似。
更可取的是打开与旧文件相同的目录中的Tempfile。 这确保了不会出现跨设备移动问题。 mv
本身不应该失败,你应该总是得到一个完整的和未被截断的文件。 在写入Tempfile的过程中,应该遇到任何故障,例如设备空间不足,权限错误等。
在目标目录中创buildTempfile的唯一缺点是:
- 有时你可能无法在那里打开一个Tempfile,例如,如果你正试图在/ proc中编辑一个文件。 出于这个原因,如果打开目标目录中的文件失败,您可能想回退并尝试/ tmp。
- 您必须在目标分区上有足够的空间来保存完整的旧文件和新文件。 但是,如果你没有足够的空间来保存两个副本,那么你可能在磁盘空间上很短,写一个截断文件的实际风险要高得多,所以我认为这是一个非常差的权衡之外的一些非常狭窄(以及监测)边缘情况。
下面是一些实现完整algorithm的代码(Windows代码未经testing和未完成):
#!/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")
没有真正的方法来就地编辑文件。 你通常做什么,当你可以摆脱它(即如果文件不是太大),你将文件读入内存( File.read
),对读取string( String#gsub
)执行replace,然后写入将更改的string返回到文件( File.open
, File#write
)。
如果文件足够大以至于不可行,那么你需要做的是以大块的forms读取文件(如果你想要replace的模式不会跨越多行,那么一个块通常意味着一行 – 你可以使用File.foreach
读取文件),并为每个块执行replace,并将其附加到一个临时文件。 当您完成对源文件的迭代时,closures它并使用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
如果您不想创build备份,请将“.bak”更改为“”。
这是一个在给定目录的所有文件中查找/replace的解决scheme。 基本上我把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
这适用于我:
filename = "foo" text = File.read(filename) content = text.gsub(/search_regexp/, "replacestring") File.open(filename, "w") { |file| file << content }
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) }
如果你需要跨越界限进行replace,那么使用ruby -pi -e
将不起作用,因为p
处理一行。 相反,我推荐以下内容,尽pipe可能会导致多GB文件失败:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
正在寻找空白(可能包括新行)后面的引号,在这种情况下,它摆脱了空白。 %q(')
只是引用引号的一种奇特的方式。
这里是吉姆的一个class轮的替代品,这次是一个脚本
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可以replace为另一个select或一些文件名或path
分解,以便我可以解释发生了什么,但仍然可执行
# 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