Я ищу сценарий для поиска в файле (или списке файлов) шаблона и, если он найден, заменяю этот шаблон заданным значением.
Мысли?
Я ищу сценарий для поиска в файле (или списке файлов) шаблона и, если он найден, заменяю этот шаблон заданным значением.
Мысли?
Ответы:
Отказ от ответственности: этот подход является наивной иллюстрацией возможностей 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 'для main: Object (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
поведение. Старый файл будет открыт, индексный дескриптор старых файлов будет сохранен и повторно открыт, а содержимое файла будет скопировано. Скорее всего, это не то, что вам нужно, и вы можете столкнуться с ошибкой «текстовый файл занят», если попытаетесь отредактировать содержимое работающего файла. Это также противоречит цели использования mv
команд файловой системы, и вы можете запустить целевую файловую систему из-за недостатка места только с частично записанным файлом.
Это также не имеет ничего общего с реализацией Ruby. Система mv
и cp
команды ведут себя аналогично.
Более предпочтительно открыть временный файл в том же каталоге, что и старый файл. Это гарантирует, что не возникнет проблем с перемещением между устройствами. Сам по mv
себе никогда не должен выходить из строя, и вы всегда должны получать полный и не усеченный файл. Любые сбои, такие как нехватка места на устройстве, ошибки разрешений и т. Д., Должны возникать во время записи временного файла.
Единственными недостатками подхода к созданию временного файла в целевом каталоге являются:
Вот код, реализующий полный алгоритм (код 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 : как минимум, это следует использовать вместо принятого ответа во всех случаях, чтобы гарантировать, что обновление является атомарным и одновременные читатели не увидят усеченные файлы. Как я упоминал выше, создание временного файла в том же каталоге, что и отредактированный файл, важно здесь, чтобы избежать преобразования mv-операций между устройствами в операции cp, если / tmp смонтирован на другом устройстве. Вызов fdatasync - это дополнительный уровень паранойи, но он приведет к снижению производительности, поэтому я пропустил его в этом примере, поскольку он обычно не практикуется.
На самом деле нет способа редактировать файлы на месте. Что вы обычно делаете, когда это может сойти с рук (например, если файлы не слишком большие), вы читаете файл в memory ( 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
) для файла. Он масштабируемый и должен быть очень быстрым.
Это работает для меня:
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) }
Если вам нужно выполнить замену через границы строк, то использование ruby -pi -e
не будет работать, потому что p
обрабатывается одна строка за раз. Вместо этого я рекомендую следующее, хотя это может привести к сбою с файлом размером в несколько ГБ:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Ищет пробелы (потенциально включая новые строки), за которыми следует кавычка, и в этом случае он избавляется от пробелов. Это %q(')
просто причудливый способ цитирования символа кавычки.
Вот альтернатива одному вкладышу от Джима, на этот раз в скрипте
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
РЕДАКТИРОВАТЬ: если вы хотите использовать регулярное выражение, используйте это вместо этого Очевидно, это только для обработки относительно небольших текстовых файлов, без монстров Gigabyte
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)
.