Como você usa o parsec de uma forma gananciosa?

9
No meu trabalho eu me deparo com um monte de sql gnarly, e eu tive a brilhante idéia de escrever um programa para analisar o sql e imprimi-lo ordenadamente. Eu fiz a maior parte rapidamente, mas me deparei com um problema que não sei como resolver.

Então, vamos fingir que o sql é "select foo from bar where 1". Meu pensamento era que sempre há uma palavra-chave seguida de dados para ela, então tudo o que tenho a fazer é analisar uma palavra-chave e, em seguida, capturar todas as sem sentido antes da próxima palavra-chave e armazená-las para uma limpeza posterior, se valer a pena. Aqui está o código:

import Text.Parsec
import Text.Parsec.Combinator
import Text.Parsec.Char
import Data.Text (strip)

newtype Statement = Statement [Atom]
data Atom = Branch String [Atom] | Leaf String deriving Show

trim str = reverse $ trim' (reverse $ trim' str)
  where
    trim' (' ':xs) = trim' xs
    trim' str = str

printStatement atoms = mapM_ printAtom atoms
printAtom atom = loop 0 atom 
  where
    loop depth (Leaf str) = putStrLn $ (replicate depth ' ') ++ str
    loop depth (Branch str atoms) = do 
      putStrLn $ (replicate depth ' ') ++ str
      mapM_ (loop (depth + 2)) atoms

keywords :: [String]
keywords = [
  "select",
  "update",
  "delete",
  "from",
  "where"]

keywordparser :: Parsec String u String
keywordparser = try ((choice $ map string keywords) <?> "keywordparser")

stuffparser :: Parsec String u String
stuffparser = manyTill anyChar (eof <|> (lookAhead keywordparser >> return ()))

statementparser = do
  key <- keywordparser
  stuff <- stuffparser
  return $ Branch key [Leaf (trim stuff)]
  <?> "statementparser"

tp = parse (many statementparser) ""

A chave aqui é o stuffparser. Esse é o material entre as palavras-chave que poderia ser qualquer coisa de listas de colunas para os critérios onde. Essa função captura todos os caracteres que levam a uma palavra-chave. Mas precisa de algo antes de terminar. E se houver uma subseleção? msgstr "selecione id, (selecione produto de produtos) da barra". Bem, nesse caso, se ele atinge essa palavra-chave, ele estraga tudo, analisa errado e estraga meu recuo. Também onde cláusulas podem ter parênteses também.

Então eu preciso mudar isso anyChar em outro combinador que absorva caracteres um de cada vez, mas também tenta procurar por parênteses, e se os encontrar, atravesse e capture tudo isso, mas também se houver mais parênteses, faça que até fecharmos completamente os parênteses, concatenar tudo e devolvê-lo. Aqui está o que eu tentei, mas não consigo fazer funcionar.

stuffparser :: Parsec String u String
stuffparser = fmap concat $ manyTill somechars (eof <|> (lookAhead keywordparser >> return ()))
  where
    somechars = parens <|> fmap (\c -> [c]) anyChar
    parens= between (char '(') (char ')') somechars

Isso causará um erro assim:

> tp "select asdf(qwerty) from foo where 1"
Left (line 1, column 14):
unexpected "w"
expecting ")"

Mas não consigo pensar em nenhuma maneira de reescrever isso para que funcione. Eu tentei usar manyTill na parte parêntese, mas acabei tendo problemas para obter typecheck quando eu tenho tanto string produzindo parens quanto single chars como alternativas. Alguém tem alguma sugestão sobre como fazer isso?

    
por David McHealy 18.07.2011 в 13:34
fonte

1 resposta

5

Sim, between pode não funcionar para o que você está procurando. É claro que, para o seu caso de uso, eu seguiria a sugestão do hammar e pegaria um analisador de SQL pronto para uso. (opinião pessoal: ou, tente não usar SQL a menos que você realmente precise; a idéia de usar strings para consultas de banco de dados foi um erro histórico).

Observação: eu adiciono um operador chamado <++> , que concatena os resultados de dois analisadores, sejam strings ou caracteres. (código na parte inferior).

Primeiro, para a tarefa de analisar parênteses: o nível superior analisará algumas coisas entre os caracteres relevantes, o que é exatamente o que o código diz,

parseParen = char '(' <++> inner <++> char ')'

