Как найти текст файла для шаблона и заменить его заданным значением
Я ищу скрипт для поиска файла (или списка файлов) по шаблону и, если найден, заменим этот шаблон с заданным значением.
Мысли?
11 ответов
Вот быстрый короткий способ сделать это.
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").
ПРИМЕЧАНИЕ: это не работает для многострочного поиска. Для них вы должны сделать это другим, менее привлекательным способом, с помощью сценария-оболочки вокруг регулярного выражения.
Имейте в виду, что при этом файловой системе может не хватать места, и вы можете создать файл нулевой длины. Это катастрофически, если вы делаете что-то вроде записи файлов /etc/passwd как часть управления конфигурацией системы.
[РЕДАКТИРОВАТЬ: обратите внимание, что редактирование файла на месте, как в принятом ответе, всегда будет обрезать файл и записывать новый файл последовательно. Всегда будет состояние гонки, при котором одновременные читатели увидят усеченный или частично усеченный файл, который может быть катастрофическим. По этой причине я думаю, что принятый ответ, скорее всего, не должен быть принятым ответом. ]
Вам нужно использовать алгоритм, который:
читает старый файл и записывает в новый файл. (Вы должны быть осторожны с тем, чтобы копать целые файлы в память).
явно закрывает новый временный файл, где вы можете выдать исключение, потому что файловые буферы не могут быть записаны на диск, потому что нет места. (Поймайте это и очистите временный файл, если хотите, но вам нужно что-то перебросить или достаточно сильно потерпеть неудачу на этом этапе.
исправляет права доступа к файлу и режимы для нового файла.
переименовывает новый файл и помещает его на место.
В файловых системах 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 - это дополнительный слой паранойи, но он повлечет за собой снижение производительности, поэтому я пропустил его в этом примере, так как это не практикуется на практике.
На самом деле нет способа редактировать файлы на месте. То, что вы обычно делаете, когда вы можете сойти с рук (то есть, если файлы не слишком большие), вы читаете файл в память (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) }
Вот альтернатива одному вкладышу от jim, на этот раз в скрипте
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
Если вам нужно сделать замены через границы строк, то с помощью ruby -pi -e
не будет работать, потому что p
обрабатывает одну строку за раз. Вместо этого я рекомендую следующее, хотя это может привести к сбою в файле размером несколько ГБ:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Ищет пробел (возможно, включая новые строки), после которого следует кавычка, и в этом случае он избавляется от пробела. %q(')
это просто модный способ процитировать цитату.
Я использую гем tty-file
Помимо замены, он включает добавление, добавление (для заданного текста / регулярного выражения внутри файла), diff и другие.