Megaparsec解析ls输出:如何在$标记前停止且不消耗标记?
解决Megaparsec解析Advent of Code 2022 Day7输入的问题
输入示例
$ cd / $ ls dir a 123 foo.txt dir d $ cd a $ ls dir e 4567 f 78901 h.zip $ cd e
期望解析数据结构
data DirContent = File (Integer, String) | Directory String deriving stock (Show) data Input = Cd String -- 原代码中DirectoryName应为String | CdRoot -- cd / | CdPop -- cd .. | Ls [DirContent] deriving stock (Show)
当前解析器代码
import Text.Megaparsec import Text.Megaparsec.Char parseFull :: Parsec Void Text [Input] parseFull = inputParser `sepBy` newline inputParser :: Parsec Void Text Input inputParser = CdRoot <$ string "$ cd /" <|> CdPop <$ string "$ cd .." <|> Cd <$> (string "$ cd " *> many alphaNumChar) <|> Ls <$> ((string "$ ls" <* newline) *> dirContentParser `sepBy` newline) dirContentParser :: Parsec Void Text DirContent dirContentParser = Directory <$> (string "dir " *> (pure <$> alphaNumChar)) <|> curry File <$> decimal <* char ' ' <*> many (alphaNumChar <|> char '.')
遇到的问题
当前解析器仅在输入包含单个ls命令且位于末尾时有效,否则会抛出错误:
input> IN: "$ cd /<newline>$ ls<newline>dir d<newli <…> input> MATCH (CERR): "$ cd /<newline>$ ls<newline>dir d<newli <…> input> ERROR: input> offset=33: input> unexpected "$ cd" input> expecting "dir " or integer
核心问题:ls分支的解析器不会在遇到下一个以$开头的命令时停止,且无法做到不消耗$标记的情况下终止解析。
解决方案
修改后的完整代码
import Text.Megaparsec import Text.Megaparsec.Char import Control.Applicative (optional) data DirContent = File (Integer, String) | Directory String deriving stock (Show) data Input = Cd String | CdRoot | CdPop | Ls [DirContent] deriving stock (Show) -- 前瞻检测是否为命令行开头(不消耗输入) isCommandLine :: Parsec Void Text () isCommandLine = void (lookAhead (string "$ ")) dirContentParser :: Parsec Void Text DirContent dirContentParser = -- 修复目录名解析:支持多字符目录名 Directory <$> (string "dir " *> many alphaNumChar) <|> curry File <$> decimal <* char ' ' <*> many (alphaNumChar <|> char '.') inputParser :: Parsec Void Text Input inputParser = CdRoot <$ string "$ cd /" <* optional newline <|> CdPop <$ string "$ cd .." <* optional newline <|> Cd <$> (string "$ cd " *> many alphaNumChar) <* optional newline <|> do string "$ ls" <* newline -- 收集内容直到文件结束,或遇到下一个命令行 contents <- manyTill dirContentParser (eof <|> (void newline *> isCommandLine)) pure $ Ls contents parseFull :: Parsec Void Text [Input] parseFull = many inputParser <* eof
关键修改说明
- 修复目录名解析:将原代码中
pure <$> alphaNumChar改为many alphaNumChar,支持解析多字符目录名。 - 添加前瞻检测解析器:
isCommandLine用lookAhead检测下一个输入是否为$,不消耗任何输入,确保遇到命令行时停止ls内容解析。 - 重写
ls分支逻辑:使用manyTill收集目录内容,终止条件为文件结束或换行后检测到命令行开头,避免将下一个命令行误判为ls结果。 - 处理命令行换行:给所有
cd相关解析器添加<* optional newline,确保命令行后的换行被正确消耗,避免残留换行干扰后续解析。 - 调整整体解析逻辑:将
parseFull改为many inputParser <* eof,让每个命令解析器自行处理换行和内容,更适配多命令场景。
修改后的解析器可以正确处理示例输入,生成符合预期的[Input]列表。
内容的提问来源于stack exchange,提问作者l7r7
相关产品推荐
相关产品推荐

