如何使嵌套的 megaparsec 解析器失败?

问题描述

我遇到了以下解析问题:

从有限的字符集中解析一些可能包含零个或多个元素的文本字符串,直到但不包括一组终止字符中的一个。应通过 Maybe 指示内容/无内容。终止字符可能以转义形式出现在字符串中。解析任何不可接受的字符都应该失败。

这是我想出的(简化):

import qualified Text.Megaparsec as MP

-- Predicate for admissible characters,not including the control characters.
isAdmissibleChar :: Char -> Bool
...

-- Predicate for control characters that need to be escaped.
isControlChar :: Char -> Bool
...

-- The escape character.
escChar :: Char
...


pComponent :: Parser (Maybe Text)
pComponent = do
  t <- MP.many (escaped <|> regular)
  if null t then return nothing else return $ Just (T.pack t)
 where
  regular = MP.satisfy isAdmissibleChar <|> fail "Inadmissible character"
  escaped = do
    _ <- MC.char escChar
    MP.satisfy isControlChar -- only control characters may be escaped

比如说,允许的字符是大写的ASCII,转义是'\',控制是':'。 然后,以下内容正确解析:ABC\:D:EF 以产生 ABC:D。 但是,解析 ABC&D(其中 & 是不可接受的)确实会产生 ABC,而我希望会出现错误消息。

两个问题:

  • 为什么 fail 会结束解析而不是让解析器失败?
  • 上述方法对于解决问题是否明智,或者是否有“适当”的规范方法来解析我不知道的此类终止字符串?

解决方法

many 必须允许它的子解析器在没有整个解析的情况下失败一次 失败 - 例如 many (char 'A') *> char 'B',在解析时 “AAAB”,必须无法解析 B 才能知道它到达了结尾

您可能需要 manyTill 来识别终止符 明确地。像这样:

MP.manyTill (escaped <|> regular) (MP.satisfy isControlChar)

假设 isControlChar 不接受“&”,“ABC&D”会在此处给出错误。

或者如果你想解析多个组件,你可以保留你的 pComponent 的现有定义并将其与 sepBy 或类似内容一起使用,例如:

MP.sepBy pComponent (MP.satisfy isControlChar)

如果您在此之后还检查文件结尾,例如:

MP.sepBy pComponent (MP.satisfy isControlChar) <* MP.eof

然后 "ABC&D" 应该再次报错,因为 '&' 将结束第一个组件但不会被接受为分隔符。

,

解析器对象通常所做的是从输入流中提取它应该接受的任何子集。这是通常的规则。

在这里,您似乎希望解析器接受跟随特定内容的字符串。从您的示例中,它是文件结尾 (eof) 或字符 ':'。因此,您可能需要考虑展望未来

环境及辅助功能:


import            Data.Void  (Void)
import qualified  Data.Text        as  T
import qualified  Text.Megaparsec  as  MP
import qualified  Text.Megaparsec.Char  as  MC

type Parser = MP.Parsec Void T.Text

-- Predicate for admissible characters,not including the control characters.
isAdmissibleChar :: Char -> Bool
isAdmissibleChar ch  =  elem ch ['A' .. 'Z']

-- Predicate for control characters that need to be escaped.
isControlChar :: Char -> Bool
isControlChar ch = elem ch ":"

-- The escape character:
escChar :: Char
escChar = '\\'

终止解析器,用于前瞻:

termination :: Parser ()
termination = MP.eof  MP.<|>  do
                                  _ <- MP.satisfy isControlChar
                                  return ()

修改后的 pComponent 解析器:

pComponent :: Parser (Maybe T.Text)
pComponent = do
    txt <- MP.many (escaped  MP.<|>  regular)
    MP.lookAhead  termination  --  **CHANGE HERE** 
    if (null txt)  then  (return Nothing)  else  (return $ Just (T.pack txt))
 where
   regular = (MP.satisfy isAdmissibleChar)  MP.<|>  (fail "Inadmissible character")
   escaped = do
     _ <- MC.char escChar
     MP.satisfy isControlChar -- only control characters may be escaped

测试实用程序:

tryParse :: String -> IO ()
tryParse str = do
    let  res = MP.parse  pComponent  "(noname)"  (T.pack str)
    putStrLn $ (show res)

让我们尝试重新运行您的示例:

$ ghci
 λ> 
 λ> :load q67809465.hs
 λ>
 λ> str1 = "ABC\\:D:EF"
 λ> putStrLn str1
 ABC\:D:EF
 λ> 
 λ> tryParse str1
 Right (Just "ABC:D")
 λ> 

这样就成功了,如您所愿。

 λ> 
 λ> tryParse "ABC&D"
Left (ParseErrorBundle {bundleErrors = TrivialError 3 (Just (Tokens ('&' :| ""))) (fromList [EndOfInput]) :| [],bundlePosState = PosState {pstateInput = "ABC&D",pstateOffset = 0,pstateSourcePos = SourcePos {sourceName = "(noname)",sourceLine = Pos 1,sourceColumn = Pos 1},pstateTabWidth = Pos 8,pstateLinePrefix = ""}})
 λ> 

因此,根据需要,失败了。

尝试我们的 2 个可接受的终止上下文:

 λ> tryParse "ABC:&D"
 Right (Just "ABC")
 λ> 
 λ> 
 λ> tryParse "ABCDEF"
 Right (Just "ABCDEF")
 λ> 

,

fail 通常不会结束解析。它只是继续下一个选择。在这种情况下,它选择由 many 组合器引入的空列表替代项,因此它会停止解析而不会显示错误消息。

我认为解决您的问题的最佳方法是指定输入必须以终止字符结尾,这意味着它不能像这样中途“成功”。您可以使用 notFollowedBylookAhead 组合符来做到这一点。 Here is the relevant part of the megaparsec tutorial