Я ищу script для поиска файла (или списка файлов) для шаблона и, если он найден, заменит этот шаблон на заданное значение.
Мысли?
Я ищу script для поиска файла (или списка файлов) для шаблона и, если он найден, заменит этот шаблон на заданное значение.
Мысли?
Отказ от ответственности: этот подход является наивной иллюстрацией возможностей 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
Собственно, Ruby имеет функцию редактирования на месте. Как и Perl, вы можете сказать
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Это применит код в двойных кавычках ко всем файлам в текущем каталоге, чьи имена заканчиваются на ".txt". Резервные копии отредактированных файлов будут созданы с расширением ".bak" ( "foobar.txt.bak", я думаю).
ПРИМЕЧАНИЕ. Это не работает для многострочного поиска. Для них вы должны сделать это другим менее привлекательным способом, с оболочкой script вокруг регулярного выражения.
Имейте в виду, что при этом файловой системе может не хватать места, и вы можете создать файл нулевой длины. Это катастрофически, если вы делаете что-то вроде записи файлов /etc/passwd как часть управления конфигурацией системы.
[РЕДАКТИРОВАТЬ: обратите внимание, что редактирование файла на месте, как в принятом ответе, всегда будет обрезать файл и записывать новый файл последовательно. Всегда будет состояние гонки, при котором одновременные читатели увидят усеченный файл. Если процесс прерывается по какой-либо причине (ctrl-c, OOM killer, сбой системы, отключение питания и т.д.) Во время записи, то усеченный файл также будет оставлен, что может иметь катастрофические последствия. Это тот сценарий потери данных, который разработчики ДОЛЖНЫ рассмотреть, потому что это произойдет. По этой причине я думаю, что принятый ответ, скорее всего, не должен быть принятым ответом. Как минимум, напишите в временный файл и переместите/переименуйте файл на место, как "простое" решение в конце этого ответа.]
Вам нужно использовать алгоритм, который:
читает старый файл и записывает в новый файл. (Вы должны быть осторожны с тем, чтобы копать целые файлы в память).
явно закрывает новый временный файл, где вы можете выдать исключение, потому что файловые буферы не могут быть записаны на диск, потому что нет места. (Поймайте это и очистите временный файл, если хотите, но вам нужно что-то перебросить или довольно сильно потерпеть неудачу на этом этапе.
исправляет права доступа к файлу и режимы для нового файла.
переименовывает новый файл и помещает его на место.
В файловых системах ext3 вам гарантируется, что запись метаданных для перемещения файла на место не будет переставлена файловой системой и записана до того, как буферы данных для нового файла будут записаны, поэтому это должно либо завершиться успешно, либо завершиться неудачно. Файловая система ext4 также была исправлена для поддержки такого поведения. Если вы очень параноик, вы должны вызвать системный вызов fdatasync()
как шаг 3.5, прежде чем перемещать файл на место.
Независимо от языка, это лучшая практика. В языках, где вызов close()
не вызывает исключение (Perl или C), вы должны явно проверять возврат close()
и генерировать исключение в случае сбоя.
Приведенное выше предложение просто залить файл в память, манипулировать им и записать его в файл, будет гарантированно создавать файлы нулевой длины в полной файловой системе. Вам всегда нужно использовать FileUtils.mv
чтобы переместить полностью записанный временный файл на место.
Окончательное рассмотрение - размещение временного файла. Если вы откроете файл в /tmp, вам придется рассмотреть несколько проблем:
Возможно, что еще более важно, когда вы попытаетесь mv
файл через устройство, вы будете преобразованы в поведение cp
. Старый файл будет открыт, старый файл Inode будет сохранен и снова открыт, а содержимое файла будет скопировано. Скорее всего, это не то, что вам нужно, и вы можете столкнуться с ошибками "текстовый файл занят", если вы попытаетесь редактировать содержимое запущенного файла. Это также противоречит цели использования команд mv
файловой системы, и вы можете запустить целевую файловую систему только из частично записанного файла.
Это также не имеет ничего общего с реализацией Ruby. Системные команды mv
и cp
ведут себя одинаково.
Что предпочтительнее, так это открыть Tempfile в том же каталоге, что и старый файл. Это гарантирует, что не будет проблем с перемещением между устройствами. Сам mv
никогда не должен выходить из строя, и вы всегда должны получить полный и не усеченный файл. Любые сбои, такие как нехватка устройства, ошибки прав доступа и т.д., Должны возникать во время записи Tempfile.
Единственными недостатками подхода к созданию 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: это следует использовать как минимум вместо принятого ответа во всех случаях, чтобы гарантировать, что обновление является атомарным, и одновременные читатели не будут видеть усеченные файлы. Как я упоминал выше, создание Tempfile в том же каталоге, что и отредактированный файл, здесь важно, чтобы избежать трансляции mv-операций между устройствами в операции cp, если /tmp смонтирован на другом устройстве. Вызов 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" на ".".
Это работает для меня:
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
обрабатывает одну строку за раз. Вместо этого я рекомендую следующее, хотя это может быть неудачно с файлом с несколькими GB:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Ищет белое пространство (потенциально включая новые строки), которое следует за цитатой, и в этом случае оно избавляется от пробелов. %q(')
- просто причудливый способ цитирования символа кавычки.
Здесь альтернатива одному вкладышу из 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