Как искать файл текста для шаблона и заменять его заданным значением

Я ищу script для поиска файла (или списка файлов) для шаблона и, если он найден, заменит этот шаблон на заданное значение.

Мысли?

Ответ 1

Отказ от ответственности: этот подход является наивной иллюстрацией возможностей 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

Ответ 2

Собственно, Ruby имеет функцию редактирования на месте. Как и Perl, вы можете сказать

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

Это применит код в двойных кавычках ко всем файлам в текущем каталоге, чьи имена заканчиваются на ".txt". Резервные копии отредактированных файлов будут созданы с расширением ".bak" ( "foobar.txt.bak", я думаю).

ПРИМЕЧАНИЕ. Это не работает для многострочного поиска. Для них вы должны сделать это другим менее привлекательным способом, с оболочкой script вокруг регулярного выражения.

Ответ 3

Имейте в виду, что при этом файловой системе может не хватать места, и вы можете создать файл нулевой длины. Это катастрофически, если вы делаете что-то вроде записи файлов /etc/passwd как часть управления конфигурацией системы.

[РЕДАКТИРОВАТЬ: обратите внимание, что редактирование файла на месте, как в принятом ответе, всегда будет обрезать файл и записывать новый файл последовательно. Всегда будет состояние гонки, при котором одновременные читатели увидят усеченный файл. Если процесс прерывается по какой-либо причине (ctrl-c, OOM killer, сбой системы, отключение питания и т.д.) Во время записи, то усеченный файл также будет оставлен, что может иметь катастрофические последствия. Это тот сценарий потери данных, который разработчики ДОЛЖНЫ рассмотреть, потому что это произойдет. По этой причине я думаю, что принятый ответ, скорее всего, не должен быть принятым ответом. Как минимум, напишите в временный файл и переместите/переименуйте файл на место, как "простое" решение в конце этого ответа.]

Вам нужно использовать алгоритм, который:

  1. читает старый файл и записывает в новый файл. (Вы должны быть осторожны с тем, чтобы копать целые файлы в память).

  2. явно закрывает новый временный файл, где вы можете выдать исключение, потому что файловые буферы не могут быть записаны на диск, потому что нет места. (Поймайте это и очистите временный файл, если хотите, но вам нужно что-то перебросить или довольно сильно потерпеть неудачу на этом этапе.

  3. исправляет права доступа к файлу и режимы для нового файла.

  4. переименовывает новый файл и помещает его на место.

В файловых системах ext3 вам гарантируется, что запись метаданных для перемещения файла на место не будет переставлена файловой системой и записана до того, как буферы данных для нового файла будут записаны, поэтому это должно либо завершиться успешно, либо завершиться неудачно. Файловая система ext4 также была исправлена для поддержки такого поведения. Если вы очень параноик, вы должны вызвать системный вызов fdatasync() как шаг 3.5, прежде чем перемещать файл на место.

Независимо от языка, это лучшая практика. В языках, где вызов close() не вызывает исключение (Perl или C), вы должны явно проверять возврат close() и генерировать исключение в случае сбоя.

Приведенное выше предложение просто залить файл в память, манипулировать им и записать его в файл, будет гарантированно создавать файлы нулевой длины в полной файловой системе. Вам всегда нужно использовать FileUtils.mv чтобы переместить полностью записанный временный файл на место.

Окончательное рассмотрение - размещение временного файла. Если вы откроете файл в /tmp, вам придется рассмотреть несколько проблем:

  • Если /tmp смонтирован в другой файловой системе, вы можете запустить /tmp из свободного пространства, прежде чем записать файл, который в противном случае можно было бы развернуть в месте назначения старого файла.
  • Возможно, что еще более важно, когда вы попытаетесь mv файл через устройство, вы будете преобразованы в поведение cp. Старый файл будет открыт, старый файл Inode будет сохранен и снова открыт, а содержимое файла будет скопировано. Скорее всего, это не то, что вам нужно, и вы можете столкнуться с ошибками "текстовый файл занят", если вы попытаетесь редактировать содержимое запущенного файла. Это также противоречит цели использования команд mv файловой системы, и вы можете запустить целевую файловую систему только из частично записанного файла.

    Это также не имеет ничего общего с реализацией Ruby. Системные команды mv и cp ведут себя одинаково.

Что предпочтительнее, так это открыть 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 в том же каталоге, что и отредактированный файл, здесь важно, чтобы избежать трансляции mv-операций между устройствами в операции cp, если /tmp смонтирован на другом устройстве. Вызов fdatasync - это дополнительный слой паранойи, но он повлечет за собой снижение производительности, поэтому я пропустил его в этом примере, так как это не практикуется на практике.

Ответ 4

На самом деле нет способа редактировать файлы на месте. Что вы обычно делаете, когда можете уйти от него (т.е. Если файлы не слишком большие), вы читаете файл в память (File.read), выполняете свои подстановки в строке чтения (String#gsub), а затем записываете измененная строка возвращается к файлу (File.open, File#write).

Если файлы достаточно велики для того, чтобы это было неосуществимо, что вам нужно сделать, читается файл в кусках (если шаблон, который вы хотите заменить, не будет охватывать несколько строк, то один фрагмент обычно означает одну строку - вы может использовать File.foreach для чтения файла по строкам), и для каждого фрагмента выполняйте подстановку на нем и добавьте его во временный файл. Когда вы закончите итерирование по исходному файлу, вы закроете его и используйте FileUtils.mv, чтобы перезаписать его временным файлом.

Ответ 5

Другим подходом является использование внутри редактирования внутри 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" на ".".

Ответ 6

Это работает для меня:

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

Ответ 7

Здесь находится решение для поиска/замены во всех файлах данного каталога. В основном я взял ответ, предоставленный 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

Ответ 8

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) }

Ответ 9

Если вам нужно выполнить замены по границам строк, то использование ruby -pi -e не будет работать, потому что p обрабатывает одну строку за раз. Вместо этого я рекомендую следующее, хотя это может быть неудачно с файлом с несколькими GB:

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

Ищет белое пространство (потенциально включая новые строки), которое следует за цитатой, и в этом случае оно избавляется от пробелов. %q(') - просто причудливый способ цитирования символа кавычки.

Ответ 10

Здесь альтернатива одному вкладышу из jim, на этот раз в script

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

Сохраните его в script, например 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