Я шукаю сценарій для пошуку у файлі (або списку файлів) для шаблону і, якщо його знайдено, замінюю цей шаблон на задане значення.
Думки?
Я шукаю сценарій для пошуку у файлі (або списку файлів) для шаблону і, якщо його знайдено, замінюю цей шаблон на задане значення.
Думки?
Відповіді:
Відмова: Цей підхід є наївною ілюстрацією можливостей 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 'for 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
поведінку. Старий файл буде відкрито, старий файл inode буде збережено та відкрито, а вміст файлу буде скопійовано. Це, швидше за все, не те, що ви хочете, і ви можете зіткнутися з помилками "текстовий файл зайнятий", якщо спробуєте відредагувати вміст запущеного файлу. Це також перешкоджає використанню mv
команд файлової системи, і ви можете запустити цільову файлову систему з простору лише лише частково записаним файлом.
Це також не має нічого спільного з реалізацією Ruby. Система mv
та cp
команди поводяться аналогічно.
Більш переважно - це відкривати Tempfile у тому самому каталозі, що і старий файл. Це гарантує, що проблем із переміщенням між пристроями не виникне. mv
Сама ніколи не вийде з ладу, і ви завжди повинні отримувати повну та untruncated файл. Будь-які збої, такі як пристрій у просторі, помилки дозволу тощо, повинні виникати під час виходу темпфіла.
Єдиними недоліками підходу до створення темпфіла в каталозі призначення є:
Ось код, який реалізує повний алгоритм (код 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'
до ''
.
read
файл slurp ( ). Це масштабується і має бути дуже швидким.
Це працює для мене:
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(')
просто фантазійний спосіб цитування символу цитати.
Тут альтернатива одному вкладиші від Jim, цього разу в сценарії
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
Збережіть його у сценарії, наприклад, замініть.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
EDIT: якщо ви хочете використовувати звичайний вираз, використовуйте це замість цього. Очевидно, це лише для обробки відносно невеликих текстових файлів, без гігабайтних монстрів
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)
.