packages feed

parse-help-0.0: System/Console/ParseHelp.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module  System.Console.ParseHelp (
  parseHelp, ParsedHelp, ParsedFlag(..)
) where

import Data.Attoparsec.Text
import Control.Applicative ((<|>), many)
import Data.Text (Text, unpack, pack)
import qualified Data.Text as T
import Language.Haskell.TH.Lift
import Prelude hiding (takeWhile)

instance Lift Text where lift = lift . unpack

data ParsedFlag = ParsedFlag {
   parsedFlagShort       :: Maybe Char, 
   parsedFlagLong        :: Maybe Text,
   parsedFlagType        :: Maybe Text,
   parsedFlagDescription :: Maybe Text
 } deriving (Show)
deriveLift ''ParsedFlag

type ParsedMode = (Text, -- ^ program name
                   Text, -- ^ mode name
                  [ParsedFlag])
type ParsedCommonMode = (Text, [ParsedFlag]) -- no mode name


parseHelp :: String -> Either String ParsedHelp
parseHelp s =
  case attoParser (pack s) of
    Left e -> Left e
    Right pHelp -> Right $ prepareParsedHelp pHelp

attoParser :: Text -> Either String (Maybe Text,           -- ^ program description
                                    Maybe ParsedCommonMode,
                                    [ParsedMode])
attoParser input = parseOnly commandArgs input
  where
    commandArgs = do
      description <- maybeManyTill anyChar blankLine
      optional blankLines
      common <- optionMaybe commonInvocation
      optional blankLines
      modes <- many modeInvocation
      return (description, common, modes)
      where
        spaces = skipSpace
        commonInvocation = do
          -- sample [OPTIONS]
          name <- commonProgName
          optional blankLines
          spaces
          -- optional blankLines
          -- optional $ takeWhile (notInClass "\r\n") >> eol'
          optional blankLines
          optional $ string "Common flags" >> takeWhile (notInClass "-\r\n") >> eol'
          spaces
          optional blankLines
          opts <- flags
          return (name, opts)

        modeInvocation = do
          (name, mode) <- progName
          optional blankLines
          opts <- flags
          return (name, mode, opts)

        betweenChars start end = do
          _ <- char start
          r <- takeWhile1 (/= end)
          _ <- char end
          return r

        bracketed = betweenChars '[' ']'

        commonProgName = do
          spaces
          t <- takeWhile1 notSpace
          spaces
          optional $ (bracketed <|> takeWhile1 notBracketed) >> return ()
          spaces
          optional $ string "..." >> return ()
          spaces
          _ <- bracketed
          return t

        notBracketed = notInClass $ '[':whiteSpaceChars
        progName = do
          spaces
          t <- takeWhile1 notSpace
          spaces
          m <- takeWhile1 notBracketed
          spaces
          _ <- bracketed
          return (t,m)

        maybeText = fmap (\t -> if T.null t then Nothing else Just t)
        flags = many1 flag
          where
            flag = do
              spaces
              ms <- optionMaybe shortFlag
              spaces
              ml <- optionMaybe longFlag
              d <- maybeText (takeTill $ inClass "\r\n")
              case (ms, ml) of
                (Nothing, Nothing) -> fail "expected flags"
                (_, Just (long,typ)) -> return $ ParsedFlag ms (Just long) typ d
                (_, Nothing) ->         return $ ParsedFlag ms Nothing Nothing d
              where
                shortFlag = do
                  _ <- char '-'
                  satisfy notSpace

                longFlag = do
                  _ <- string "--"
                  long <- takeWhile1 (notInClass $ '=':whiteSpaceChars)
                  def <- optionMaybe $ char '=' >> takeWhile1 notSpace
                  return (long, def)

        whiteSpaceChars = " \v\f\t\r\n"
        notSpace = notInClass whiteSpaceChars

        maybeManyTill p stop = do
          r <- manyTill p stop
          return $ if r == [] then Nothing else Just $ pack r

        blankLines = many1 blankLine >> return ()
        blankLine = eol >> eol

        eol' = eol >> return ()
        eol = endOfLine

        optional = option ()

{-
        showRest = do
          rest <- restOfLine
          error $ "rest: " ++ (show rest)
        restOfLine = manyTill anyChar eol
        -}

        optionMaybe p = option Nothing $ (fmap Just) p

type ParsedHelp = (Text, Maybe Text, [ParsedFlag], [(Text, [ParsedFlag])])
prepareParsedHelp :: (Maybe Text, Maybe ParsedCommonMode, [ParsedMode])
                  -> ParsedHelp
prepareParsedHelp (_, Nothing, []) = error "No command flags found"
prepareParsedHelp (description, mCommon, parsedModes) =
  let (name, common) = case mCommon of
                        Nothing                  -> (fst3 $ head parsedModes, defaultCommonFlags)
                        Just (commonName, flags) -> (commonName, flags)
  in let modes = map (checkMode name) parsedModes
     in (name, description, common, modes)
  where
    fst3 (x,_,_) = x
    checkMode :: Text -> (Text, Text, [ParsedFlag]) -> (Text, [ParsedFlag])
    checkMode commonProgName (progName, mode, flags) =
      if progName /= commonProgName then error $ "inconsistent program name: expected "++unpack commonProgName++", got "++unpack progName
        else (mode, flags)

    defaultCommonFlags = [
       ParsedFlag (Just '?') (Just $ pack "help") Nothing (Just $ pack "Display help message")
     , ParsedFlag (Just 'V') (Just $ pack "version") Nothing (Just $ pack "Print version information")
     ]

  -- makeCmdArgs $ prepareParsedHelp parseHelp


-- check indent level?
{-parseIndent :: Parser Int -}
{-parseIndent =-}
    {-sum `fmap` many ((char ' ' >> return 1) <|> (char '\t' >> return 4)) -}