Em seguida, a função inner deve analisar qualquer outra coisa: não-parentes, possivelmente incluindo outro conjunto de parênteses e lixo não parental que se segue.

parseParen = char '(' <++> inner <++> char ')' where
    inner = many (noneOf "()") <++> option "" (parseParen <++> inner)

Suponho que, para o resto da solução, o que você quer fazer é analisar a divisão de palavras por palavras-chave SQL de nível superior. (ou seja, ignorando aqueles entre parênteses). Ou seja, teremos um analisador que se comportará assim,

Main> parseTest parseSqlToplevel "select asdf(select m( 2) fr(o)m w where n) from b where delete 4"
[(Select," asdf(select m( 2) fr(o)m w where n) "),(From," b "),(Where," "),(Delete," 4")]

Suponha que tenhamos um analisador parseKw que obterá os gostos de select , etc. Depois que consumirmos uma palavra-chave, precisaremos ler até a próxima palavra-chave [de nível superior]. O último truque para minha solução é usar o lookAhead combinator para determinar se a próxima palavra é uma palavra-chave e colocá-la de volta em caso afirmativo. Se não for, consumimos um parêntese ou outro caractere e depois recapitulamos o resto.

-- consume spaces, then eat a word or parenthesis
parseOther = many space <++>
    (("" <$ lookAhead (try parseKw)) <|> -- if there's a keyword, put it back!
     option "" ((parseParen <|> many1 (noneOf "() \t")) <++> parseOther))

Minha solução inteira é a seguinte

-- overloaded operator to concatenate string results from parsers
class CharOrStr a where toStr :: a -> String
instance CharOrStr Char where toStr x = [x]
instance CharOrStr String where toStr = id
infixl 4 <++>
f <++> g = (\x y -> toStr x ++ toStr y) <$> f <*> g

data Keyword = Select | Update | Delete | From | Where deriving (Eq, Show)

parseKw =
    (Select <$ string "select") <|>
    (Update <$ string "update") <|>
    (Delete <$ string "delete") <|>
    (From <$ string "from") <|>
    (Where <$ string "where") <?>
    "keyword (select, update, delete, from, where)"

-- consume spaces, then eat a word or parenthesis
parseOther = many space <++>
    (("" <$ lookAhead (try parseKw)) <|> -- if there's a keyword, put it back!
     option "" ((parseParen <|> many1 (noneOf "() \t")) <++> parseOther))

parseSqlToplevel = many ((,) <$> parseKw <*> (space <++> parseOther)) <* eof

parseParen = char '(' <++> inner <++> char ')' where
    inner = many (noneOf "()") <++> option "" (parseParen <++> inner)

edit - versão com suporte às cotações

você pode fazer a mesma coisa que com os parens para apoiar citações,

import Control.Applicative hiding (many, (<|>))
import Text.Parsec
import Text.Parsec.Combinator

-- overloaded operator to concatenate string results from parsers
class CharOrStr a where toStr :: a -> String
instance CharOrStr Char where toStr x = [x]
instance CharOrStr String where toStr = id
infixl 4 <++>
f <++> g = (\x y -> toStr x ++ toStr y) <$> f <*> g

data Keyword = Select | Update | Delete | From | Where deriving (Eq, Show)

parseKw =
    (Select <$ string "select") <|>
    (Update <$ string "update") <|>
    (Delete <$ string "delete") <|>
    (From <$ string "from") <|>
    (Where <$ string "where") <?>
    "keyword (select, update, delete, from, where)"

-- consume spaces, then eat a word or parenthesis
parseOther = many space <++>
    (("" <$ lookAhead (try parseKw)) <|> -- if there's a keyword, put it back!
     option "" ((parseParen <|> parseQuote <|> many1 (noneOf "'() \t")) <++> parseOther))

parseSqlToplevel = many ((,) <$> parseKw <*> (space <++> parseOther)) <* eof

parseQuote = char '\'' <++> inner <++> char '\'' where
    inner = many (noneOf "'\") <++>
        option "" (char '\' <++> anyChar <++> inner)

parseParen = char '(' <++> inner <++> char ')' where
    inner = many (noneOf "'()") <++>
        (parseQuote <++> inner <|> option "" (parseParen <++> inner))

Eu tentei com parseTest parseSqlToplevel "select ('a(sdf'())b" . aplausos

    
por gatoatigrado 19.07.2011 / 06:35
fonte