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('$'), но это не соответствует пустым строкам.

С уважением,
Даниэль.


person Danyel    schedule 18.07.2013    source источник


Ответы (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 соответствует каждой строке по отдельности, а не просто потребляет весь ввод.

person Neil Slater    schedule 18.07.2013
comment
Это выглядит как хорошее решение. Мой обходной путь заключался в том, чтобы работать и с \n, и добавлять новую строку во входящую строку, чтобы предотвратить сбой совпадения в конце. Однако это выглядит чище. Спасибо! - person Danyel; 19.07.2013

Обычно я определяю правило для end_of_line. Это основано на трюке в http://kschiess.github.io/parslet/tricks.html для сопоставления конец_файла.

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 есть тела, которые что-то потребляют. Теперь мы золотые.

person Nigel Thorne    schedule 24.07.2013
comment
Это превращается для меня в бесконечный цикл. - person Danyel; 26.07.2013
comment
ой. да я это исправлю. - person Nigel Thorne; 28.07.2013
comment
Бесконечные циклы возникают, когда у вас есть правила, которые могут сопоставляться без каких-либо входных данных. Здесь line соответствует пустой строке, за которой следует any.absent? версия eol, которая также ничего не потребляет, поэтому может продолжать соответствовать. - person Nigel Thorne; 28.07.2013