使用FParsec,如何在解析器之间使用manyCharsTill而不使结束字符串失败?

时间:2018-08-10 01:00:26

标签: parsing f# fparsec

我正在尝试使用FParsec来解析TOML多行字符串,并且在使用结束定界符(""")时遇到了麻烦。我有以下解析器:

let controlChars = 
    ['\u0000'; '\u0001'; '\u0002'; '\u0003'; '\u0004'; '\u0005'; '\u0006'; '\u0007';
     '\u0008'; '\u0009'; '\u000a'; '\u000b'; '\u000c'; '\u000d'; '\u000e'; '\u000f';
     '\u0010'; '\u0011'; '\u0012'; '\u0013'; '\u0014'; '\u0015'; '\u0016'; '\u0017';
     '\u0018'; '\u0019'; '\u001a'; '\u001b'; '\u001c'; '\u001d'; '\u001e'; '\u001f';
     '\u007f']

let nonSpaceCtrlChars =
    Set.difference (Set.ofList controlChars) (Set.ofList ['\n';'\r';'\t'])

let multiLineStringContents : Parser<char,unit> =
    satisfy (isNoneOf nonSpaceCtrlChars)

let multiLineString         : Parser<string,unit> =
    optional newline >>. manyCharsTill multiLineStringContents (pstring "\"\"\"")
    |> between (pstring "\"\"\"") (pstring "\"\"\"") 

let test parser str =
    match run parser str with
    | Success (s1, s2, s3) -> printfn "Ok: %A %A %A" s1 s2 s3
    | Failure (f1, f2, f3) -> printfn "Fail: %A %A %A" f1 f2 f3

当我针对这样的输入测试multiLineString时:

test multiLineString "\"\"\"x\"\"\""

解析器因以下错误而失败:

  

失败:“ Ln错误:1列:8”“” x“”“          ^注:错误发生在输入流的末尾。期望:““”“'

对此我感到困惑。 manyCharsTill multiLineStringContents (pstring "\"\"\"")解析器是否会在"""解析器处在between停下来找到它?为什么解析器会吃掉所有输入,然后使between解析器失败?

这似乎是一篇相关的文章:How to parse comments with FParsec

但是我看不出该解决方案与我在这里所做的事情有什么不同。

2 个答案:

答案 0 :(得分:2)

manyCharsTill documentation说(强调我):

  

manyCharsTill cp endp用字符解析器cp解析字符,直到解析器endp成功。它在endp 之后停止,并以字符串形式返回解析的字符。

因此,您不想将betweenmanyCharsTill结合使用;您想做类似pstring "\"\"\"" >>. manyCharsTill (pstring "\"\"\"")的事情。

但是碰巧的是,我可以为您节省很多工作。我在业余时间一直在与FParsec一起开发TOML解析器。它还远未完成,但是字符串部分可以正常工作并正确处理反斜杠转义(据我所知:我已经进行了彻底的测试,但还没有穷尽全部测试)。我唯一缺少的是“条状第一换行符,如果它出现在分隔符之后”,您已经使用optional newline处理了该规则。因此,只需将该位添加到下面的我的代码中,您应该就可以使用TOML字符串解析器。

顺便说一句,我打算在MIT许可下许可我的代码(如果完成的话)。因此,我特此在MIT许可下发布以下代码块。如果对您有用,请随时在项目中使用它。

let pShortCodepointInHex = // Anything from 0000 to FFFF, *except* the range D800-DFFF
    (anyOf "dD" >>. (anyOf "01234567" <?> "a Unicode scalar value (range D800-DFFF not allowed)") .>>. exactly 2 isHex |>> fun (c,s) -> sprintf "d%c%s" c s)
    <|> (exactly 4 isHex <?> "a Unicode scalar value")

let pLongCodepointInHex = // Anything from 00000000 to 0010FFFF, *except* the range D800-DFFF
        (pstring "0000" >>. pShortCodepointInHex)
        <|> (pstring "000"  >>. exactly 5 isHex)
        <|> (pstring "0010" >>. exactly 4 isHex |>> fun s -> "0010" + s)
        <?> "a Unicode scalar value (i.e., in range 00000000 to 0010FFFF)"

let toCharOrSurrogatePair p =
    p |> withSkippedString (fun codePoint _ -> System.Int32.Parse(codePoint, System.Globalization.NumberStyles.HexNumber) |> System.Char.ConvertFromUtf32)

let pStandardBackslashEscape =
    anyOf "\\\"bfnrt"
    |>> function
        | 'b' -> "\b"      // U+0008 BACKSPACE
        | 'f' -> "\u000c"  // U+000C FORM FEED
        | 'n' -> "\n"      // U+000A LINE FEED
        | 'r' -> "\r"      // U+000D CARRIAGE RETURN
        | 't' -> "\t"      // U+0009 CHARACTER TABULATION a.k.a. Tab or Horizonal Tab
        | c   -> string c

let pUnicodeEscape =     (pchar 'u' >>. (pShortCodepointInHex |> toCharOrSurrogatePair))
                     <|> (pchar 'U' >>. ( pLongCodepointInHex |> toCharOrSurrogatePair))

let pEscapedChar = pstring "\\" >>. (pStandardBackslashEscape <|> pUnicodeEscape)

let quote = pchar '"'
let isBasicStrChar c = c <> '\\' && c <> '"' && c > '\u001f' && c <> '\u007f'
let pBasicStrChars = manySatisfy isBasicStrChar
let pBasicStr = stringsSepBy pBasicStrChars pEscapedChar |> between quote quote

let pEscapedNewline = skipChar '\\' .>> skipNewline .>> spaces
let isMultilineStrChar c = c = '\n' || isBasicStrChar c
let pMultilineStrChars = manySatisfy isMultilineStrChar


let pTripleQuote = pstring "\"\"\""

let pMultilineStr = stringsSepBy pMultilineStrChars (pEscapedChar <|> (notFollowedByString "\"\"\"" >>. pstring "\"")) |> between pTripleQuote pTripleQuote

答案 1 :(得分:1)

@rmunn提供了正确的答案,谢谢!在使用FParsec API多一点之后,我还以稍微不同的方式解决了这个问题。正如另一个答案所解释的那样,endp的{​​{1}}参数正在吃掉结尾的manyCharTill,所以我需要切换到不这样做的位置。使用"""的简单修改就可以达到目的:

lookAhead