使用 parsec 解析子字符串(通过忽略不匹配的前缀)

Parse a sub-string with parsec (by ignoring unmatched prefixes)

我想从 git remote -v 的第一行提取存储库名称,通常是这样的形式:

origin git@github.com:some-user/some-repo.git (fetch)

我使用 parsec 快速制作了以下解析器:

-- | Parse the repository name from the output given by the first line of `git remote -v`.
repoNameFromRemoteP :: Parser String
repoNameFromRemoteP = do
    _ <- originPart >> hostPart
    _ <- char ':'
    firstPart <- many1 alphaNum
    _ <- char '/'
    secondPart <- many1 alphaNum
    _ <- string ".git"
    return $ firstPart ++ "/" ++ secondPart
    where
      originPart = many1 alphaNum >> space
      hostPart =  many1 alphaNum
               >> (string "@" <|> string "://")
               >> many1 alphaNum `sepBy` char '.'

但是这个解析器看起来有点笨拙。其实我只对冒号后面的东西感兴趣(":"),如果我能为它写一个解析器会更容易。

有没有办法让 parsec 在匹配失败时跳过一个字符,然后从下一个位置重试?

如果我理解了问题,请尝试 many (noneOf ":")。这将消耗任何字符,直到它看到 ':',然后停止。

编辑: 看来我没有理解这个问题。您可以使用 try 组合器将失败前可能会消耗一些字符的解析器转换为失败时不消耗任何字符的解析器。所以:

skipUntil p = try p <|> (anyChar >> skipUntil p)

请注意,这在运行时(因为它会尝试在每个位置匹配 p)和内存(因为 try 阻止 p 消耗字符和因此在 p 完成之前根本无法对输入进行垃圾回收)。您可以通过参数化 anyChar 位来缓解这两个问题中的第一个问题,以便调用者可以选择一些廉价的解析器来查找候选位置;例如

skipUntil p skipper = try p <|> (skipper >> skipUntil p skipper)

然后您可以潜在地使用上述 many (noneOf ":") 构造仅在以 :.

开头的位置上尝试 p

sepCap 组合器来自 replace-megaparsec 可以在匹配失败时跳过一个字符,然后从下一个位置重试。

对于您的特定情况,这可能有点矫枉过正,但它确实解决了 一般问题。

import Replace.Megaparsec
import Text.Megaparsec
import Text.Megaparsec.Char
import Data.Maybe
import Data.Either

username :: Parsec Void String String
username = do
    void $ single ':'
    some $ alphaNumChar <|> single '-'

listToMaybe . rights =<< parseMaybe (sepCap username)
    "origin git@github.com:some-user/some-repo.git (fetch)"
Just "some-user"