packages feed

cli 0.0.5 → 0.1.0

raw patch · 15 files changed

+1134/−627 lines, 15 filesdep +QuickCheckdep +mtldep +tastydep −containersdep ~basenew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, mtl, tasty, tasty-quickcheck, terminfo, transformers

Dependencies removed: containers

Dependency ranges changed: base

API changes (from Hackage documentation)

- Application.CLI: Help :: Help
- Application.CLI: OptHelp :: [String] -> Maybe String -> String -> OptHelp
- Application.CLI: [optArgument] :: OptHelp -> Maybe String
- Application.CLI: [optDescription] :: OptHelp -> String
- Application.CLI: [optSymbols] :: OptHelp -> [String]
- Application.CLI: action :: CLI cli => cli -> CLIContext -> Options -> IO ()
- Application.CLI: class CLI cli
- Application.CLI: data CLIContext
- Application.CLI: data Help
- Application.CLI: data OptHelp
- Application.CLI: defaultMain :: CLIContext -> IO ()
- Application.CLI: desc :: CLI cli => cli -> String
- Application.CLI: execute :: IO a -> Options -> IO a
- Application.CLI: getHeader :: CLIContext -> String
- Application.CLI: initialize :: String -> CLIContext
- Application.CLI: initializeWithDefault :: CLI cliDefault => cliDefault -> String -> CLIContext
- Application.CLI: instance Application.CLI.Class.CLI Application.CLI.Help
- Application.CLI: instance Application.CLI.Class.CLI Application.CLI.Usage
- Application.CLI: name :: CLI cli => cli -> String
- Application.CLI: options :: CLI cli => cli -> [OptHelp]
- Application.CLI: printHelp :: String -> String -> CLIContext -> String
- Application.CLI: type Options = [String]
- Application.CLI: with :: CLI cli => cli -> CLIContext -> CLIContext
- Application.CLI: withFlag :: [String] -> (Bool -> Options -> IO a) -> Options -> IO a
- Application.CLI: withOptionalParameterStr :: [String] -> (Maybe String -> Options -> IO a) -> Options -> IO a
- Application.CLI: withOptionalStr :: (Maybe String -> Options -> IO a) -> Options -> IO a
- Application.CLI: withParameterStr :: [String] -> (String -> Options -> IO a) -> Options -> IO a
- Application.CLI: withStr :: String -> (String -> Options -> IO a) -> Options -> IO a
+ Console.Display: Bg :: Color -> OutputElem
+ Console.Display: Black :: Color
+ Console.Display: Blue :: Color
+ Console.Display: ColorNumber :: Int -> Color
+ Console.Display: Cyan :: Color
+ Console.Display: Fg :: Color -> OutputElem
+ Console.Display: Green :: Color
+ Console.Display: JustifyLeft :: Justify
+ Console.Display: JustifyRight :: Justify
+ Console.Display: LeftT :: Int -> String -> OutputElem
+ Console.Display: Magenta :: Color
+ Console.Display: NA :: OutputElem
+ Console.Display: Red :: Color
+ Console.Display: RightT :: Int -> String -> OutputElem
+ Console.Display: T :: String -> OutputElem
+ Console.Display: White :: Color
+ Console.Display: Yellow :: Color
+ Console.Display: columnNew :: Int -> String -> Column
+ Console.Display: data Color :: *
+ Console.Display: data Column
+ Console.Display: data Justify
+ Console.Display: data OutputElem
+ Console.Display: data ProgressBar
+ Console.Display: data Summary
+ Console.Display: data Table
+ Console.Display: data TerminalDisplay
+ Console.Display: display :: TerminalDisplay -> [OutputElem] -> IO ()
+ Console.Display: displayInit :: IO TerminalDisplay
+ Console.Display: displayLn :: TerminalDisplay -> Color -> String -> IO ()
+ Console.Display: displayTextColor :: TerminalDisplay -> Color -> String -> IO ()
+ Console.Display: instance GHC.Classes.Eq Console.Display.OutputElem
+ Console.Display: instance GHC.Show.Show Console.Display.OutputElem
+ Console.Display: justify :: Justify -> Int -> String -> String
+ Console.Display: progress :: TerminalDisplay -> Int -> (ProgressBar -> IO a) -> IO a
+ Console.Display: progressTick :: ProgressBar -> IO ()
+ Console.Display: summary :: TerminalDisplay -> IO Summary
+ Console.Display: summarySet :: Summary -> [OutputElem] -> IO ()
+ Console.Display: tableAppend :: TerminalDisplay -> Table -> [String] -> IO ()
+ Console.Display: tableCreate :: [Column] -> Table
+ Console.Display: tableHeaders :: TerminalDisplay -> Table -> IO ()
+ Console.Display: termText :: String -> TermOutput
+ Console.Options: FlagDescription :: String -> FlagFrag
+ Console.Options: FlagLong :: String -> FlagFrag
+ Console.Options: FlagOptional :: a -> (ValueParser a) -> FlagParser a
+ Console.Options: FlagRequired :: (ValueParser a) -> FlagParser a
+ Console.Options: FlagShort :: Char -> FlagFrag
+ Console.Options: OptionError :: String -> OptionRes r
+ Console.Options: OptionHelp :: OptionRes r
+ Console.Options: OptionInvalid :: String -> OptionRes r
+ Console.Options: OptionSuccess :: Params -> (Action r) -> OptionRes r
+ Console.Options: Params :: [(Nid, Maybe String)] -> [String] -> [String] -> Params
+ Console.Options: [paramsFlags] :: Params -> [(Nid, Maybe String)]
+ Console.Options: [paramsPinnedArgs] :: Params -> [String]
+ Console.Options: [paramsRemainingArgs] :: Params -> [String]
+ Console.Options: action :: Action r -> OptionDesc r ()
+ Console.Options: argument :: String -> ValueParser a -> OptionDesc r (Arg a)
+ Console.Options: command :: String -> OptionDesc r () -> OptionDesc r ()
+ Console.Options: conflict :: Flag a -> Flag b -> OptionDesc r ()
+ Console.Options: data Arg a
+ Console.Options: data ArgRemaining a
+ Console.Options: data Flag a
+ Console.Options: data FlagFrag
+ Console.Options: data FlagLevel a
+ Console.Options: data FlagMany a
+ Console.Options: data FlagParam a
+ Console.Options: data FlagParser a
+ Console.Options: data OptionDesc r a
+ Console.Options: data OptionRes r
+ Console.Options: data Params
+ Console.Options: defaultMain :: OptionDesc (IO ()) () -> IO ()
+ Console.Options: defaultMainWith :: OptionDesc (IO ()) () -> [String] -> IO ()
+ Console.Options: description :: String -> OptionDesc r ()
+ Console.Options: flag :: FlagFrag -> OptionDesc r (Flag Bool)
+ Console.Options: flagMany :: OptionDesc r (FlagParam a) -> OptionDesc r (FlagMany a)
+ Console.Options: flagParam :: FlagFrag -> FlagParser a -> OptionDesc r (FlagParam a)
+ Console.Options: getParams :: Param p => Params -> (forall a. p a -> Ret p a)
+ Console.Options: parseOptions :: OptionDesc r () -> [String] -> (ProgramDesc r, OptionRes r)
+ Console.Options: programDescription :: String -> OptionDesc r ()
+ Console.Options: programName :: String -> OptionDesc r ()
+ Console.Options: programVersion :: Version -> OptionDesc r ()
+ Console.Options: remainingArguments :: String -> OptionDesc r (ArgRemaining [String])
+ Console.Options: type Action r = (forall a p. Param p => p a -> Ret p a) -> r

Files

