- Create a new project ini-parser using the simple Stack template:
stack new ini-parser simple
- Open the file src/Main.hs for editing.
- After the initial module header, add the following imports:
import Data.Functor
import Control.Applicative
import Control.Monad
import Data.Map hiding (empty)
import Data.Char
- Define the INI file data structure. Represent name-value pairs in each section (variables) as a map of name to values. Both name and values are represented by strings. The sections inside an INI file are a map between the section name and variables for each section:
type Variables = Map String String
type Sections = Map String Variables
newtype INI = INI Sections
- Start defining the parser. A parser is defined as follows:
data Parser a = Parser { runParser :: String -> Maybe (a,
String) }
The parser is represented as a data type around a function runParser that takes a string as input and generates a tuple (a String) where a is the type of the value parsed by the parser. The second member of the tuple, a string, represents the remaining input after parsing the value of type a. Since we can fail during parsing, we use Maybe so that we can represent either tuple or nothing.
- If we have a parser of type a, then we can define a Functor instance. If we apply a function of type a -> b, we can convert Parser a into Parser b. Define the Functor instance as follows:
instance Functor Parser where
fmap f (Parser p) =
let parserfunc input = do
(x, remaining) <- p input -- run supplied parser against
input
return (f x, remaining) -- apply function on parsed value
in Parser parserfunc
- Similarly, we can define the Applicative instance for our parser as follows:
instance Applicative Parser where
pure x = let parserfunc input = Just (x, input)
in Parser parserfunc
(Parser pf) <*> (Parser pa) =
let parserfunc input = do
(f, remaining) <- pf input
(a, remaining2) <- pa remaining
return (f a, remaining2)
in Parser parserfunc
- Now, we can go ahead with the monad instance for our parser:
instance Monad Parser where
return = pure
(Parser pa) >>= fab =
let parsefunc input = do
(a, remaining) <- pa input
runParser (fab a) remaining
in Parser parsefunc
It runs a parser on input, producing certain output. Then, it feeds this output to another parser, with the remaining input to continue parsing.
- We will now implement an Alternative instance for our parser. The Alternative is a logical extension of Applicative, where it allows us to define an empty (or complementary to pure) case for our data type, and if we have two values of data types, we can go with the second one if the first one is empty. Define the Alternative instance for our parser now:
instance Alternative Parser where
empty = Parser (\_ -> Nothing )
(Parser pa) <|> (Parser pb) =
let parsefunc input = case pa input of
Nothing -> pb input
Just (x, remaining) -> Just (x, remaining)
in Parser parsefunc
-- return a list of v for which v satisfies. The list should
satisfy at least one v.
some v =
let parsefunc input = do
(x, remaining) <- runParser v input
(xs, remaining2) <- runParser (many v) remaining
return (x:xs, remaining2)
in Parser parsefunc
-- return a list of v for which v satisfies, the list can
satisfy zero or more v.
many v =
let parsefunc input = case runParser (some v) input of
Just (xs, remaining) -> Just (xs,
remaining)
Nothing -> Just ([], input)
in Parser parsefunc
The definition of some and many is interesting. In the parsing context, some matches at least one value, whereas many matches zero or more values parsed by the supplied parser.
- The basic machinery for parsing is now done. Now, start writing concrete parsing functions. If the parser fails, the parser function should return Nothing; otherwise, it should return value successfully parsed and the remaining input.
- The first function to be used is a conditional character parser. If the character meets certain criteria, we will return the character as a successfully parsed value:
conditional :: (Char -> Bool) -> Parser Char
conditional f =
let parsefunc [] = Nothing -- Input is empty, nothing to
produce
parsefunc (x:xs) | f x = Just (x, xs) -- We got a match,
produce output
parsefunc _ = Nothing -- No match, just fail.
in Parser parsefunc
Use the conditional parser to implement a parser to match the given character:
char :: Char -> Parser Char
char c = conditional (== c)
- We will implement the bracketed parser. We are interested in the enclosed value (such as within open and closed parenthesis and without parenthesis):
bracketed :: Parser a -> Parser b -> Parser c -> Parser b
bracketed pa pb pc = do
pa -- match first parser, but ignore value
b <- pb -- interested in value parsed by pb
pc -- match end parser, again ignoring value
return b -- return second value
- Now, implement a bunch of parsers to match square bracket characters, alpha-numeric characters, and white spaces. We use many with white space to match one or more white spaces:
bracketOpen :: Parser Char
bracketOpen = char '['
bracketClose :: Parser Char
bracketClose = char ']'
alphanum :: Parser Char
alphanum = conditional isAlphaNum
isWhiteSpace :: Char -> Bool
isWhiteSpace ' ' = True
isWhiteSpace '\t' = True
isWhiteSpace _ = False
whitespace :: Parser Char
whitespace = conditional isWhiteSpace
whitespaces :: Parser String
whitespaces = many whitespace
- SectionHeader is the section name enclosed in brackets:
sectionName :: Parser String
sectionName = bracketed whitespaces (some alphanum) whitespaces
sectionHeader :: Parser String
sectionHeader = bracketed bracketOpen sectionName bracketClose
- A name is some alpha-numeric identifier. The value can either be alpha numeric or may be a quoted value (which can have spaces):
name :: Parser String
name = (some alphanum)
quote :: Parser Char
quote = char '\"'
-- allow alpha numeric and white space characters
quotedchar :: Parser Char
quotedchar = conditional (\c -> isAlphaNum c || isWhiteSpace c)
quotedvalue :: Parser String
quotedvalue = bracketed quote (many quotedchar) quote
value :: Parser String
value = name <|> quotedvalue
An assignment is a name-value pair separated by the = character. We ignore the white spaces around these:
assignment :: Parser (String,String)
assignment = do
whitespaces
name <- name
whitespaces
char '='
whitespaces
value <- value
return (name, value)
- Finally, write a parser for a section. A section has a section header and name-value pairs separated by newline:
newline :: Parser Char
newline = conditional (\c -> c == '\r' || c == '\n' )
newlines :: Parser ()
newlines = many newline >> return ()
blank :: Parser ()
blank = whitespaces >> newline >> return ()
blanks :: Parser ()
blanks = many blank >> return ()
assignments :: Parser Variables
assignments = fromList <$> many (blanks >> assignment)
section :: Parser (String, Variables)
section = do
blanks
whitespaces
name <- sectionHeader
blanks
variables <- assignments
return (name, variables)
- The INI file is a list of many sections. Use Functor fmap to do the job of converting many sections into the INI data type:
ini :: Parser INI
ini = (INI . fromList) <$> many section
- Finally, write the main function to parse an INI file:
main :: IO ()
main = do
args <- getArgs
contents <- readFile (head args)
case runParser ini contents of
Just inicontents -> print inicontents
Nothing -> putStrLn "Could not parse INI file"
- Build the project. Then, create a sample.ini file with the following contents and run it against the project:
stack build
stack exec -- ini-parser sample.ini
- The sample file is shown here:
[section]
name = value
name2 = "quoted value"
[ section2]
name = value
name2 = "quoted value 2"
- The output should look like this:
