如何在模式文本中搜索模式并将其替换为给定值

Dan*_*nor 115 ruby file-io

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

思考?

Max*_*yak 187

这是一个快速的简短方法.

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
Run Code Online (Sandbox Code Playgroud)

  • 是的,我不确定那是你想要的.要编写使用File.open(file_name,"w"){| file | file.puts output_of_gsub} (7认同)
  • 我不得不使用file.write:File.open(file_name,"w"){| file | file.write(text)} (6认同)
  • 要写入文件,请将puts'line替换为`File.write(file_name,text.gsub(/ regexp /,"replace")` (2认同)

小智 100

实际上,Ruby确实具有就地编辑功能.和Perl一样,你可以说

ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Run Code Online (Sandbox Code Playgroud)

这将以双引号将代码应用于当前目录中名称以".txt"结尾的所有文件.编辑文件的备份副本将以".bak"扩展名创建(我认为"foobar.txt.bak").

注意:这似乎不适用于多行搜索.对于那些,你必须用另一个不那么漂亮的方式来做,使用正则表达式的包装脚本.

  • @NinadPachpute`-i`编辑到位.`.bak`是用于备份文件的扩展名(可选).`-p`就像``获得; <SCRIPT>; 放$ _; end`.(`$ _`是最后一行读取行,但您可以为它分配一些类似`echo aa | ruby​​ -p -e'$ _.upcase!'`.) (15认同)
  • 我如何在ruby脚本中使用它? (5认同)

lam*_*ont 47

请记住,执行此操作时,文件系统可能空间不足,您可能会创建一个零长度文件.如果您正在执行类似于/ etc/passwd文件作为系统配置管理的一部分的操作,那么这将是灾难性的.

[编辑:请注意,就接受的答案中的就地文件编辑将始终截断文件并按顺序写出新文件.始终存在竞争条件,并发读者将看到截断或部分截断的文件,这可能是灾难性的.出于这个原因,我认为接受的答案很可能不是公认的答案.]

您需要使用以下算法:

  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")
Run Code Online (Sandbox Code Playgroud)

这里有一个稍微紧凑的版本,不用担心每个可能的边缘情况(如果你在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")
Run Code Online (Sandbox Code Playgroud)

非常简单的用例,因为当您不关心文件系统权限时(要么您没有以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")
Run Code Online (Sandbox Code Playgroud)

TL; DR:在所有情况下,应该至少使用它来代替接受的答案,以确保更新是原子的,并发读者不会看到截断的文件.如上所述,在此处创建与编辑文件相同的目录中的Tempfile非常重要,以避免在/ tmp安装在其他设备上时将跨设备mv操作转换为cp操作.调用fdatasync是一个增加的偏执层,但它会导致性能损失,所以我从这个例子中省略了它,因为它不常用.

  • 我真的很感激这个额外的想法.作为一个初学者,看到经验丰富的开发者的思维模式是非常有趣的,他们不仅可以回答原始问题,还可以评论原始问题实际意义的更大背景. (3认同)

sep*_*p2k 11

实际上没有一种方法可以就地编辑文件.当你可以逃脱它时通常做的事情(即文件不是太大)是,你将文件读入内存(File.read),对读取字符串(String#gsub)执行替换,然后将更改后的字符串写回文件(File.open,File#write).

如果文件是足够大的,要成为不可行的,你需要做的,是在读的块文件(如果你想更换不会跨越多行模式,然后一个块通常意味着一条线-您可以使用File.foreach来逐行读取文件,并为每个块执行替换并将其附加到临时文件.完成对源文件的迭代后,关闭它并使用FileUtils.mv临时文件覆盖它.


Dav*_*idG 9

另一种方法是在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
Run Code Online (Sandbox Code Playgroud)

如果您不想创建备份,请将".bak"更改为"".


Ala*_*ois 6

这对我有用:

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


tan*_*ner 6

这是在给定目录的所有文件中查找/替换的解决方案.基本上我接受了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
Run Code Online (Sandbox Code Playgroud)