− Application/CLI.hs
@@ -1,252 +0,0 @@--- |--- Module      : Application.CLI--- License     : BSD-Style--- Copyright   : Copyright © 2014 Nicolas DI PRIMA------ Maintainer  : Nicolas DI PRIMA <nicolas@di-prima.fr>--- Stability   : experimental--- Portability : unknown------ This module aimes to provide an easy and low-dependencies Command Line--- configuration options.------ You can create a new command for your program by creating an object--- And creating an instance of CLI of this object------ @---   data MyEchoCommand = MyEchoCommand---   instance CLI MyCommand where---      name _ = "cmd"---      desc _ = "just echo the given arguments"---      options _ = [ OptHelp [] (Just "string") "the message to print" ]---      action _ _ =---          withStr "string" $ \str -> exectute $ putStrLn str--- @------ And to create you application, you need 2 functions (initialize, with and defaultMain).--- * @initialize@: only create the default CLIContext with a default Help command---   and a default Usage function--- * @with@: insert the Command into the CLIContext--- * @defaultMain@: will trigger the right command------ @---   main :: IO ()---   main = defaultMain $ with MyEchoCommand $ initialize "a message header in case the help command is triggered"--- @-----module Application.CLI-    ( -- * Default Main-      defaultMain--      -- * CLI-    , CLI(..)-    , OptHelp(..)-      -- ** Default CLIs-    , Help(..)-    , printHelp--      -- * Commands-    , CLIContext-    , getHeader-    , initialize-    , initializeWithDefault-    , with--      -- * Options-    , Options-    , withStr-    , withOptionalStr-    , withParameterStr-    , withOptionalParameterStr-    , withFlag-    , execute-    ) where--import Application.CLI.Class-import Application.CLI.Types-import Data.List-import System.Environment-import System.Exit---- | Default usage Command Interface-data Usage = Usage-instance CLI Usage where-    name    _ = ""-    desc    _ = ""-    options _ = []-    action  _ ctx opts = do-        progName <- getProgName-        usage (printUsage "    " progName ctx) opts--usage :: String-      -> [String]-      -> IO ()-usage list msgs = do-    mapM_ (\str -> putStrLn str) msgs-    putStrLn list-    exitFailure---- | This is the default help command------ It is triggered by the command "help" and also provides--- an option to print a specific command options-data Help = Help-instance CLI Help where-    name    _  = "help"-    desc    _  = "show help message"-    options _  = [ OptHelp [] (Just "command") "print the help message for a specific command"-                 ]-    action  _ ctx =-        withOptionalStr $ \mcommand ->-        execute $ do-            case mcommand of-                Nothing -> printStdHelp-                Just c  -> printCmdHelp c-      where-        printCmdHelp :: String -> IO ()-        printCmdHelp cmd = do-            case lookupCommand cmd ctx of-                Nothing -> error $ "command '" ++ cmd ++ "' does not exist"-                Just c  -> putStrLn $ printCommandHelp "   " c--        printStdHelp :: IO ()-        printStdHelp = do-            progName <- getProgName-            putStrLn $ progName ++ ": " ++ getHeader ctx-            putStrLn $ printHelp "    " progName ctx------------------------------------------------------------------------------------                               Commands                                    -------------------------------------------------------------------------------------- | Add a new Command into a collection of commands-with :: CLI cli-     => cli      -- ^ A new Command Interface-     -> CLIContext -- ^ The original Collection of commands-     -> CLIContext-with c l = insertCommand (cliToCommand c) l---- | Initialize a collection of commands-initializeWithDefault :: CLI cliDefault-                      => cliDefault -- ^ The command to execute if no option is given-                      -> String     -- ^ CLI Description-                      -> CLIContext-initializeWithDefault defaultCli description =-    insertDefault (cliToCommand defaultCli) cmdMap-  where-    cmdMap = initialize description---- | Initialize a collection of command-initialize :: String     -- ^ CLI Description-           -> CLIContext-initialize description =-    createContext (cliToCommand Usage) description------------------------------------------------------------------------------------                             Options                                       -------------------------------------------------------------------------------------- | This function is to expect an argument at the Head of the @Options@--- if the command is not found at the head, the program fails and the description--- message is printed-withStr :: String  -- ^ a description of what is expected-        -> (String -> Options -> IO a)-        -> Options-        -> IO a-withStr what _ []     = error $ "expecting <" ++ what ++ ">"-withStr _    f (x:xs) = f x xs---- | This function is to expect an optional argument--- of the command is not found at the Head of the @Options@--- the function will be executed with Nothing, otherwise it will be executed--- with (Just @String@)-withOptionalStr :: (Maybe String -> Options -> IO a)-                -> Options-                -> IO a-withOptionalStr f []     = f Nothing  []-withOptionalStr f (x:xs) = f (Just x) xs---- | Look for a parameterized option--- The given list of flags are the potential list of flags that can be use to--- find the position of the options------ This parameter can be found anywhere in the Options list and the argument--- of the function will be the string following one of this Flags--- The parameter and its value are removed from the options before being use--- into the function.-withParameterStr :: [String] -- ^ the reference flag-                 -> (String -> Options -> IO a)-                 -> Options-                 -> IO a-withParameterStr flags f l =-    case break (flip elem flags) l of-        (_  , [])      -> error $ "expecting parameter " ++ intercalate ", " flags ++ " <value>"-        (_  , _:[])    -> error $ "parameter " ++ intercalate ", " flags ++ " is expection an argument"-        (xs1, _:p:xs2) -> f p (xs1 ++ xs2)---- | idem as @withParameterStr@ but it is an optional parameter-withOptionalParameterStr :: [String] -- ^ the reference flag-                         -> (Maybe String -> Options -> IO a)-                         -> Options-                         -> IO a-withOptionalParameterStr flags f l =-    case break (flip elem flags) l of-        (xs1, [])      -> f Nothing xs1-        (_  , _:[])    -> error $ "parameter " ++ intercalate ", " flags ++ " is expecting an argument"-        (xs1, _:p:xs2) -> f (Just p) (xs1 ++ xs2)---- | This function is to expect Flag in the Options--- it will look into all the options and will remove the string--- from the Options-withFlag :: [String]                   -- ^ the flag to lookup-         -> (Bool -> Options -> IO a)  -- ^ the function to execute-         -> Options                    -- ^ the options-         -> IO a-withFlag flags f l =-    case break (flip elem flags) l of-        (xs1, [])    -> f False xs1-        (xs1, _:xs2) -> f True  (xs1 ++ xs2)---- | This function makes the use of the other function easier--- and also provide a verification that there is no "unused function".--- If there is unexpected options remaining, the program will stop and an--- error message will be printed-execute :: IO a-        -> Options-        -> IO a-execute f [] = f-execute _ l  = error $ "this options are not expected: " ++ intercalate " " l------------------------------------------------------------------------------------                             Default Main                                  -------------------------------------------------------------------------------------- | The default Main--- It will analyze the program's parameters and will trigger the--- right command.-defaultMain :: CLIContext -- ^ A collection of commands-            -> IO ()-defaultMain cmdMap = do-    args <- getArgs-    case args of-        []     -> tryDefault-        (x:xs) -> tryCommand x xs-  where-    raiseHelper :: String-                -> IO ()-    raiseHelper what =-        cmdAction (getHelper cmdMap) cmdMap [what]-    tryDefault :: IO ()-    tryDefault = do-        progName <- getProgName-        case getDefault cmdMap of-            Nothing  -> raiseHelper $ progName ++ " does not provide default command"-            Just cmd -> cmdAction cmd cmdMap []-    tryCommand :: String-               -> [String]-               -> IO ()-    tryCommand cmdstr opts =-        case lookupCommand cmdstr cmdMap of-            Nothing  -> raiseHelper $ cmdstr ++ ": command does not exist"-            Just cmd -> cmdAction cmd cmdMap opts
− Application/CLI/Class.hs
@@ -1,53 +0,0 @@--- |--- Module      : Application.CLI.Class--- License     : BSD-Style--- Copyright   : Copyright © 2014 Nicolas DI PRIMA------ Maintainer  : Nicolas DI PRIMA <nicolas@di-prima.fr>--- Stability   : experimental--- Portability : unknown----module Application.CLI.Class-    ( CLI (..)-    , cliToCommand-    ) where--import Application.CLI.Types---- | This is the CLI Class--- All command you want to add into the CLIContext must be an instance of this--- class.-class CLI cli where-    -- | This is the name of the command. It is also the command line option-    -- the action will be triggered every time this string is found to be the-    -- first argument of the program-    name    :: cli-            -> String-    -- | A description of this command-    -- Will be use in the default Usage and the Help function-    desc    :: cli-            -> String-    -- | The list of Options-    -- It is only for printing, the parser won't use this to parse the command-    -- parameters.-    options :: cli-            -> [OptHelp]-    -- | The action to perfom every time the @name@ is found to be the first-    -- string of the program parameters-    action  :: cli-            -> CLIContext -- ^ This could be use in order to know all the different available commands etc...-            -> Options    -- ^ The Command Parameters-            -> IO ()---- | Make the transformation of a CLI instance to a Command--- object-cliToCommand :: CLI cli-             => cli-             -> Command-cliToCommand cli =-    Command-        { cmdName    = name cli-        , cmdDesc    = desc cli-        , cmdOptions = options cli-        , cmdAction  = action cli-        }
− Application/CLI/Types.hs
@@ -1,226 +0,0 @@--- |--- Module      : Application.CLI.Types--- License     : BSD-Style--- Copyright   : Copyright © 2014 Nicolas DI PRIMA------ Maintainer  : Nicolas DI PRIMA <nicolas@di-prima.fr>--- Stability   : experimental--- Portability : unknown----module Application.CLI.Types-    ( -- * Types-      CLIContext-    , Command(..)-    , OptHelp(..)-    , Options-    -      -- * Commands-      -- ** Getter/Setter-    , createContext-    , getHeader-    , getHelper-    , insertDefault-    , getDefault-    , insertCommand-      -- ** Lookup-    , lookupCommand--      -- * pretty printer-    , printUsage-    , printHelp-    , printOptHelp-    , printCommandHelp-    ) where--import           Data.Map.Strict (Map)-import qualified Data.Map.Strict as M------------------------------------------------------------------------------------                               Commands                                    -------------------------------------------------------------------------------------- | This is the Command Line Context--- This object embeds:--- * the commands to trigger--- * a default command in case no command is given--- * a program description--- * a Helper Command: the command to trigger in case of a wrong command (example: Usage)-data CLIContext = CLIContext-    { getCommands :: Map String Command-    , getDefault  :: Maybe Command -- ^ get the default command (if any)-    , getHeader   :: String  -- ^ Program's description-    , getHelper   :: Command -- ^ get the program's help function (usage)-    } deriving (Show, Eq)---- | Create a CLIContext-createContext :: Command -- ^ the command helper (example: the Usage)-              -> String  -- ^ a description about the program-              -> CLIContext-createContext helper header =-    CLIContext-        { getCommands = M.empty-        , getDefault  = Nothing-        , getHelper   = helper-        , getHeader   = header-        }---- | Insert a command into the CLIContext------ If the command with the same name is already present, the old one will be--- removed and the new one will take place-insertCommand :: Command    -- ^ the command to insert-              -> CLIContext -- ^ the CLIContext to update-              -> CLIContext-insertCommand cmd cmdMap = cmdMap { getCommands = M.insert (cmdName cmd) cmd (getCommands cmdMap) }---- | Insert a default Command into the CLI------ This command won't be added into the command to trigger--- If there was already a Default Command this command will replace the old one-insertDefault :: Command    -- ^ The Default Command-              -> CLIContext-              -> CLIContext-insertDefault cmd cmdMap =-    insertCommand cmd $cmdMap { getDefault  = Just cmd }---- | Lookup a command by its command's name-lookupCommand :: String     -- ^ the command's name-              -> CLIContext -- ^ the commands to look up into-              -> Maybe (Command)-lookupCommand k cmdMap = M.lookup k (getCommands cmdMap)------------------------------------------------------------------------------------                                  Options                                  -------------------------------------------------------------------------------------- | The Command Options parameter-type Options = [String]------------------------------------------------------------------------------------                                  Command                                  -------------------------------------------------------------------------------------- | A command-data Command =-      Command-        { cmdName :: String-        , cmdDesc :: String-        , cmdOptions :: [OptHelp]-        , cmdAction  :: CLIContext -> Options -> IO ()-        }--instance Eq Command where-    (==) a b = eqname && eqdesc && eqopts-      where-        eqname = cmdName a == cmdName b-        eqdesc = cmdDesc a == cmdDesc b-        eqopts = cmdOptions a == cmdOptions b--instance Show Command where-    show c =-        "Command { " ++ "name: " ++ (cmdName c) ++ ", "-                     ++ "desc: " ++ (cmdDesc c) ++ ", "-                     ++ "opts: " ++ (show $ cmdOptions c) ++ " }"---- | Pretty print a command-printCommandHelp :: String  -- ^ indentation-                 -> Command -- ^ the command to pretty print-                 -> String-printCommandHelp prefix cmd =-    prefix ++ cmdName cmd ++ ": " ++ cmdDesc cmd ++ "\n"-    ++ (foldr (\c acc -> prefix ++ prefix ++ (printOptHelp c) ++ "\n" ++ acc) "" (cmdOptions cmd))------------------------------------------------------------------------------------                         Option helps                                      -------------------------------------------------------------------------------------- | Option help------ It describes the Command parameters-data OptHelp = OptHelp-    { -- | an option can have multiple symbols, mainly a short option and a long option-      -- for example [ "-h", "--help" ]-      optSymbols     :: [String]-    , -- | an option can have an argument, or not-      optArgument    :: Maybe String-    , optDescription :: String -- ^ option description-    } deriving (Eq, Show)---- | This function is a helper to print an OptHelp-printOptHelp :: OptHelp-             -> String-printOptHelp opthelp =-    (printSymbols $ optSymbols opthelp)-    ++ (printArgument $ optArgument opthelp)-    ++ (optDescription opthelp)-  where-    printSymbols :: [String] -> String-    printSymbols []     = ""-    printSymbols [x]    = x ++ " "-    printSymbols (x:xs) = x ++ ", " ++ printSymbols xs--    printArgument :: Maybe String -> String-    printArgument Nothing  = ""-    printArgument (Just x) = "<" ++ x ++ "> "------------------------------------------------------------------------------------                          Pretty printers                                  -------------------------------------------------------------------------------------- | Print all the Command's Option help-printHelp :: String   -- ^ indentation-          -> String   -- ^ program name-          -> CLIContext -- ^ command list-          -> String-printHelp prefix name cmdMap =-    (defaultUsage prefix name $ getDefault cmdMap)-    ++ "\n"-    ++ (listCommands prefix cmdMap)---- | Print the all the commands helps-listCommands :: String-             -> CLIContext-             -> String-listCommands prefix cmdMap =-    "Available commands:\n"-    ++ (foldr (\c acc -> "\n" ++ (printCommandHelp listPrefix c) ++ acc) "" (M.elems $ getCommands cmdMap))-  where-    listPrefix :: String-    listPrefix = prefix---- | Pretty printer: list the available commands-printUsage :: String   -- ^ indentation-           -> String   -- ^ program name-           -> CLIContext -- ^ the command to print-           -> String-printUsage prefix name cmdMap =-    (defaultUsage prefix name $ getDefault cmdMap)-    ++ "\n"-    ++ (listDescriptions prefix cmdMap)---- | Print the defaul usage (if any)-defaultUsage :: String -- ^ prefix-             -> String -- ^ program name-             -> Maybe (Command)-             -> String-defaultUsage _      _    Nothing    = ""-defaultUsage prefix name (Just cmd) =-    prefix ++ "Usage: " ++ name ++ " [" ++ (cmdName cmd) ++ "]\n"---- | will list all the description's commands present in the CLI Context-listDescriptions :: String     -- ^ prefix-                 -> CLIContext -- ^ the context-                 -> String-listDescriptions prefix cmdMap =-    "Available commands:\n"-    ++ (foldr (\c acc -> "\n" ++ (commandDescription listPrefix c) ++ acc) "" (M.elems $ getCommands cmdMap))-  where-    listPrefix :: String-    listPrefix = prefix---- | print a command's description-commandDescription :: String-                   -> Command-                   -> String-commandDescription prefix cmd =-    prefix ++ cmdName cmd ++ ": " ++ cmdDesc cmd ++ "\n"
+ Console/Display.hs view
@@ -0,0 +1,301 @@+module Console.Display+    ( TerminalDisplay+    -- * Basic+    , displayInit+    , display+    , displayTextColor+    , displayLn+    -- * Progress Bar+    , ProgressBar+    , progress+    , progressTick+    -- * Summary line+    , Summary+    , summary+    , summarySet+    -- * Attributes+    , Color(..)+    , OutputElem(..)+    , termText+    , justify+    -- * Table+    , Justify(..)+    , Table+    , Column+    , columnNew+    , tableCreate+    , tableHeaders+    , tableAppend+    ) where++import           Control.Applicative+import           Control.Monad+import           Control.Concurrent.MVar+import           System.Console.Terminfo+import           System.IO+import           Data.List++{-+data LineWidget =+      Text+    | Progress+    | Done+-}++data OutputElem =+      Bg Color+    | Fg Color+    | T  String+    | LeftT Int String+    | RightT Int String+    | NA+    deriving (Show,Eq)++data TerminalDisplay = TerminalDisplay (MVar Bool) Terminal++displayInit :: IO TerminalDisplay+displayInit = do+    hSetBuffering stdout NoBuffering+    cf <- newMVar False+    TerminalDisplay cf <$> setupTermFromEnv++display :: TerminalDisplay -> [OutputElem] -> IO ()+display tdisp@(TerminalDisplay clearFirst term) oelems = do+    cf <- modifyMVar clearFirst $ \cf -> return (False, cf)+    when cf $ runTermOutput term (maybe mempty id clearLineFirst)+    runTermOutput term $ renderOutput tdisp oelems+  where+        clearLineFirst = getCapability term clearEOL++renderOutput :: TerminalDisplay -> [OutputElem] -> TermOutput+renderOutput (TerminalDisplay _ term) to = mconcat $ map toTermOutput to+  where+        wF = maybe (const mempty) id $ getCapability term setForegroundColor+        wB = maybe (const mempty) id $ getCapability term setBackgroundColor+        rD = maybe mempty         id $ getCapability term restoreDefaultColors++        toTermOutput (Fg c) = wF c+        toTermOutput (Bg c) = wB c+        toTermOutput (T t)  = termText t+        toTermOutput (LeftT sz t)  = termText (t ++ replicate (sz - length t) ' ')+        toTermOutput (RightT sz t)  = termText (replicate (sz - length t) ' ' ++ t)+        toTermOutput NA     = rD++displayTextColor :: TerminalDisplay -> Color -> String -> IO ()+displayTextColor term color msg = do+    display term [Fg color, T msg]++displayLn :: TerminalDisplay -> Color -> String -> IO ()+displayLn disp color msg = displayTextColor disp color (msg ++ "\n")++data ProgressBar = ProgressBar TerminalDisplay ProgressBackend (MVar ProgressState)++type ProgressBackend = String -> IO ()++data Summary = Summary SummaryBackend+type SummaryBackend = [OutputElem] -> IO ()++data ProgressState = ProgressState+    { pgLhs     :: String+    , pgRhs     :: String+    , pgMax     :: Int+    , pgCurrent :: Int+    }++initProgressState :: Int -> ProgressState+initProgressState maxItems = ProgressState+    { pgLhs     = ""+    , pgRhs     = ""+    , pgMax     = maxItems+    , pgCurrent = 0+    }++progress :: TerminalDisplay+         -> Int+         -> (ProgressBar -> IO a)+         -> IO a+progress tdisp@(TerminalDisplay cf term) numberItems f = do+    let b = backend (getCapability term cursorDown)+                    (getCapability term carriageReturn)+                    (getCapability term clearEOL)+    pbar <- ProgressBar tdisp b <$> newMVar (initProgressState numberItems)++    progressStart pbar+    a <- f pbar+    displayLn tdisp White ""+    return a+  where+    backend :: Maybe (Int -> TermOutput)+            -> Maybe TermOutput+            -> Maybe TermOutput+            -> ProgressBackend+    backend _ (Just goHome) (Just clearEol) = \msg -> do+        runTermOutput term $ mconcat [clearEol, termText msg, goHome]+        modifyMVar_ cf $ return . const True+    backend _ _ _ = \msg ->+        displayLn tdisp White msg++showBar :: ProgressBar -> IO ()+showBar (ProgressBar _ backend pgsVar) = do+    pgs <- readMVar pgsVar+    let bar = getBar pgs+    backend bar+  where+    getBar (ProgressState lhs rhs maxItems current) =+            lhs `sep` bar `sep`+            (show current ++ "/" ++ show maxItems) `sep`+            rhs+      where+        sep s1 s2+            | null s1   = s2+            | null s2   = s1+            | otherwise = s1 ++ " " ++ s2++        bar+            | maxItems == current = "[" ++ replicate szMax fillingChar ++ "]"+            | otherwise           = "[" ++ replicate filled fillingChar ++ ">" ++ replicate (unfilled-1) ' ' ++ "]"++        fillingChar = '='++        unfilled, filled :: Int+        unfilled   = szMax - filled+        filled     = floor numberChar++        numberChar = fromIntegral szMax / currentProgress+        szMax      = 40++        currentProgress :: Double+        currentProgress = fromIntegral maxItems / fromIntegral current++progressStart :: ProgressBar -> IO ()+progressStart pbar = do+    showBar pbar+    return ()++progressTick :: ProgressBar -> IO ()+progressTick pbar@(ProgressBar _ _ st) = do+    modifyMVar_ st $ \pgs -> return $ pgs { pgCurrent = min (pgMax pgs) (pgCurrent pgs + 1) }+    showBar pbar+    return ()++summary :: TerminalDisplay -> IO Summary+summary tdisp@(TerminalDisplay cf term) = do+    let b = backend (getCapability term cursorDown)+                    (getCapability term carriageReturn)+                    (getCapability term clearEOL)+    return $ Summary b+  where+    backend :: Maybe (Int -> TermOutput)+            -> Maybe TermOutput+            -> Maybe TermOutput+            -> SummaryBackend+    backend _ (Just goHome) (Just clearEol) = \msg -> do+        runTermOutput term $ mconcat [clearEol, renderOutput tdisp msg, goHome]+        modifyMVar_ cf $ return . const True+    backend _ _ _ = \msg ->+        runTermOutput term $ mconcat [renderOutput tdisp msg]++summarySet :: Summary -> [OutputElem] -> IO ()+summarySet (Summary backend) output = do+    backend output++data Justify = JustifyLeft | JustifyRight++justify :: Justify -> Int -> String -> String+justify dir sz s+    | sz <= szS = s+    | otherwise =+        let pad = replicate (sz - szS) ' '+         in case dir of+                JustifyLeft  -> pad ++ s+                JustifyRight -> s ++ pad+  where+    szS = length s+{-+data Attr = Attr+    { attrHasSize    :: Maybe Int+    , attrHasJustify :: Maybe Justify+    , attrHasColor   :: Maybe Color+    , attrHasBgColor :: Maybe Color+    , attrText       :: String+    }++instance Monoid Attr where+    mempty = Attr Nothing Nothing Nothing Nothing ""+    mappend a1 a2 =+        Attr { attrHasSize    = attrHasSize a2+             , attrHasJustify = attrHasJustify a2+             , attrHasColor   = attrHasColor a2+             , attrHasBgColor = attrHasBgColor a2+             , attrText       = attrText a1 ++ attrText a2+             }++attrColor :: Color -> Attr -> Attr+attrColor c a = a { attrHasColor = Just c }++attrSize :: Int -> Attr -> Attr+attrSize 0 a = a { attrHasSize = Nothing }+attrSize n a = a { attrHasSize = Just n }++attrJustify :: Justify -> Attr -> Attr+attrJustify j a = a { attrHasJustify = Just j }++text :: String -> Attr+text s = mempty { attrText = s }++toElem :: Attr -> [OutputElem]+toElem attr =+    let j = case attrHasSize attr of+            Just n ->+                case attrHasJustify attr of+                    Nothing           -> [LeftT n " "]+                    Just JustifyLeft  -> [LeftT n " "]+                    Just JustifyRight -> [RightT n " "]+            Nothing ->+                []+        fwc = case attrHasColor attr of+                    Nothing -> []+                    Just c  -> [Fg c]+        bgc = case attrHasBgColor attr of+                    Nothing -> []+                    Just c  -> [Bg c]+     in mconcat [j,fwc,bgc, [T $ attrText attr,NA]]+-}++-- column+data Column = Column+    { columnSize    :: Int+    , columnName    :: String+    , columnJustify :: Justify+    , columnWrap    :: Bool+    }++columnNew :: Int -> String -> Column+columnNew n name = Column n name JustifyLeft False++data Table = Table+    { tColumns     :: [Column]+    , rowSeparator :: String+    }++tableCreate :: [Column] -> Table+tableCreate cols = Table { tColumns = cols, rowSeparator = "" }++tableHeaders :: TerminalDisplay -> Table -> IO ()+tableHeaders td t =+    tableAppend td t $ map columnName $ tColumns t++tableAppend :: TerminalDisplay -> Table -> [String] -> IO ()+tableAppend td (Table cols rowSep) l = do+    let disp = case compare (length l) (length cols) of+                        LT -> zip cols (l ++ replicate (length cols - length l) "")+                        _  -> zip cols l+    mapM_ printColRow $ intersperse Nothing $ map Just disp+  where+    printColRow Nothing =+        display td [T $ rowSep]+    printColRow (Just (c, fieldElement)) = do+        let oe = case columnJustify c of+                   JustifyLeft  -> RightT (columnSize c) fieldElement+                   JustifyRight -> LeftT (columnSize c) fieldElement+        display td [oe,T "\n"]
+ Console/Options.hs view
@@ -0,0 +1,357 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE TypeFamilies #-}+module Console.Options+    (+    -- * Running+      defaultMain+    , defaultMainWith+    , parseOptions+    , OptionRes(..)+    , OptionDesc+    -- * Description+    , programName+    , programVersion+    , programDescription+    , command+    , FlagFrag(..)+    , flag+    , flagParam+    , flagMany+    , conflict+    , argument+    , remainingArguments+    , action+    , description+    , Action+    -- * Arguments+    , FlagParser(..)+    , Flag+    , FlagLevel+    , FlagParam+    , FlagMany+    , Arg+    , ArgRemaining+    , Params(..)+    , getParams+    ) where++import           Console.Options.Flags hiding (Flag, flagArg)+import qualified Console.Options.Flags as F+import           Console.Options.Nid+import           Console.Options.Utils+import           Console.Options.Monad+import           Console.Options.Types+import           Console.Display (justify, Justify(..))++import           Control.Applicative+import           Control.Monad+import           Control.Monad.IO.Class+import           Control.Monad.State+import           Control.Monad.Writer++import           Data.List+import           Data.Version+import           Data.Functor.Identity++import           System.Environment (getArgs, getProgName)+import           System.Exit++----------------------------------------------------------------------+setDescription :: String -> Command r -> Command r+setDescription desc  (Command hier _ opts act)    = Command hier desc opts act++setAction :: Action r -> Command r -> Command r+setAction act (Command hier desc opts _)   = Command hier desc opts (Just act)++addOption :: FlagDesc -> Command r -> Command r+addOption opt   (Command hier desc opts act) = Command hier desc (opt : opts) act++tweakOption :: Nid -> (FlagDesc -> FlagDesc) -> Command r -> Command r+tweakOption nid mapFlagDesc (Command hier desc opts act) =+    Command hier desc (modifyNid opts) act+  where+    modifyNid []           = []+    modifyNid (f:fs)+        | flagNid f == nid = mapFlagDesc f : fs+        | otherwise        = f : modifyNid fs++addArg :: Argument -> Command r -> Command r+addArg arg = modifyHier $ \hier ->+    case hier of+        CommandLeaf l  -> CommandLeaf (arg:l)+        CommandTree {} -> hier -- ignore argument in a hierarchy.+----------------------------------------------------------------------++data FlagParser a =+      FlagRequired (ValueParser a)+    | FlagOptional a (ValueParser a)++type ValueParser a = String -> Either String a++data OptionRes r =+      OptionSuccess Params (Action r)+    | OptionHelp+    | OptionError String -- user cmdline error in the arguments+    | OptionInvalid String -- API has been misused++defaultMain :: OptionDesc (IO ()) () -> IO ()+defaultMain dsl = getArgs >>= defaultMainWith dsl++defaultMainWith :: OptionDesc (IO ()) () -> [String] -> IO ()+defaultMainWith dsl args = do+    let (programDesc, res) = parseOptions dsl args+     in case res of+        OptionError s          -> putStrLn s >> exitFailure+        OptionHelp             -> help (stMeta programDesc) (stCT programDesc) >> exitSuccess+        OptionSuccess params r -> r (getParams params)+        OptionInvalid s        -> putStrLn s >> exitFailure++parseOptions :: OptionDesc r () -> [String] -> (ProgramDesc r, OptionRes r)+parseOptions dsl args =+    let descState = gatherDesc dsl+     in (descState, runOptions (stMeta descState) (stCT descState) args)++--helpSubcommand :: [String] -> IO ()++help :: ProgramMeta -> Command (IO ()) -> IO ()+help pmeta (Command hier _ commandOpts _) = mapM_ putStrLn . lines $ snd $ runWriter $ do+    tell (maybe "<program>" id (programMetaName pmeta) ++ " version " ++ maybe "<undefined>" id (programMetaVersion pmeta) ++ "\n")+    tell "\n"+    maybe (return ()) (\d -> tell d >> tell "\n\n") (programMetaDescription pmeta)+    tell "Options:\n"+    tell "\n"+    mapM_ (tell . printOpt 0) commandOpts+    case hier of+        CommandTree subs -> do+            tell "\n"+            tell "Commands:\n"+            let cmdLength = maximum (map (length . fst) subs) + 2+            mapM_ (\(n, c) -> tell $ indent 2 (justify JustifyRight cmdLength n ++ getCommandDescription c ++ "\n")) subs+            tell "\n"+            mapM_ (printSub 0) subs+        CommandLeaf _    ->+            return ()+  where+    printSub iLevel (name, cmdOpt) = do+        tell ("\n" ++ name ++ " options:\n\n")+        mapM_ (tell . printOpt iLevel) (getCommandOptions cmdOpt)+        case getCommandHier cmdOpt of+            CommandTree _ -> do+                return ()+            CommandLeaf _ -> do+                return ()+        --tell . indent 2 ""++    printOpt iLevel fd =+        let optShort = maybe (replicate 2 ' ') (\c -> "-" ++ [c]) $ flagShort ff+            optLong  = maybe (replicate 8 ' ') (\s -> "--" ++ s) $ flagLong ff+            optDesc  = maybe "" ("  " ++) $ flagDescription ff+         in indent (iLevel + 2) $ intercalate " " [optShort, optLong, optDesc] ++ "\n"+      where+        ff = flagFragments fd++runOptions :: ProgramMeta+           -> Command r -- commands+           -> [String] -- arguments+           -> OptionRes r+runOptions pmeta ct allArgs+    | "--help" `elem` allArgs = OptionHelp+    | "-h" `elem` allArgs     = OptionHelp+    | otherwise               = go [] ct allArgs+  where+        -- parse recursively using a Command structure+        go :: [[F.Flag]] -> Command r -> [String] -> OptionRes r+        go parsedOpts (Command hier _ commandOpts act) unparsedArgs =+            case parseFlags commandOpts unparsedArgs of+                (opts, unparsed, [])  -> do+                    case hier of+                        -- if we have sub commands, then we pass the unparsed options+                        -- to their parsers+                        CommandTree subs -> do+                            case unparsed of+                                []     -> errorExpectingMode subs+                                (x:xs) -> case lookup x subs of+                                                Nothing      -> errorInvalidMode x subs+                                                Just subTree -> go (opts:parsedOpts) subTree xs+                        -- no more subcommand (or none to start with)+                        CommandLeaf unnamedArgs ->+                            case validateUnnamedArgs (reverse unnamedArgs) unparsed of+                                Left err   -> errorUnnamedArgument err+                                Right (pinnedArgs, remainingArgs) -> do+                                    let flags = concat (opts:parsedOpts)+                                    case act of+                                        Nothing -> OptionInvalid "no action defined"+                                        Just a  ->+                                            let params = Params flags+                                                                pinnedArgs+                                                                remainingArgs+                                             in OptionSuccess params a+                (_, _, ers) -> do+                    OptionError $ mconcat $ map showOptionError ers++        validateUnnamedArgs :: [Argument] -> [String] -> Either String ([String], [String])+        validateUnnamedArgs argOpts l =+            v [] argOpts >>= \(opts, _hasCatchall) -> do+                let unnamedRequired = length opts+                if length l < unnamedRequired+                    then Left "missing arguments"+                    else Right $ splitAt unnamedRequired l+          where+            v :: [Argument] -> [Argument] -> Either String ([Argument], Bool)+            v acc []                    = Right (reverse acc, False)+            v acc (a@(Argument {}):as)  = v (a:acc) as+            v acc ((ArgumentCatchAll {}):[]) = Right (reverse acc, True)+            v _   ((ArgumentCatchAll {}):_ ) = Left "arguments expected after remainingArguments"++        showOptionError (FlagError opt i s) = do+            let optName = (maybe "" (:[]) $ flagShort $ flagFragments opt) ++ " " ++ (maybe "" id $ flagLong $ flagFragments opt)+             in ("error: " ++ show i ++ " option " ++ optName ++ " : " ++ s ++ "\n")++        errorUnnamedArgument err =+            OptionError $ mconcat+                [ "error: " ++ err+                , ""+                ]++        errorExpectingMode subs =+            OptionError $ mconcat (+                [ "error: expecting one of the following mode:\n"+                , "\n"+                ] ++ map (indent 4 . (++ "\n") . fst) subs)+        errorInvalidMode got subs =+            OptionError $ mconcat (+                [ "error: invalid mode '" ++ got ++ "', expecting one of the following mode:\n"+                , ""+                ] ++ map (indent 4 . (++ "\n") . fst) subs)++indent :: Int -> String -> String+indent n s = replicate n ' ' ++ s++-- | Set the program name+programName :: String -> OptionDesc r ()+programName s = modify $ \st -> st { stMeta = (stMeta st) { programMetaName = Just s } }++-- | Set the program version+programVersion :: Version -> OptionDesc r ()+programVersion s = modify $ \st -> st { stMeta = (stMeta st) { programMetaVersion = Just $ showVersion s } }++-- | Set the program description+programDescription :: String -> OptionDesc r ()+programDescription s = modify $ \st -> st { stMeta = (stMeta st) { programMetaDescription = Just s } }++-- | Set the description for a command+description :: String -> OptionDesc r ()+description doc = modify $ \st -> st { stCT = setDescription doc (stCT st) }++modifyHier :: (CommandHier r -> CommandHier r) -> Command r -> Command r+modifyHier f (Command hier desc opts act) = Command (f hier) desc opts act++modifyCT :: (Command r -> Command r) -> OptionDesc r ()+modifyCT f = modify $ \st -> st { stCT = f (stCT st) }++-- | Create a new sub command+command :: String -> OptionDesc r () -> OptionDesc r ()+command name sub = do+    let subSt = gatherDesc sub+    modifyCT (addCommand (stCT subSt))+    --modify $ \st -> st { stCT = addCommand (stCT subSt) $ stCT st }+  where addCommand subTree = modifyHier $ \hier ->+            case hier of+                CommandLeaf _ -> CommandTree [(name,subTree)]+                CommandTree t -> CommandTree ((name, subTree) : t)++-- | Set the action to run in this command+action :: Action r -> OptionDesc r ()+action ioAct = modify $ \st -> st { stCT = setAction ioAct (stCT st) }++-- | Flag option either of the form -short or --long+--+-- for flag that doesn't have parameter, use 'flag'+flagParam :: FlagFrag -> FlagParser a -> OptionDesc r (FlagParam a)+flagParam frag fp = do+    nid <- getNextID++    let fragmentFlatten = flattenFragments frag++    let opt = FlagDesc+                { flagFragments   = fragmentFlatten+                , flagNid         = nid+                , F.flagArg       = argp+                , flagArgValidate = validator+                , flagArity       = 1+                }++    modify $ \st -> st { stCT = addOption opt (stCT st) }++    case mopt of+        Just a  -> return (FlagParamOpt nid a parser)+        Nothing -> return (FlagParam nid parser)+  where+    (argp, parser, mopt, validator) = case fp of+        FlagRequired p   -> (FlagArgHave, toArg p, Nothing, isValid p)+        FlagOptional a p -> (FlagArgMaybe, toArg p, Just a, isValid p)++    toArg :: (String -> Either String a) -> String -> a+    toArg p = either (error "internal error toArg") id . p++    isValid f = either FlagArgInvalid (const FlagArgValid) . f++flagMany :: OptionDesc r (FlagParam a) -> OptionDesc r (FlagMany a)+flagMany fp = do+    f <- fp+    let nid = case f of+                FlagParamOpt n _ _ -> n+                FlagParam n _      -> n+    modify $ \st -> st { stCT = tweakOption nid (\fd -> fd { flagArity = maxBound }) (stCT st) }+    return $ FlagMany f++-- | Flag option either of the form -short or --long+--+-- for flag that expect a value (optional or mandatory), uses 'flagArg'+flag :: FlagFrag -> OptionDesc r (Flag Bool)+flag frag = do+    nid <- getNextID++    let fragmentFlatten = flattenFragments frag++    let opt = FlagDesc+                { flagFragments   = fragmentFlatten+                , flagNid         = nid+                , F.flagArg       = FlagArgNone+                , flagArgValidate = error ""+                , flagArity       = 0+                }++    modify $ \st -> st { stCT = addOption opt (stCT st) }+    return (Flag nid)++-- | An unnamed argument+--+-- For now, argument in a point of tree that contains sub trees will be ignored.+-- TODO: record a warning or add a strict mode (for developping the CLI) and error.+argument :: String -> ValueParser a -> OptionDesc r (Arg a)+argument name fp = do+    idx <- getNextIndex+    let a = Argument { argumentName        = name+                     , argumentDescription = ""+                     , argumentValidate    = either Just (const Nothing) . fp+                     }+    modifyCT $ addArg a+    return (Arg idx (either (error "internal error") id . fp))++remainingArguments :: String -> OptionDesc r (ArgRemaining [String])+remainingArguments name = do+    let a = ArgumentCatchAll { argumentName        = name+                             , argumentDescription = ""+                             }+    modifyCT $ addArg a+    return ArgsRemaining++-- | give the ability to set options that are conflicting with each other+-- if option a is given with option b then an conflicting error happens+conflict :: Flag a -> Flag b -> OptionDesc r ()+conflict = undefined
+ Console/Options/Flags.hs view
@@ -0,0 +1,146 @@+module Console.Options.Flags+    ( parseFlags+    , FlagDesc(..)+    , FlagFragments(..)+    , Flag+    , FlagArgValidation(..)+    , FlagArgDesc(..)+    , FlagError(..)+    -- * fragments+    , FlagFrag(FlagShort, FlagLong, FlagDescription)+    , flattenFragments+    ) where++import Control.Applicative+import Control.Monad+import Console.Options.Nid+import Data.List+import Data.Monoid++data FlagArgValidation = FlagArgValid | FlagArgInvalid String++-- | How to parse a specific flag+data FlagDesc = FlagDesc+    { flagFragments     :: FlagFragments+    , flagNid           :: Nid                    -- ^ flag number. internal value+    , flagArg           :: FlagArgDesc            -- ^ parser for the argument to an flag+    , flagArgValidate   :: String -> FlagArgValidation -- ^ if the argument doesn't validate, return the error message associated, otherwise Nothing+    , flagArity         :: Int+    }++data FlagFrag =+      FlagShort       Char+    | FlagLong        String+    | FlagDescription String+    -- | FlagDefault     String+    | FlagMany        [FlagFrag]+    deriving (Show,Eq)++data FlagFragments = FlagFragments+    { flagShort         :: Maybe Char             -- ^ short flag parser 'o'+    , flagLong          :: Maybe String           -- ^ long flag "flag"+    , flagDescription   :: Maybe String           -- ^ Description of this "flag"+    --, flagDefault       :: Maybe String           -- ^ Has a default+    }++flattenFragments :: FlagFrag -> FlagFragments+flattenFragments frags =+    foldl' flat startVal $ case frags of+                                FlagMany l -> l+                                _          -> [frags]+  where+    startVal = FlagFragments Nothing Nothing Nothing+    flat ff  (FlagShort f)       = ff { flagShort = Just f }+    flat ff  (FlagLong f)        = ff { flagLong = Just f }+    flat ff  (FlagDescription f) = ff { flagDescription = Just f }+    flat acc (FlagMany l)        = foldl' flat acc l++instance Monoid FlagFrag where+    mempty                              = FlagMany []+    mappend (FlagMany l1) (FlagMany l2) = FlagMany (l1 ++ l2)+    mappend (FlagMany l1) o             = FlagMany (l1 ++ [o])+    mappend o             (FlagMany l2) = FlagMany (o : l2)+    mappend o1            o2            = FlagMany [o1,o2]++-- | Whether a flag has an argument, an optional one or always an argument+data FlagArgDesc =+      FlagArgNone+    | FlagArgMaybe+    | FlagArgHave+    deriving (Show,Eq)++data Matching a = NoMatching | Matching a | MatchingWithArg a String++-- | the state of parsing the command line arguments+data ParseState a = ParseState [Flag]      -- Args : in reverse order+                               [String]    -- Unparsed: in reverse order+                               [FlagError] -- errors: in reverse order++type Flag = (Nid, Maybe String)++data FlagError = FlagError FlagDesc Int String++parseFlags :: [FlagDesc]+           -> [String]+           -> ([Flag], [String], [FlagError])+parseFlags flagParsers = loop (ParseState [] [] []) [1..]+  where+        loop :: ParseState a -> [Int] -> [String] -> ([Flag], [String], [FlagError])+        loop _                      []     _      = error "impossible case"+        loop (ParseState os us ers) _      []     = (reverse os, reverse us, reverse ers)+        loop (ParseState os us ers) (i:is) (a:as) =+            case a of+                '-':'-':[]   -> (reverse os, reverse us ++ as, reverse ers)+                '-':'-':long -> loop (processFlag (findLong long)) is as+                '-':short:[] -> loop (processFlag (findShort short)) is as+                _            -> loop (ParseState os (a:us) ers) is as+          where processFlag NoMatching     = ParseState os (a:us) ers+                processFlag (Matching opt) =+                    case flagArg opt of+                        FlagArgNone  -> ParseState ((flagNid opt, Nothing) : os) us ers+                        FlagArgMaybe -> ParseState ((flagNid opt, Nothing) : os) us ers+                        FlagArgHave  ->+                            case as of+                                []     -> let e = mkFlagError opt "required argument missing"+                                           in ParseState os (a:us) (e:ers)+                                (x:_) ->+                                    case (flagArgValidate opt) x of+                                        FlagArgValid          -> ParseState ((flagNid opt, Just x):os) us ers+                                        FlagArgInvalid optErr ->+                                            let e = mkFlagError opt ("invalid argument: " ++ optErr)+                                             in ParseState os us (e:ers)+                processFlag (MatchingWithArg opt arg) =+                    case flagArg opt of+                        FlagArgNone  -> let e = mkFlagError opt "invalid argument, expecting no argument" -- fixme: tell which flag+                                         in ParseState os (a:us) (e:ers)+                        FlagArgMaybe ->+                            case (flagArgValidate opt) arg of+                                FlagArgValid   -> ParseState ((flagNid opt, Just arg):os) us ers+                                FlagArgInvalid optErr ->+                                    let e = mkFlagError opt ("invalid argument: " ++ optErr)+                                     in ParseState os us (e:ers)+                        FlagArgHave  ->+                            case (flagArgValidate opt) arg of+                                FlagArgValid          -> ParseState ((flagNid opt, Just arg):os) us ers+                                FlagArgInvalid optErr ->+                                    let e = mkFlagError opt ("invalid argument: " ++ optErr)+                                     in ParseState os us (e:ers)++                mkFlagError opt s = FlagError opt i s++        findShort short = findRetArg (flagShortMatch short) flagParsers+        findLong long = findRetArg (flagLongMatch long) flagParsers++        flagShortMatch toMatch opt = maybe NoMatching (\x -> if x == toMatch then Matching opt else NoMatching) $ flagShort $ flagFragments opt+        flagLongMatch toMatch opt = maybe NoMatching match $ flagLong $ flagFragments opt+          where match optLong+                    | leftPart == optLong && null rightPart           = Matching opt+                    | leftPart == optLong && isPrefixOf "=" rightPart = MatchingWithArg opt (drop 1 rightPart)+                    | otherwise                                       = NoMatching+                  where (leftPart,rightPart) = splitAt (length optLong) toMatch++        findRetArg _ []         = NoMatching+        findRetArg f (opt:opts) =+            case f opt of+                NoMatching -> findRetArg f opts+                r          -> r
+ Console/Options/Monad.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE Rank2Types #-}+module Console.Options.Monad+    ( ProgramDesc(..)+    , ProgramMeta(..)+    , OptionDesc+    , gatherDesc+    , getNextID+    , getNextIndex+    ) where++import           Console.Options.Nid+import           Console.Options.Types+import           Console.Options.Utils+import           Control.Monad.State+import           Control.Monad.Identity+import           System.Exit++-- the current state of the program description+-- as the monad unfold ..+data ProgramDesc r = ProgramDesc+    { stMeta        :: ProgramMeta+    , stCT          :: Command r     -- the command with the return type of actions+    , stNextID      :: !NidGenerator -- next id for flag+    , stNextIndex   :: !UnnamedIndex -- next index for unnamed argument+    }++data ProgramMeta = ProgramMeta+    { programMetaName        :: Maybe String+    , programMetaDescription :: Maybe String+    , programMetaVersion     :: Maybe String+    , programMetaHelp        :: [String]+    }++programMetaDefault :: ProgramMeta+programMetaDefault = ProgramMeta Nothing Nothing Nothing ["-h", "--help"]++-- OptionDesc (return value of action) a+newtype OptionDesc r a = OptionDesc { runOptionDesc :: StateT (ProgramDesc r) Identity a }+    deriving (Functor,Applicative,Monad,MonadState (ProgramDesc r))++gatherDesc :: OptionDesc r a -> ProgramDesc r+gatherDesc dsl = runIdentity $ execStateT (runOptionDesc dsl) initialProgramDesc++initialProgramDesc :: ProgramDesc r+initialProgramDesc = ProgramDesc { stMeta        = programMetaDefault+                                 , stCT          = iniCommand+                                 , stNextID      = nidGenerator+                                 , stNextIndex   = 0+                                 }+  where+    iniCommand :: Command r+    iniCommand = Command (CommandLeaf []) "..." [] Nothing++-- | Return the next unique argument ID+getNextID :: OptionDesc r Nid+getNextID = do+    (nid, nidGen) <- nidNext . stNextID <$> get+    modify $ \st -> st { stNextID = nidGen }+    return nid++getNextIndex :: OptionDesc r UnnamedIndex+getNextIndex = do+    idx <- stNextIndex <$> get+    modify $ \st -> st { stNextIndex = idx + 1 }+    return idx
+ Console/Options/Nid.hs view
@@ -0,0 +1,21 @@+module Console.Options.Nid+    ( Nid+    , NidGenerator+    , nidGenerator+    , nidNext+    ) where++-- | A unique ID+newtype Nid = Nid Int+    deriving (Show,Eq,Ord)++-- | A unique ID generator+newtype NidGenerator = NidGenerator Int++-- | Create a new unique generator+nidGenerator :: NidGenerator+nidGenerator = NidGenerator 0++-- | get the next unique id and return a new generator+nidNext :: NidGenerator -> (Nid, NidGenerator)+nidNext (NidGenerator next) = (Nid next, NidGenerator (next+1))
+ Console/Options/Types.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE TypeFamilies #-}+module Console.Options.Types+    ( Argument(..)+    , Command(..)+    , CommandHier(..)+    , Action+    , UnnamedIndex+    -- * User Binders to retrieve their options+    , Flag(..)+    , FlagLevel(..)+    , FlagParam(..)+    , FlagMany(..)+    , Arg(..)+    , ArgRemaining(..)+    , Params(..)+    , Param+    , getParams+    ) where++import           Console.Options.Flags (FlagDesc)+import           Console.Options.Nid++-- | A unnamed argument+data Argument =+      Argument+        { argumentName        :: String+        , argumentDescription :: String+        , argumentValidate    :: String -> Maybe String+        }+    | ArgumentCatchAll+        { argumentName        :: String+        , argumentDescription :: String+        }++data Flag a where+    Flag       :: Nid -> Flag Bool++data FlagLevel a where+    FlagLevel  :: Nid -> FlagLevel Int++data FlagParam a where+    FlagParamOpt     :: Nid -> a -> (String -> a) -> FlagParam a+    FlagParam        :: Nid -> (String -> a) -> FlagParam a++newtype FlagMany a = FlagMany (FlagParam a)++data Arg a where+    Arg           :: UnnamedIndex -> (String -> a) -> Arg a++data ArgRemaining a where+    ArgsRemaining :: ArgRemaining [String]++type UnnamedIndex = Int++-- A command that is composed of a hierarchy+--+data Command r = Command+    { getCommandHier        :: CommandHier r+    , getCommandDescription :: String+    , getCommandOptions     :: [FlagDesc]+    , getCommandAction      :: Maybe (Action r)+    }++-- | Recursive command tree+data CommandHier r =+      CommandTree [(String, Command r)]+    | CommandLeaf [Argument]+++data Params = Params+    { paramsFlags         :: [(Nid, Maybe String)]+    , paramsPinnedArgs    :: [String]+    , paramsRemainingArgs :: [String]+    }++-- | Represent a program to run+type Action r = (forall a p . Param p => p a -> Ret p a) -> r -- flags++class Param p where+    type Ret p a :: *+    getParams :: Params -> (forall a . p a -> Ret p a)++{-+flag :: optional on command line+    | no value       -> Bool+param :: optional on command line but with a value+    | optional value -> Maybe a+    | required value -> Maybe a+arg :: required on command line+    | required value (itself) -> a+-}+instance Param Flag where+    type Ret Flag a = Bool+    getParams (Params flagArgs _ _) (Flag nid) =+        maybe False (const True) $ lookup nid flagArgs+instance Param FlagLevel where+    type Ret FlagLevel a = Int+    getParams (Params flagArgs _ _) (FlagLevel nid) =+        length $ filter ((== nid) . fst) flagArgs+instance Param FlagParam where+    type Ret FlagParam a = Maybe a+    getParams (Params flagArgs _ _) (FlagParamOpt nid a p) =+        case lookup nid flagArgs of+            Just (Just param) -> Just (p param)+            Just Nothing      -> Just a+            Nothing           -> Nothing+    getParams (Params flagArgs _ _) (FlagParam nid p) =+        case lookup nid flagArgs of+            Just Nothing      -> error "internal error: parameter is missing" -- something is wrong with the flag parser+            Just (Just param) -> Just (p param)+            Nothing           -> Nothing+instance Param FlagMany where+    type Ret FlagMany a = [a]+    getParams (Params flagArgs _ _) (FlagMany (FlagParamOpt nid a p)) =+        let margs = map snd $ filter ((== nid) . fst) flagArgs+         in map (maybe a p) margs+    getParams (Params flagArgs _ _) (FlagMany (FlagParam nid p)) =+        let margs = map snd $ filter ((== nid) . fst) flagArgs+         in map (maybe (error "") p) margs+instance Param Arg where+    type Ret Arg a = a+    getParams (Params _ unnamedArgs _) (Arg index p) =+        p (unnamedArgs !! index)+instance Param ArgRemaining where+    type Ret ArgRemaining a = [String]+    getParams (Params _ _ otherArgs) _ = otherArgs
+ Console/Options/Utils.hs view
@@ -0,0 +1,10 @@+module Console.Options.Utils+    ( hPutErrLn+    ) where++import           System.IO (hPutStrLn, stderr)++-- add implementation for windows+hPutErrLn :: String -> IO ()+hPutErrLn = hPutStrLn stderr+
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2014 Nicolas DI PRIMA <nicolas@di-prima.fr>+Copyright (c) 2015-2016 Vincent Hanquez <vincent@snarc.org>  All rights reserved. 
README.md view
@@ -1,6 +1,9 @@-# CLI+cli+======= +[![Build Status](https://travis-ci.org/vincenthz/hs-cli.png?branch=master)](https://travis-ci.org/vincenthz/hs-cli) [![BSD](http://b.repl.ca/v1/license-BSD-blue.png)](http://en.wikipedia.org/wiki/BSD_licenses) [![Haskell](http://b.repl.ca/v1/language-haskell-lightgrey.png)](http://haskell.org)+  Documentation: [cli on hackage](http://hackage.haskell.org/package/cli)
cli.cabal view
@@ -1,46 +1,62 @@ Name:                cli-Version:             0.0.5-Synopsis:            Simple Command Line Interface Library-Description:-    This package provides a simple Command Line Library+Version:             0.1.0+Synopsis:            Command Line Interface+Description: +    All you need for interacting with users at the Console level+    .+    * Display routines, formatting, progress bars+    .+    * Options parsing+    . License:             BSD3 License-file:        LICENSE-Copyright:           Nicolas DI PRIMA <nicolas@di-prima.fr>-Author:              Nicolas DI PRIMA <nicolas@di-prima.fr>-Maintainer:          Nicolas DI PRIMA <nicolas@di-prima.fr>-                     Vincent Hanquez <vincent@snarc.org>-Category:            Application+Copyright:           Vincent Hanquez <vincent@snarc.org>, Nicolas Di Prima <nicolas@di-prima.fr>+Author:              Vincent Hanquez <vincent@snarc.org>, Nicolas Di Prima <nicolas@di-prima.fr>+Maintainer:          vincent@snarc.org+Category:            cli Stability:           experimental Build-Type:          Simple-Homepage:            https://github.com/NicolasDP/hs-cli+Homepage:            https://github.com/vincenthz/hs-cli+Bug-Reports:         https://github.com/vincenthz/hs-cli/issues Cabal-Version:       >=1.10 extra-source-files:  README.md +source-repository head+  type: git+  location: https://github.com/vincenthz/hs-cli+ Library-  Exposed-modules:   Application.CLI-  Other-modules:     Application.CLI.Types-                   , Application.CLI.Class+  Exposed-modules:   Console.Display+                   , Console.Options+  Other-modules:     Console.Options.Nid+                   , Console.Options.Flags+                   , Console.Options.Monad+                   , Console.Options.Types+                   , Console.Options.Utils   Build-depends:     base >= 4 && < 5-                   , containers+                   , transformers+                   , mtl+                   , terminfo   ghc-options:       -Wall -fwarn-tabs-  Default-Language:  Haskell2010+  default-language:  Haskell2010 -source-repository head-  type: git-  location: https://github.com/NicolasDP/hs-cli -Flag executable-    default: False+--Executable           cli+--  Main-Is:           cli.hs+-- mtl ghc-options:       -Wall -fno-warn-missing-signatures+--  Hs-Source-Dirs:    .+--  Build-depends:     base >= 4 && < 5+--  default-language:  Haskell2010 -Executable Example-  Main-Is:           Main.hs-  hs-source-dirs:    example-  ghc-options:       -Wall -fno-warn-orphans -fno-warn-missing-signatures-  Default-Language:  Haskell2010-  if flag(executable)-    Build-depends:   base >= 4 && < 5-                   , cli+Test-Suite test-cli+  type:              exitcode-stdio-1.0+  hs-source-dirs:    tests+  Main-is:           Cli.hs+  default-language:  Haskell2010+  Build-Depends:     base >= 3 && < 5                    , directory-    buildable: True-  else-    buildable: False+                   , tasty+                   , tasty-quickcheck+                   , QuickCheck+                   , cli+  ghc-options:       -Wall -fno-warn-orphans -fno-warn-missing-signatures -fwarn-tabs -fno-warn-unused-imports
− example/Main.hs
@@ -1,63 +0,0 @@--- |--- Module      : Main--- License     : BSD-Style--- Copyright   : Copyright © 2014 Nicolas DI PRIMA------ Maintainer  : Nicolas DI PRIMA <nicolas@di-prima.fr>--- Stability   : experimental--- Portability : unknown----module Main-    ( main-    ) where--import Application.CLI-import Control.Applicative-import Data.List-import System.Directory--data CLIls = CLIls FilePath-instance CLI CLIls where-    name _ = "ls"-    desc _ = "list file in the current directory"-    options _ = [ OptHelp ["-a", "--all"] Nothing "include hidden files"-                , OptHelp ["-s", "--separator"] (Just "string") "a separator to put between the found files (default is \" \")"-                , OptHelp [] (Just "directory") "specify a directory to list the content from"-                ]-    action (CLIls dir) _ =-        withOptionalParameterStr ["-s", "--separator"] $ \mseparator ->-        withFlag ["-a", "--all"] $ \withHidden ->-        withOptionalStr $ \mdir _ -> do-            let separator = maybe " " id mseparator-            list <- getDirectoryContents $ maybe dir id $ mdir-            if withHidden-                then putStrLn $ intercalate separator list-                else putStrLn $ intercalate separator $ filter (not . isHiddenFile) list-      where-        isHiddenFile :: FilePath -> Bool-        isHiddenFile [] = True-        isHiddenFile ('.':_) = True-        isHiddenFile _ = False--data CLIla = CLIla-instance CLI CLIla where-    name _ = "la"-    desc _ = "list file in the current directory -- plus hidden one"-    options _ = [ OptHelp [] (Just "directory") "the directory to list the content from (mandatory)"-                , OptHelp ["-p", "--prefix"] (Just "string") "a prefix to the output"-                ]-    action _ _ =-        withOptionalParameterStr ["-p", "--prefix"] $ \mprefix ->-        withStr "directory" $ \dir _ -> do-            let prefix = maybe "" id mprefix-            list <- filter (not . flip elem [ ".", ".." ]) <$> getDirectoryContents dir-            mapM_ putStrLn $ map ((++) prefix) list--main :: IO ()-main = do-    dir <- getCurrentDirectory-    defaultMain-        $ with Help-        $ with CLIla-        $ with (CLIls dir)-        $ initialize "Example of use of CLI Library"
+ tests/Cli.hs view
@@ -0,0 +1,52 @@+module Main (main) where++import           Control.Applicative+import           Control.Monad++import           Test.Tasty+import           Test.Tasty.QuickCheck+import           Test.QuickCheck.Monadic++import           Storage.HashFS.Meta+import           System.Directory+import           Data.List+import           Data.Monoid+import           Data.Functor.Identity++import           Console.Options hiding (defaultMain)++flagA = flagParam (FlagShort 'a' <> FlagLong "aaa") (FlagRequired Right)+flagB = flagParam (FlagShort 'b' <> FlagLong "bbb") (FlagRequired Right)++commandFoo = command "foo" $ do+    action $ \toParam -> return True++--commandBar :: Monad m => OptionDesc (m Bool) ()+commandBar :: OptionDesc (Identity Bool) ()+commandBar = command "bar" $ do+    a <- flagA+    action $ \toParam -> do+        return True++testParseHelp name f = testProperty name $ runIdentity $ do+    case snd f of+        OptionHelp -> return True+        _          -> return False++testParseSuccess name values f =+    testProperty name $ runIdentity $ do+        let (_,r) = f+         in case r of+                OptionSuccess p act -> return (sort values == sort (paramsFlags p)) --act (getParams p)+                _                   -> return False++main = defaultMain $ testGroup "options"+    [ testGroup "help"+        [ testParseHelp "1" $ parseOptions (commandBar) ["options", "argument", "--help", "a"]+        , testParseHelp "2" $ parseOptions (commandBar) ["options", "argument", "-h", "a"]+        ]+    , testGroup "success"+        [ testParseSuccess "1" [] $ parseOptions (commandBar) ["bar", "-a", "option-a"]+        , testParseSuccess "2" [] $ parseOptions (commandBar >> commandFoo) ["foo", "a"]+        ]+    ]