Ruby parslet: разбор нескольких строк
Я ищу способ сочетания нескольких строк Parslet. Код выглядит так:
rule(:line) { (match('$').absent? >> any).repeat >> match('$') }
rule(:lines) { line.repeat }
Тем не мение, lines
всегда будет в конечном итоге в бесконечном цикле, потому что match('$')
будет бесконечно повторяться, чтобы соответствовать концу строки.
Можно ли сопоставить несколько строк, которые могут быть пустыми?
irb(main)> lines.parse($stdin.read)
This
is
a
multiline
string^D
должен соответствовать успешно. Я что-то пропустил? Я тоже пробовал (match('$').absent? >> any.maybe).repeat(1) >> match('$')
но это не соответствует пустым строкам.
С Уважением,
Danyel.
2 ответа
Я думаю, что у вас есть две проблемы, связанные с вашим соответствием:
Псевдосимвольное совпадение
$
не потребляет никаких реальных символов. Вам все еще нужно как-то использовать новые строки.Parslet каким-то образом обманывает ввод, делая
$
совпадать в местах, которые вы не можете ожидать. Лучший результат, который я мог получить, используя$
в конечном итоге соответствует каждому отдельному персонажу.
Гораздо безопаснее в использовании \n
как символ конца строки. Я получил следующее на работу (я сам несколько новичок в Parslet, поэтому извиняюсь, если это может быть яснее):
require 'parslet'
class Lines < Parslet::Parser
rule(:text) { match("[^\n]") }
rule(:line) { ( text.repeat(0) >> match("\n") ) | text.repeat(1) }
rule(:lines) { line.as(:line).repeat }
root :lines
end
s = "This
is
a
multiline
string"
p Lines.new.parse( s )
Правило для строки является сложным из-за необходимости сопоставления пустых строк и возможной финальной строки без \n
,
Вам не нужно использовать .as(:line)
синтаксис - я просто добавил его, чтобы ясно показать, что :line
правило соответствует каждой строке индивидуально, а не просто потребляет весь ввод.
Я обычно определяю правило для end_of_line. Это основано на уловке в http://kschiess.github.io/parslet/tricks.html для сопоставления end_of_file.
class MyParser < Parslet::Parser
rule(:cr) { str("\n") }
rule(:eol?) { any.absent? | cr }
rule(:line_body) { (eol?.absent? >> any).repeat(1) }
rule(:line) { cr | line_body >> eol? }
rule(:lines?) { line.repeat (0)}
root(:lines?)
end
puts MyParser.new.parse(""" this is a line
so is this
that was too
This ends""").inspect
Очевидно, что если вы хотите сделать с парсером больше, чем вы можете достичь с помощью String::split("\n"), вы замените line_body
с чем-то полезным:)
Я быстро решил ответить на этот вопрос и все испортил. Я просто хотел объяснить ошибку, которую я совершил, и показать вам, как избежать ошибок такого рода.
Вот мой первый ответ.
rule(:eol) { str('\n') | any.absent? }
rule(:line) { (eol.absent? >> any).repeat >> eol }
rule(:lines) { line.as(:line).repeat }
Я не следовал своим обычным правилам:
- Всегда делайте количество повторений явным
- Любое правило, которое может соответствовать строкам нулевой длины, должно иметь имя, оканчивающееся на '?'
Итак, давайте применим эти...
rule(:eol?) { str('\n') | any.absent? }
# as the second option consumes nothing
rule(:line?) { (eol.absent? >> any).repeat(0) >> eol? }
# repeat(0) can consume nothing
rule(:lines?) { line.as(:line?).repeat(0) }
# We have a problem! We have a rule that can consume nothing inside a `repeat`!
Вот видите, почему мы получаем бесконечный цикл. Поскольку вход потребляется, вы в конечном итоге только end of file
, который соответствует eol?
и поэтому line?
(так как тело строки может быть пустым). Быть внутри lines
' repeat
, он продолжает соответствовать, ничего не потребляя и зацикливается навсегда.
Нам нужно изменить правило линии, чтобы оно всегда что-то потребляло.
rule(:cr) { str('\n') }
rule(:eol?) { cr | any.absent? }
rule(:line_body) { (eol.absent? >> any).repeat(1) }
rule(:line) { cr | line_body >> eol? }
rule(:lines?) { line.as(:line).repeat(0) }
Сейчас line
должен соответствовать чему-либо, либо cr
(для пустых строк) или хотя бы один символ, за которым следует необязательный eol?
, Все repeat
У них есть тела, которые что-то потребляют. Мы сейчас золотые.