cmdtheline (empty) → 0.1.0.0
raw patch · 21 files changed
+3021/−0 lines, 21 filesdep +basedep +containersdep +data-defaultsetup-changed
Dependencies added: base, containers, data-default, directory, parsec, pretty, process
Files
- LICENSE +18/−0
- README.md +33/−0
- Setup.hs +2/−0
- cmdtheline.cabal +52/−0
- doc/examples/FizzBuzz.hs +69/−0
- doc/examples/cipher.hs +222/−0
- doc/examples/cp.hs +102/−0
- doc/examples/fail.hs +84/−0
- doc/examples/grep.hs +28/−0
- doc/examples/hello.hs +26/−0
- doc/examples/poly.hs +189/−0
- src/System/Console/CmdTheLine.hs +130/−0
- src/System/Console/CmdTheLine/Arg.hs +421/−0
- src/System/Console/CmdTheLine/ArgVal.hs +292/−0
- src/System/Console/CmdTheLine/CmdLine.hs +162/−0
- src/System/Console/CmdTheLine/Common.hs +207/−0
- src/System/Console/CmdTheLine/Err.hs +116/−0
- src/System/Console/CmdTheLine/Help.hs +262/−0
- src/System/Console/CmdTheLine/Manpage.hs +234/−0
- src/System/Console/CmdTheLine/Term.hs +271/−0
- src/System/Console/CmdTheLine/Trie.hs +101/−0
+ LICENSE view
@@ -0,0 +1,18 @@+Copyright © 2012 Vincent Elisha Lee Frey++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the Software is furnished to do so,+subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,33 @@+CmdTheLine v0.1+===============++Command line option parsing with applicative functors.++Installation+------------+To install, do++ cabal install++Depends+-------+All dependencies are provided by the [Haskell+Platform](http://hackage.haskell.org/platform). See cmdtheline.cabal for library+dependencies.++Docs+----+If you have enabled documentation in your cabal `config` file, after+installation you should have a copy of the documentation locally. Otherwise+see the [Hackage repo](http://hackage.haskell.org/package/cmdtheline).++Bugs+----+Please report bugs to the+[issue tracker](http://github.com/eli-frey/cmdtheline/issues).++LICENSE+-------+MIT - See file 'LICENSE' for details.++Copyright © 2012 Vincent Elisha Lee Frey
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cmdtheline.cabal view
@@ -0,0 +1,52 @@+Name: cmdtheline+Version: 0.1.0.0+Synopsis: Declaritive command-line option parsing and documentation library.+Description:+ CmdTheLine aims to remove tedium from the definition of command-line+ programs, producing usage, help and man pages with little effort.+ .+ The inspiration was found in Daniel Bunzli's+ <http://erratique.ch/software/cmdliner> library.+ .+ CmdTheLine uses applicative functors to provide a declarative, compositional+ mechanism for defining command-line programs by lifting regular Haskell+ functions over argument parsers.+ .+ Suggestions, comments, and bug reports are appreciated. Please see the+ bug and issue tracker at <http://github.com/eli-frey/cmdtheline>.+++Homepage: http://github.com/eli-frey/cmdtheline+License: MIT+License-file: LICENSE+Author: Eli Frey+Maintainer: Eli Frey <eli.lee.frey gmail com>+Stability: Experimental+Category: Console+Cabal-version: >=1.6+Build-type: Simple++Extra-source-files: doc/examples/*.hs, README.md++Source-repository head+ type: git+ location: git://github.com/eli-frey/cmdtheline.git++Library+ hs-source-dirs: src+ extensions: FlexibleInstances+ build-depends: base >= 4.5 && < 5, containers >= 0.4 && < 0.5,+ parsec >= 3.1 && < 3.2, pretty >= 1.1 && < 1.2,+ process >= 1.1, directory >= 1.1, data-default >= 0.4++ exposed-modules: System.Console.CmdTheLine,+ System.Console.CmdTheLine.Arg,+ System.Console.CmdTheLine.ArgVal,+ System.Console.CmdTheLine.Term++ other-modules: System.Console.CmdTheLine.Common,+ System.Console.CmdTheLine.Err,+ System.Console.CmdTheLine.Help,+ System.Console.CmdTheLine.Trie,+ System.Console.CmdTheLine.Manpage,+ System.Console.CmdTheLine.CmdLine
+ doc/examples/FizzBuzz.hs view
@@ -0,0 +1,69 @@+import System.Console.CmdTheLine+import Control.Applicative++import System.Exit ( exitSuccess )++data Verbosity = Verbose | Normal | Silent++fizzBuzz :: Verbosity -> String -> String -> Int -> IO ()+fizzBuzz verb fizz buzz n = do+ case verb of+ Verbose -> putStrLn $ concat+ [ "FizzBuzz: Printing the result of FizzBuzz over the numbers 1 to ", show n+ , "\nFizz = ", fizz, "\nBuzz = ", buzz+ ]+ Silent -> exitSuccess+ _ -> return ()++ mapM_ fizzAndBuzzOr [1..n]+ where+ fizzAndBuzzOr n = putStrLn output+ where+ output = case fizz' ++ buzz' of+ "" -> show n+ str -> str++ fizz' = if (n `mod` 3) == 0 then fizz else ""+ buzz' = if (n `mod` 5) == 0 then buzz else ""+++-- A flag that can appear many times on the command line, only the last of+-- which will be counted.+verbosity :: Term Verbosity+verbosity = lastOf $ vFlagAll [Normal] [ ( Verbose, verbose )+ , ( Silent, silent )+ ]+ where+ verbose =(optInfo [ "verbose", "v" ])+ { argDoc = "Give verbose output." }++ silent =(optInfo [ "quiet", "silent", "q", "s" ])+ { argDoc = "Provide no output." }++fizz, buzz :: Term String+fizz = opt "Fizz" $ (optInfo [ "Fizz", "fizz", "f" ])+ { argDoc = "A string to print in the 'Fizz' case." }++buzz = opt "Buzz" $ (optInfo [ "Buzz", "buzz", "b" ])+ { argDoc = "A string to print in the 'Buzz' case." }++times :: Term Int+times = opt 100 $ (optInfo [ "times", "t" ])+ { argName = "TIMES"+ , argDoc = "Run $(mname) for the numbers 1 to $(argName)."+ }++term :: Term (IO ())+term = fizzBuzz <$> verbosity <*> fizz <*> buzz <*> times++termInfo :: TermInfo+termInfo = def+ { termName = "FizzBuzz"+ , version = "v1.0"+ , termDoc = "An implementation of the world renowned FizzBuzz algorithm."+ , man = [ S "BUGS"+ , P "Email bug reports to <sirFrancisDrank@example.com>"+ ]+ }++main = run ( term, termInfo )
+ doc/examples/cipher.hs view
@@ -0,0 +1,222 @@+import System.Console.CmdTheLine+import Control.Applicative+import Data.Default++import Data.Char ( isUpper, isAlpha, isAlphaNum, isSpace+ , toLower+ )+import Data.List ( intersperse )++import System.IO+import System.Exit++infixr 2 <||>+-- Split a value between predicates and 'or' the results together+(<||>) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool)+p <||> p' = (||) <$> p <*> p'+++--+-- Rot+--++data Cycle a = Cycle+ { backward :: (Cycle a)+ , at :: a+ , forward :: (Cycle a)+ }++fromList :: [a] -> Cycle a+fromList [] = error "Cycle must have at least one element"+fromList xs = first+ where+ ( first, last ) = go last xs first++ go :: Cycle a -> [a] -> Cycle a -> ( Cycle a, Cycle a )+ go prev [] next = ( next, prev )+ go prev (x : xs) next = ( this, last )+ where+ this = Cycle prev x rest+ (rest,last) = go this xs next++-- Return a Cycle centered on x.+seekTo :: Eq a => a -> Cycle a -> Cycle a+seekTo x xs+ | x == at xs = xs+ | otherwise = seekTo x $ forward xs+ +-- Seek n places forwards or backwards.+seek :: Bool -> Int -> Cycle a -> Cycle a+seek _ 0 xs = xs+seek back n xs = seek back (n - 1) (dir xs)+ where+ dir = if back then backward else forward++lowers = fromList ['a'..'z']+uppers = fromList ['A'..'Z']++rot :: Bool -> Int -> Maybe String -> IO ()+rot back n mStr = do+ input <- case mStr of+ Nothing -> getContents+ Just str -> return str++ putStrLn $ map rotChar input+ where+ rotChar c = if isAlpha c then c' else c+ where+ c' = at . seek back n $ seekTo c cycle+ cycle = if isUpper c then uppers else lowers+++--+-- Morse+--++switch ( x, y ) = ( y, x )++fromCode = map switch toCode+toCode =+ [ ( 'a', ".-" ), ( 'b', "-..." ), ( 'c', "-.-." ), ( 'd', "-.." )+ , ( 'e', "." ), ( 'f', "..-." ), ( 'g', "--." ), ( 'h', "...." )+ , ( 'i', ".." ), ( 'j', ".---" ), ( 'k', "-.-" ), ( 'l', ".-.." )+ , ( 'm', "--" ), ( 'n', "-." ), ( 'o', "---" ), ( 'p', ".--." )+ , ( 'q', "--.-" ), ( 'r', ".-." ), ( 's', "..." ), ( 't', "-" )+ , ( 'u', "..-" ), ( 'v', "...-" ), ( 'w', ".--" ), ( 'x', "-..-" )+ , ( 'y', "-.--" ), ( 'z', "--.." ), ( '1', ".----" ), ( '2', "..---" )+ , ( '3', "...--" ), ( '4', "....-" ), ( '5', "....." ), ( '6', "-...." )+ , ( '7', "--..." ), ( '8', "---.." ), ( '9', "----." ), ( '0', "-----" )+ , ( ' ', "/" )+ ]++fromMorse, toMorse :: [String] -> Maybe String+fromMorse = mapM (`lookup` fromCode)++toMorse strs = sepCat " / " <$> mapM convertLetters strs+ where+ convertLetters chars = sepCat " " <$> mapM (`lookup` toCode) chars++ sepCat sep = concat . intersperse sep++morse :: Bool -> Maybe String -> IO ()+morse from mStr = do+ input <- case mStr of+ Nothing -> getContents+ Just str -> return str++ if all pred input+ then return ()+ else do hPutStrLn stderr err+ exitFailure++ convert input+ where+ pred = if from then (== '/') <||> (== '-') <||> (== '.') <||> isSpace+ else isAlphaNum <||> isSpace++ err = if from+ then "cipher: morse input must be all spaces, '/'s, '-'s, and '.'s."+ else "cipher: morse input must be alphanumeric and/or spaces"++ convert str = maybe badConvert putStrLn . convert' . words $ map toLower str+ where+ badConvert = hPutStrLn stderr err >> exitFailure+ where+ err = if from then "cipher: could not convert from morse"+ else "cipher: could not convert to morse"++ convert' = if from then fromMorse else toMorse+++--+-- Terms+--++-- The heading under which to place common options.+comOpts = "COMMON OPTIONS"++-- A modified default 'TermInfo' to be shared by commands.+def' :: TermInfo+def' = def+ { man =+ [ S comOpts+ , P "These options are common to all commands."+ , S "MORE HELP"+ , P "Use '$(mname) $(i,COMMAND) --help' for help on a single command."+ , S "BUGS"+ , P "Email bug reports to <snideHighland@example.com>"+ ]+ , stdOptSection = comOpts+ }++-- 'input' is a common option. We set its 'argSection' field to 'comOpts' so+-- that it is placed under that heading instead of the default '"OPTIONS"'+-- heading, which we will reserve for command-specific options.+input = opt Nothing (optInfo [ "input", "i" ])+ { argName = "INPUT"+ , argDoc = "For specifying input on the command line. If present, "+ ++ "input is not read form standard-in."+ , argSection = comOpts+ }+++rotTerm = ( rot <$> back <*> n <*> input, termInfo )+ where+ back = flag (optInfo [ "back", "b" ])+ { argName = "BACK"+ , argDoc = "Rotate backwards instead of forwards."+ }++ n = opt 13 (optInfo [ "n" ])+ { argName = "N"+ , argDoc = "How many places to rotate by."+ }++ termInfo = def'+ { termName = "rot"+ , termDoc = "Rotate the input characters by N."+ , man = [ S "DESCRIPTION"+ , P $ "Rotate input gathered from INPUT or standard-in N "+ ++ "places. The input must be composed totally of "+ ++ "alphabetic characters and spaces."+ ] ++ man def'+ }+++morseTerm = ( morse <$> from <*> input, termInfo )+ where+ from = flag (optInfo [ "from", "f" ])+ { argName = "FROM"+ , argDoc = "Convert from morse-code to the Latin alphabet. "+ ++ "If absent, convert from Latin alphabet to morse-code."+ }++ termInfo = def'+ { termName = "morse"+ , termDoc = "Convert to and from morse-code."+ , man = [ S "DESCRIPTION"+ , P desc+ ] ++ man def'+ }++ desc = concat+ [ "Converts input gathered from INPUT or standard in to and from morse "+ , "code. 'dah' is represented by '-', 'dit' by '.'. Each morse character "+ , "is separated from the next by one or more ' '. Morse words are "+ , "separated by a '/'."+ ]+++defaultTerm = ( ret $ const (Left $ HelpFail Pager Nothing) <$> input+ , termInfo+ )+ where+ termInfo = def'+ { termName = "cipher"+ , version = "v1.0"+ , termDoc = doc+ }++ doc = "An implementation of the morse-code and rotational(Caesar) ciphers."++main = runChoice defaultTerm [ rotTerm, morseTerm ]
+ doc/examples/cp.hs view
@@ -0,0 +1,102 @@+import System.Console.CmdTheLine+import Control.Applicative++import System.Directory ( copyFile+ , doesFileExist+ , doesDirectoryExist+ )+import System.FilePath ( takeFileName+ , pathSeparator+ , hasTrailingPathSeparator+ )++import System.IO+import System.Exit ( exitFailure )++sep = [pathSeparator]++infixr 1 ?+-- Like C's ternary operator. 'predicate ? then-clause $ else-clause'.+-- Nice for nested, if-elseif style boolean bifurcation.+(?) True = const+(?) False = flip const++cp :: Bool -> [String] -> String -> IO ()+cp dry sources dest =+ chooseTactic =<< doesDirectoryExist dest+ where+ chooseTactic isDir = + singleFile ? singleCopy isDir $+ not isDir ? notDirErr $+ mapM_ copySourcesToDir sources++ singleFile = length sources == 1++ -- Errors+ notDirErr = do+ hPutStrLn stderr $ "cp: target '" ++ dest ++ "' is not a directory"+ exitFailure++ notFileErr str = do+ hPutStrLn stderr $ "cp: '" ++ str ++ "': no such file"+ exitFailure++ -- Tactics+ singleCopy isDir = do+ choose =<< doesFileExist filePath+ where+ choose isFile =+ isFile && isDir ? copyToDir filePath $+ isFile ? copyToFile filePath $+ notFileErr filePath++ filePath = head sources++ copySourcesToDir filePath = do+ isFile <- doesFileExist filePath+ isFile ? copyToDir filePath+ $ notFileErr filePath++ -- File copying+ copyToDir filePath = if dry+ then putStrLn $ concat [ "cp: copying ", filePath, " to ", dest' ]+ else copyFile filePath dest'+ where+ dest' = withTrailingSep ++ takeFileName filePath+ withTrailingSep = hasTrailingPathSeparator dest ? dest $ dest ++ sep++ copyToFile filePath = if dry+ then putStrLn $ concat [ "cp: copying ", filePath, " to ", dest ]+ else copyFile filePath dest+++-- An example of using the 'rev' and 'Left' variants of 'pos'.+cpTerm = cp <$> dry <*> sources <*> dest+ where+ dry = flag (optInfo [ "dry", "d" ])+ { argName = "DRY"+ , argDoc = "Perform a dry run. Print what would be copied, but do not "+ ++ "copy it."+ }++ sources = nonEmpty $ revPosLeft 0 [] posInfo+ { argName = "SOURCES"+ , argDoc = "Source file(s) to copy."+ }++ dest = required $ revPos 0 Nothing posInfo+ { argName = "DEST"+ , argDoc = "Destination of the copy. Must be a directory if there "+ ++ "is more than one $(i,SOURCE)."+ }++termInfo = def+ { termName = "cp"+ , version = "v1.0"+ , termDoc = "Copy files from SOURCES to DEST."+ , man = [ S "BUGS"+ , P "Email bug reports to <portManTwo@example.com>"+ ]+ }++main = run ( cpTerm, termInfo )
+ doc/examples/fail.hs view
@@ -0,0 +1,84 @@+import System.Console.CmdTheLine+import Control.Applicative++import Text.PrettyPrint ( fsep -- Paragraph fill a list of 'Doc'.+ , text -- Make a 'String' into a 'Doc'.+ , quotes -- Quote a 'Doc'.+ , (<+>) -- Glue two 'Doc' together with a space.+ )++import Data.List ( intersperse )+++cmdNames = [ "msg", "usage", "help", "success" ]++failMsg, failUsage, success :: [String] -> Err String+failMsg strs = Left . MsgFail . fsep $ map text strs+failUsage strs = Left . UsageFail . fsep $ map text strs+success strs = Right . concat $ intersperse " " strs++help :: String -> Err String+help name+ | any (== name) cmdNames = Left . HelpFail Pager $ Just name+ | name == "" = Left $ HelpFail Pager Nothing+ | otherwise =+ Left . UsageFail $ quotes (text name) <+> text "is not the name of a command"++noCmd :: Err String+noCmd = Left $ HelpFail Pager Nothing+++def' = def+ { stdOptSection = "COMMON OPTIONS"+ , man =+ [ S "COMMON OPTIONS"+ , P "These options are common to all commands."+ , S "MORE HELP"+ , P "Use '$(mname) $(i,COMMAND) --help' for help on a single command."+ , S "BUGS"+ , P "Email bug reports to <dogWalter@example.com>"+ ]+ }++input :: Term [String]+input = nonEmpty $ posAny [] posInfo+ { argName = "INPUT"+ , argDoc = "Some input you would like printed to the screen on failure "+ ++ "or success."+ }++cmds :: [( Term String, TermInfo )]+cmds =+ [ ( ret $ failMsg <$> input+ , def' { termName = "msg"+ , termDoc = "Print a failure message."+ }+ )++ , ( ret $ failUsage <$> input+ , def' { termName = "usage"+ , termDoc = "Print a usage message."+ }+ )++ , ( ret $ success <$> input+ , def' { termName = "success"+ , termDoc = "Print a message to the screen"+ }+ )++ , ( ret $ help <$> (pos 0 "" posInfo { argName = "TERM" })+ , def' { termName = "help"+ , termDoc = "Display help for a command."+ }+ )+ ]++noCmdTerm = ( ret $ pure noCmd+ , def' { termName = "fail"+ , termDoc = "A program demoing CmdTheLine's user "+ ++ "error functionality."+ }+ )++main = putStrLn =<< execChoice noCmdTerm cmds
+ doc/examples/grep.hs view
@@ -0,0 +1,28 @@+import System.Console.CmdTheLine+import Control.Applicative++import Data.List ( intersperse )++import System.Cmd ( system )+import System.Exit ( exitWith )++grep :: String -> [String] -> IO ()+grep pattern dests = do+ exitWith =<< system (concat . intersperse " " $ [ "grep", pattern ] ++ dests)++-- An example of using the 'pos' and 'posRight' Terms.+grepTerm = ( grep <$> pattern <*> files, termInfo )+ where+ pattern = required $ pos 0 Nothing posInfo { argName = "PATTERN" }+ files = posRight 0 [] posInfo { argName = "FILE" }+ termInfo = def+ { termName = "grep"+ , version = "2.5"+ , termDoc = "Search for PATTERN in FILE(s) or standard in."+ , man =+ [ S "BUGS"+ , P "Please send bug reports to <throatWobblerMangrove@example.com>"+ ]+ }++main = run grepTerm
+ doc/examples/hello.hs view
@@ -0,0 +1,26 @@+import System.Console.CmdTheLine+import Control.Applicative++-- Define a flag argument under the names '--silent' and '-s'+silent :: Term Bool+silent = flag $ optInfo [ "silent", "s" ]++-- Define the 0th positional argument, defaulting to the value '"world"' in+-- absence.+greeted :: Term String+greeted = pos 0 "world" posInfo { argName = "GREETED" }++hello :: Bool -> String -> IO ()+hello silent str =+ if silent+ then return ()+ else putStrLn $ "Hello, " ++ str ++ "!"++term :: Term (IO ())+term = hello <$> silent <*> greeted++termInfo :: TermInfo+termInfo = def { termName = "Hello", version = "1.0" }++main :: IO ()+main = run ( term, termInfo )
+ doc/examples/poly.hs view
@@ -0,0 +1,189 @@+-- We need 'FlexibleInstances to instance 'ArgVal' for 'Maybe Exp' and+-- '( String, Exp )'.+{-# LANGUAGE FlexibleInstances #-}++import Prelude hiding ( exp )+import System.Console.CmdTheLine hiding ( eval )+import Control.Applicative hiding ( (<|>) )++import Control.Monad ( guard )+import Data.Char ( isAlpha )+import Data.Function ( on )++import Text.Parsec+import qualified Text.PrettyPrint as PP++import qualified Data.Map as M++import System.IO++type Parser a = Parsec String () a++data Bin = Pow+ | Mul+ | Div+ | Add+ | Sub++prec :: Bin -> Int+prec b = case b of+ Pow -> 3+ Mul -> 2+ Div -> 2+ Add -> 1+ Sub -> 1++assoc :: Bin -> Assoc+assoc b = case b of+ Pow -> R+ _ -> L++toFunc :: Bin -> (Int -> Int -> Int)+toFunc b = case b of+ Pow -> (^)+ Mul -> (*)+ Div -> div+ Add -> (+)+ Sub -> (-)++data Exp = IntExp Int+ | VarExp String+ | BinExp Bin Exp Exp++instance ArgVal Exp where+ parser = fromParsec onErr exp+ where+ onErr str = PP.text "invalid expression" PP.<+> PP.quotes (PP.text str)++ pp = pretty 0++instance ArgVal (Maybe Exp) where+ parser = just+ pp = maybePP++data Assoc = L | R++type Env = M.Map String Exp++instance ArgVal ( String, Exp ) where+ parser = pair '='+ pp = pairPP '='++catParsers :: [Parser String] -> Parser String+catParsers = foldl (liftA2 (++)) (return "")++integer :: Parser Int+integer = read <$> catParsers [ option "" $ string "-", many1 digit ]++tok p = p <* spaces++parens = between op cp+ where+ op = tok $ char '('+ cp = tok $ char ')'++-- Parse a terminal expression.+term :: Parser Exp+term = parens exp <|> int <|> var+ where+ int = tok $ IntExp <$> try integer -- Try so '-<not-digits>' won't fail.+ var = tok $ VarExp <$> many1 (satisfy isAlpha)++-- Parse a binary operator.+bin :: Parser Bin+bin = choice [ pow, mul, div, add, sub ]+ where+ pow = tok $ Pow <$ char '^'+ mul = tok $ Mul <$ char '*'+ div = tok $ Div <$ char '/'+ add = tok $ Add <$ char '+'+ sub = tok $ Sub <$ char '-'++exp :: Parser Exp+exp = e 0++-- Precedence climbing expressions. See+-- <www.engr.mun.ca/~theo/Misc/exp_parsing.htm> for further information.+e :: Int -> Parser Exp+e p = do+ t <- term+ try (go t) <|> return t+ where+ go e1 = do+ b <- bin+ guard $ prec b >= p++ let q = case assoc b of+ R -> prec b+ L -> prec b + 1+ e2 <- e q++ let expr = BinExp b e1 e2+ try (go expr) <|> return expr++-- Beta reduce by replacing variables in 'e' with values in 'env'.+beta :: Env -> Exp -> Maybe Exp+beta env e = case e of+ VarExp str -> M.lookup str env+ int@(IntExp _) -> return int+ BinExp b e1 e2 -> (liftA2 (BinExp b) `on` beta env) e1 e2++eval :: Exp -> Int+eval e = case e of+ VarExp str -> error $ "saw VarExp " ++ str ++ " while evaluating"+ IntExp i -> i+ BinExp b e1 e2 -> (toFunc b `on` eval) e1 e2++pretty :: Int -> Exp -> PP.Doc+pretty p e = case e of+ VarExp str -> PP.text str+ IntExp i -> PP.int i+ BinExp b e1 e2 -> let q = prec b+ in parensOrNot q $ PP.cat [ pretty q e1, ppBin b, pretty q e2 ]+ where+ parensOrNot q = if q < p then PP.parens else id++ppBin :: Bin -> PP.Doc+ppBin b = case b of+ Pow -> PP.char '^'+ Mul -> PP.char '*'+ Div -> PP.char '/'+ Add -> PP.char '+'+ Sub -> PP.char '-'++poly :: Bool -> [( String, Exp )] -> Exp -> IO ()+poly pp assoc e = if pp+ then maybe badEnv (print . pretty 0) $ beta (M.fromList assoc) e+ else maybe badEnv (print . eval) $ beta (M.fromList assoc) e+ where+ badEnv = hPutStrLn stderr "poly: bad environment"++polyTerm = ( poly <$> pp <*> env <*> e, ti )+ where+ pp = flag (optInfo [ "pretty", "p" ])+ { argName = "PP"+ , argDoc = "If present, pretty print instead of evaluating EXP."+ }++ env = nonEmpty $ posRight 0 [] posInfo+ { argName = "ENV"+ , argDoc = "One or more assignments of the form '<name>=<exp>' to be "+ ++ "substituted in the input expression."+ }++ e = required $ pos 0 Nothing posInfo+ { argName = "EXP"+ , argDoc = "An arithmetic expression to be evaluated."+ }++ ti = def+ { termName = "exp"+ , version = "0.3"+ , termDoc = "Evaluate mathematical functions demonstrating precedence "+ ++ "climbing and instantiating 'ArgVal'."+ , man = [ S "BUGS"+ , P "Email bug reports to <fitsCarolDo@example.com>"+ ]+ }++main = run polyTerm
+ src/System/Console/CmdTheLine.hs view
@@ -0,0 +1,130 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine+ ( module System.Console.CmdTheLine.Term+ , module System.Console.CmdTheLine.Arg+ , module System.Console.CmdTheLine.ArgVal++ -- * Terms+ -- $term+ , Term()+ , TermInfo(..)+ , Default(..)++ -- * Manpages+ , ManBlock(..)++ -- * Argument information+ , ArgInfo( argDoc, argName, argSection )++ -- * User error reporting+ -- $err+ , Fail(..), HelpFormat(..), Err, ret+ )+ where++import Data.Default+import System.Console.CmdTheLine.Common+import System.Console.CmdTheLine.Term+import System.Console.CmdTheLine.Arg+import System.Console.CmdTheLine.ArgVal++import Control.Monad ( join )++{-$term++ CmdTheLine is centered around the 'Term' Applicative Functor. It allows us+ to define command line programs like the following.++> import System.Console.CmdTheLine+> import Control.Applicative+>+> -- Define a flag argument under the names '--silent' and '-s'+> silent :: Term Bool+> silent = flag $ optInfo [ "silent", "s" ]+>+> -- Define the 0th positional argument, defaulting to the value '"world"' in+> -- absence.+> greeted :: Term String+> greeted = pos 0 "world" posInfo { argName = "GREETED" }+> +> hello :: Bool -> String -> IO ()+> hello silent str =+> if silent+> then return ()+> else putStrLn $ "Hello, " ++ str ++ "!"+>+> term :: Term (IO ())+> term = foo <$> silent <*> greeted+> +> termInfo :: TermInfo+> termInfo = def { termName = "Hello", version = "1.0" }+> +> main :: IO ()+> main = run ( term, termInfo )++ CmdTheLine then generates usage, help in the form of man-pages, and manages+ all the related tedium of getting values from the command line into our+ program so we can go on thinking in regular Haskell functions.++ See the accompanying examples(including the above) provided under the+ @doc/examples@ directory of the distributed package, or go to+ <http://github.com/eli-frey/cmdtheline> and peruse them there.++-}++{-$err++ There is nothing stopping you from printing and formating your own error+ messages. However, some of the time you will want more tight integration+ with the library. That is what 'Fail', the 'Err' monad, and 'ret' are for.++ Here is a snippet of an example program that can be found at+ @doc\/examples\/fail.hs@ in the library distribution tarball, or at+ <http://github.com/eli-frey/cmdtheline>.++> import System.Console.CmdTheLine+> import Control.Applicative+>+> import Text.PrettyPrint ( fsep -- Paragraph fill a list of 'Doc'.+> , text -- Make a 'String' into a 'Doc'.+> , quotes -- Quote a 'Doc'.+> , (<+>) -- Glue two 'Doc' together with a space.+> )+>+> import Data.List ( intersperse )+>+> failMsg, failUsage, success :: [String] -> Err String+> failMsg strs = Left . MsgFail . fsep $ map text strs+> failUsage strs = Left . UsageFail . fsep $ map text strs+> success strs = Right . concat $ intersperse " " strs+>+> help :: String -> Err String+> help name+> | any (== name) cmdNames = Left . HelpFail Pager $ Just name+> | name == "" = Left $ HelpFail Pager Nothing+> | otherwise =+> Left . UsageFail $ quotes (text name) <+> text "is not the name of a command"+>+> noCmd :: Err String+> noCmd = Left $ HelpFail Pager Nothing++ We can now turn any of these functions into a @Term String@ by lifting into+ 'Term' and passing the result to 'ret' to fold the 'Err' monad into the+ library. Here is an example of what it might look like to do this with @noCmd@.++> noCmdTerm :: Term (Err String)+> noCmdTerm = pure noCmd+>+> prepedNoCmdTerm :: Term String+> prepedNoCmdTerm = ret noCmdTerm+-}++-- | 'ret' @term@ folds @term@'s 'Err' context into the library to be handled+-- internally and as seamlessly as other error messages that are built in.+ret :: Term (Err a) -> Term a+ret (Term ais yield) = Term ais yield'+ where+ yield' ei cl = join $ yield ei cl
+ src/System/Console/CmdTheLine/Arg.hs view
@@ -0,0 +1,421 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Arg+ (+ -- * Creating ArgInfos+ optInfo, posInfo++ -- * Optional arguments+ -- $opt++ -- ** Flag options+ , flag, flagAll, vFlag, vFlagAll++ -- ** Assignable options+ , opt, defaultOpt, optAll, defaultOptAll++ -- * Positional arguments+ -- $pos+ , pos, revPos, posAny, posLeft, posRight, revPosLeft, revPosRight++ -- * Constraining Terms+ , required, nonEmpty, lastOf+ ) where++import System.Console.CmdTheLine.Common+import System.Console.CmdTheLine.CmdLine ( optArg, posArg )+import System.Console.CmdTheLine.ArgVal ( ArgVal(..) )+import qualified System.Console.CmdTheLine.Err as E+import qualified System.Console.CmdTheLine.Trie as T++import Control.Applicative+import Text.PrettyPrint++import Data.List ( sort, sortBy )+import Data.Function ( on )+++argFail :: Doc -> Err a+argFail = Left . MsgFail++-- | Initialize an 'ArgInfo' by providing a list of names. The fields+-- @argName@(found at "System.Console.CmdTheLine#argName"),+-- @argDoc@(found at "System.Console.CmdTheLine#argDoc"), and+-- @argSection@(found at "System.Console.CmdTheLine#argSection")+-- can then be manipulated post-mortem, as in+--+-- > inf =(optInfo [ "i", "insufflation" ])+-- > { argName = "INSUFFERABLE"+-- > , argDoc = "in the haunted house's harrow"+-- > , argSection = "NOT FOR AUGHT"+-- > }+--+-- Names of one character in length will be prefixed by @-@ on the+-- command line, while longer names will be prefixed by @--@.+--+-- This function is meant to be used with optional arguments produced by 'flag',+-- 'opt', and friends-- not with positional arguments. Positional arguments+-- provided with names will yield a run-time error, halting any and all program+-- runs. Use 'posInfo' for positional arguments.+--+-- Likewise, if an optional argument is created with an 'ArgInfo' produced by+-- passing an empty list of names to 'optInfo', a run-time error will occur.+-- All optional arguments must have names.+optInfo :: [String] -> ArgInfo+optInfo names = ArgInfo+ { absence = Present ""+ , argDoc = ""+ , argName = ""+ , argSection = defaultSection+ , posKind = PosAny+ , optKind = FlagKind+ , optNames = map dash names+ , repeatable = False+ }+ where+ defaultSection+ | names == [] = "ARGUMENTS"+ | otherwise = "OPTIONS"++ dash "" =+ error "System.Console.CmdTheLine.Arg.optInfo recieved empty string as name"++ dash str@[_] = "-" ++ str+ dash str = "--" ++ str++-- | As 'optInfo' but for positional arguments, which by virtue of their+-- positions require no names. If a positional argument is created with a+-- name, a run-time error will occur halting any and all program runs.+--+-- If you mean to create an optional argument, use 'optInfo', and be sure to+-- give it a non-empty list of names to avoid these errors.+posInfo :: ArgInfo+posInfo = optInfo []+++{- $opt++ An optional argument is specified on the command line by a /name/ possibly+ followed by a /value/.++ The name of an option can be /short/ or /long/.++ * A /short/ name is a dash followed by a single alphanumeric character:+ @-h@, @-q@, @-I@.++ * A /long/ name is two dashes followed by alphanumeric characters and dashes:+ @--help@, @--silent@, @--ignore-case@.++ More than one name may refer to the same optional argument. For example in+ a given program the names @-q@, @--quiet@, and @--silent@ may all stand for+ the same boolean argument indicating the program to be quiet. Long names can+ be specified by any non-ambiguous prefix.++ There are three ways to assign values to an optional argument on the command+ line.++ * As the next token on the command line: @-o a.out@, @--output a.out@.++ * Glued to a short name: @-oa.out@.++ * Glued to a long name after an equal character: @--output=a.out@.++ Glued forms are necessary if the value itself starts with a dash, as is the+ case for negative numbers, @--min=-10@.++-}++--+-- Flags+--++-- | Create a command line flag that can appear at most once on the+-- command line. Yields @False@ in absence and @True@ in presence.+flag :: ArgInfo -> Term Bool+flag ai =+ if isPos ai+ then error E.errNotPos+ else Term [ai] yield+ where+ yield _ cl = case optArg cl ai of+ [] -> Right False+ [( _, _, Nothing )] -> Right True+ [( _, f, Just v )] -> argFail $ E.flagValue f v++ (( _, f, _ ) :+ ( _, g, _ ) :+ _ ) -> argFail $ E.optRepeated f g++-- | As 'flag' but may appear an infinity of times. Yields a list of @True@s+-- as long as the number of times present.+flagAll :: ArgInfo -> Term [Bool]+flagAll ai+ | isPos ai = error E.errNotPos+ | otherwise = Term [ai'] yield+ where+ ai' = ai { repeatable = True }++ yield _ cl = case optArg cl ai' of+ [] -> Right []+ xs -> mapM truth xs++ truth ( _, f, mv ) = case mv of+ Nothing -> Right True+ Just v -> argFail $ E.flagValue f v++-- | 'vFlag' @v [ ( v1, ai1 ), ... ]@ is an argument that can be present at most+-- once on the command line. It takes on the value @vn@ when appearing as+-- @ain@.+vFlag :: a -> [( a, ArgInfo )] -> Term a+vFlag v assoc = Term (map flag assoc) yield+ where+ flag ( _, ai )+ | isPos ai = error E.errNotPos+ | otherwise = ai++ yield _ cl = go Nothing assoc+ where+ go mv [] = case mv of+ Nothing -> Right v+ Just ( _, v ) -> Right v++ go mv (( v, ai ) : rest) = case optArg cl ai of+ [] -> go mv rest++ [( _, f, Nothing )] -> case mv of+ Nothing -> go (Just ( f, v )) rest+ Just ( g, _ ) -> argFail $ E.optRepeated g f++ [( _, f, Just v )] -> argFail $ E.flagValue f v++ (( _, f, _ ) :+ ( _, g, _ ) :+ _ ) -> argFail $ E.optRepeated g f++-- | 'vFlagAll' @vs assoc@ is as 'vFlag' except that it can be present an+-- infinity of times. In absence, @vs@ is yielded. When present, each+-- value is collected in the order they appear.+vFlagAll :: [a] -> [( a, ArgInfo)] -> Term [a]+vFlagAll vs assoc = Term (map flag assoc) yield+ where+ flag ( _, ai ) + | isPos ai = error E.errNotOpt+ | otherwise = ai { repeatable = True }++ yield _ cl = do+ result <- foldl addLookup (Right []) assoc+ case result of+ [] -> return vs+ _ -> return . map snd $ sortBy (compare `on` fst) result+ where+ addLookup acc ( v, ai ) = case optArg cl ai of+ [] -> acc+ xs -> (++) <$> mapM flagVal xs <*> acc+ where+ flagVal ( pos, f, mv ) = case mv of+ Nothing -> Right ( pos, v )+ Just v -> argFail $ E.flagValue f v+ ++--+-- Options+--++parseOptValue :: ArgVal a => String -> String -> Err a+parseOptValue f v = case parser v of+ Left e -> Left . UsageFail $ E.optParseValue f e+ Right v -> Right v++mkOpt :: ArgVal a => Maybe a -> a -> ArgInfo -> Term a+mkOpt vopt v ai+ | isPos ai = error E.errNotOpt+ | otherwise = Term [ai'] yield+ where+ ai' = ai { absence = Present . show $ pp v+ , optKind = case vopt of+ Nothing -> OptKind+ Just dv -> OptVal . show $ pp dv+ }+ yield _ cl = case optArg cl ai' of+ [] -> Right v+ [( _, f, Just v )] -> parseOptValue f v++ [( _, f, Nothing )] -> case vopt of+ Nothing -> argFail $ E.optValueMissing f+ Just optv -> Right optv++ (( _, f, _ ) :+ ( _, g, _ ) :+ _ ) -> argFail $ E.optRepeated g f++-- | 'opt' @v ai@ is an optional argument that yields @v@ in absence, or an+-- assigned value in presence. If the option is present, but no value is+-- assigned, it is considered a user-error and usage is printed on exit.+opt :: ArgVal a => a -> ArgInfo -> Term a+opt = mkOpt Nothing++-- | 'defaultOpt' @def v ai@ is as 'opt' except if it is present and no value is+-- assigned on the command line, @def@ is the result.+defaultOpt :: ArgVal a => a -> a -> ArgInfo -> Term a+defaultOpt x = mkOpt $ Just x++mkOptAll :: ( ArgVal a, Ord a ) => Maybe a -> [a] -> ArgInfo -> Term [a]+mkOptAll vopt vs ai+ | isPos ai = error E.errNotOpt+ | otherwise = Term [ai'] yield+ where+ ai' = ai { absence = Present ""+ , repeatable = True+ , optKind = case vopt of+ Nothing -> OptKind+ Just dv -> OptVal . show $ pp dv+ }++ yield _ cl = case optArg cl ai' of+ [] -> Right vs+ xs -> map snd . sortBy (compare `on` fst) <$> mapM parse xs++ parse ( pos, f, mv' ) = case mv' of+ Just v -> (,) pos <$> parseOptValue f v+ Nothing -> case vopt of+ Nothing -> argFail $ E.optValueMissing f+ Just dv -> Right ( pos, dv )++-- | 'optAll' @vs ai@ is like 'opt' except that it yields @vs@ in absence and+-- can appear an infinity of times. The values it is assigned on the command+-- line are accumulated in the order they appear.+optAll :: ( ArgVal a, Ord a ) => [a] -> ArgInfo -> Term [a]+optAll = mkOptAll Nothing++-- | 'defaultOptAll' @def vs ai@ is like 'optAll' except that if it is present+-- without being assigned a value, the value @def@ takes its place in the list+-- of results.+defaultOptAll :: ( ArgVal a, Ord a ) => a -> [a] -> ArgInfo -> Term [a]+defaultOptAll x = mkOptAll $ Just x+++{- $pos++ Positional arguments are tokens on the command line that are not option names+ or the values being assigned to an optional argument.++ Since positional arguments may be mistaken as the optional value of an+ optional argument or they may need to look like an optional name, anything+ that follows the special token @--@(with spaces on both sides) on the command+ line is considered to be a positional argument.++ Positional arguments are listed in documentation sections iff they are+ assigned both an @argName@ and an @argDoc@.++-}++--+-- Positional arguments.+--++parsePosValue :: ArgVal a => ArgInfo -> String -> Err a+parsePosValue ai v = case parser v of+ Left e -> Left . UsageFail $ E.posParseValue ai e+ Right v -> Right v++mkPos :: ArgVal a => Bool -> Int -> a -> ArgInfo -> Term a+mkPos rev pos v ai = Term [ai'] yield+ where+ ai' = ai { absence = Present . show $ pp v+ , posKind = PosN rev pos+ }+ yield _ cl = case posArg cl ai' of+ [] -> Right v+ [v] -> parsePosValue ai' v+ _ -> error "saw list with more than one member in pos converter"++-- | 'pos' @n v ai@ is an argument defined by the @n@th positional argument+-- on the command line. If absent the value @v@ is returned.+pos :: ArgVal a => Int -> a -> ArgInfo -> Term a+pos = mkPos False++-- | 'revPos' @n v ai@ is as 'pos' but counting from the end of the command line+-- to the front.+revPos :: ArgVal a => Int -> a -> ArgInfo -> Term a+revPos = mkPos True++posList :: ArgVal a => PosKind -> [a] -> ArgInfo -> Term [a]+posList kind vs ai+ | isOpt ai = error E.errNotPos+ | otherwise = Term [ai'] yield+ where+ ai' = ai { posKind = kind }+ yield _ cl = case posArg cl ai' of+ [] -> Right vs+ xs -> mapM (parsePosValue ai') xs++-- | 'posAny' @vs ai@ yields a list of all positional arguments or @vs@ if none+-- are present.+posAny :: ArgVal a => [a] -> ArgInfo -> Term [a]+posAny = posList PosAny++-- | 'posLeft' @n vs ai@ yield a list of all positional arguments to the left of+-- the @n@th positional argument or @vs@ if there are none.+posLeft :: ArgVal a => Int -> [a] -> ArgInfo -> Term [a]+posLeft = posList . PosL False++-- | 'posRight' @n vs ai@ is as 'posLeft' except yielding all values to the right+-- of the @n@th positional argument.+posRight :: ArgVal a => Int -> [a] -> ArgInfo -> Term [a]+posRight = posList . PosR False++-- | 'revPosLeft' @n vs ai@ is as 'posLeft' except @n@ counts from the end of the+-- command line to the front.+revPosLeft :: ArgVal a => Int -> [a] -> ArgInfo -> Term [a]+revPosLeft = posList . PosL True++-- | 'revPosRight' @n vs ai@ is as 'posRight' except @n@ counts from the end of+-- the command line to the front.+revPosRight :: ArgVal a => Int -> [a] -> ArgInfo -> Term [a]+revPosRight = posList . PosR True+++--+-- Arguments as terms.+--++absent = map (\ ai -> ai { absence = Absent })++-- | 'required' @term@ converts @term@ so that it fails in the 'Nothing' and+-- yields @a@ in the 'Just'.+--+-- This is used for required positional arguments. There is nothing+-- stopping you from using it with optional arguments, except that they+-- would no longer be optional and it would be confusing from a user's+-- perspective.+required :: Term (Maybe a) -> Term a+required (Term ais yield) = Term ais' yield'+ where+ ais' = absent ais+ yield' ei cl = case yield ei cl of+ Left e -> Left e+ Right mv -> maybe (argFail . E.argMissing $ head ais') Right mv++-- | 'nonEmpty' @term@ is a term that fails if its result is empty. Intended+-- for non-empty lists of positional arguments.+nonEmpty :: Term [a] -> Term [a]+nonEmpty (Term ais yield) = Term ais' yield'+ where+ ais' = absent ais+ yield' ei cl = case yield ei cl of+ Left e -> Left e+ Right [] -> argFail . E.argMissing $ head ais'+ Right xs -> Right xs++-- | 'lastOf' @term@ is a term that fails if its result is empty and evaluates+-- to the last element of the resulting list otherwise. Intended for lists+-- of flags or options where the last takes precedence.+lastOf :: Term [a] -> Term a+lastOf (Term ais yield) = Term ais yield'+ where+ yield' ei cl = case yield ei cl of+ Left e -> Left e+ Right [] -> argFail . E.argMissing $ head ais+ Right xs -> Right $ last xs
+ src/System/Console/CmdTheLine/ArgVal.hs view
@@ -0,0 +1,292 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+{-# LANGUAGE FlexibleInstances #-}+module System.Console.CmdTheLine.ArgVal+ (+ -- * Parsing values from the command line+ ArgVal(..), ArgParser, ArgPrinter++ -- ** Helpers for instantiating ArgVal+ , fromParsec+ , enum+ -- *** Maybe values+ , just, maybePP+ -- *** List values+ , list, listPP+ -- *** Tuple values+ , pair, pairPP+ , triple, triplePP+ , quadruple, quadruplePP+ , quintuple, quintuplePP+ ) where++import System.Console.CmdTheLine.Common ( splitOn )+import qualified System.Console.CmdTheLine.Err as E+import qualified System.Console.CmdTheLine.Trie as T++import Control.Arrow ( first, (***) )+import Data.Function ( on )+import Data.List ( sort, unfoldr )+import Data.Ratio ( Ratio )+import Data.Default++import Control.Applicative hiding ( (<|>), empty )+import Text.Parsec hiding ( char )+import Text.PrettyPrint++-- | The type of parsers of individual command line argument values.+type ArgParser a = String -> Either Doc a++-- | The type of printers of values retrieved from the command line.+type ArgPrinter a = a -> Doc++decPoint = string "."+digits = many1 digit+concatParsers = foldl (liftA2 (++)) $ return []+sign = option "" $ string "-"++pInteger :: ( Read a, Integral a ) => Parsec String () a+pFloating :: ( Read a, Floating a ) => Parsec String () a+pInteger = read <$> concatParsers [ sign, digits ]+pFloating = read <$> concatParsers [ sign, digits, decPoint, digits ]++-- | 'fromParsec' @onErr p@ makes an 'ArgParser' from @p@ using @onErr@ to+-- produce meaningful error messages. On failure, @onErr@ will receive a+-- raw string of the value found on the command line.+fromParsec :: ( String -> Doc) -> Parsec String () a -> ArgParser a+fromParsec onErr p str = either (const . Left $ onErr str) Right+ $ parse p "" str++-- | A parser of 'Maybe' values of 'ArgVal' instances. A convenient default+-- that merely lifts the 'ArgVal' instance's parsed value with 'Just'.+just :: ArgVal a => ArgParser (Maybe a)+just = either Left (Right . Just) . parser++-- | A printer of 'Maybe' values of 'ArgVal' instances. A convenient default+-- that prints nothing on the 'Nothing' and just the value on the 'Just'.+maybePP :: ArgVal a => ArgPrinter (Maybe a)+maybePP = maybe empty id . fmap pp++-- | A parser of enumerated values conveyed as an association list of+-- @( string, value )@ pairs. Unambiguous prefixes of @string@ map to+-- @value@.+enum :: [( String, a )] -> ArgParser a+enum assoc str = case T.lookup str trie of+ Right v -> Right v+ Left T.Ambiguous -> Left $ E.ambiguous "enum value" str ambs+ Left T.NotFound -> Left . E.invalidVal (text str) $ text "expected" <+> alts+ where+ ambs = sort $ T.ambiguities trie str+ alts = E.alts $ map fst assoc+ trie = T.fromList assoc++-- | @'list' sep@ creates a parser of lists of an 'ArgVal' instance separated+-- by @sep@.+list :: ArgVal a => Char -> ArgParser [a]+list sep str = either (Left . E.element "list" str)+ Right+ . sequence $ unfoldr parseElem str+ where+ parseElem [] = Nothing+ parseElem str = Just . first parser $ splitOn sep str++-- | @'listPP' sep@ creates a pretty printer of lists of an 'ArgVal' instance+-- seperated by @sep@.+listPP :: ArgVal a => Char -> ArgPrinter [a]+listPP sep = fsep . punctuate (char sep) . map pp++-- | @'pair' sep@ creates a parser of pairs of 'ArgVal' instances separated+-- by @sep@.+pair :: ( ArgVal a, ArgVal b ) => Char -> ArgParser ( a, b )+pair sep str = do+ case yStr of+ [] -> Left $ E.sepMiss sep str+ _ -> return ()+ case ( eX, eY ) of+ ( Right x, Right y ) -> Right ( x, y )+ ( Left e, _ ) -> Left $ E.element "pair" xStr e+ ( _, Left e ) -> Left $ E.element "pair" yStr e+ where+ ( eX, eY ) = parser *** parser $ xyStr+ xyStr@( xStr, yStr ) = splitOn sep str++-- | @'pairPP' sep@ creates a pretty printer of pairs of 'ArgVal' instances+-- separated by @sep@+pairPP :: ( ArgVal a, ArgVal b ) => Char -> ArgPrinter ( a, b )+pairPP sep ( x, y ) = pp x <> char sep <+> pp y++-- | @'triple' sep@ creates a parser of triples of 'ArgVal' instances separated+-- by @sep@.+triple :: ( ArgVal a, ArgVal b, ArgVal c ) => Char -> ArgParser ( a, b, c )+triple sep str = do+ [ xStr, yStr, zStr ] <-+ if length strs == 3+ then Right strs+ else Left $ E.sepMiss sep str+ case ( parser xStr, parser yStr, parser zStr ) of+ ( Right x, Right y, Right z ) -> Right ( x, y, z )+ ( Left e, _ , _ ) -> Left $ E.element "pair" xStr e+ ( _, Left e, _ ) -> Left $ E.element "pair" yStr e+ ( _, _ , Left e ) -> Left $ E.element "pair" zStr e+ where+ strs = unfoldr split str++ split [] = Nothing+ split str = Just $ splitOn sep str++-- | @'triplePP' sep@ creates a pretty printer of triples of 'ArgVal' instances+-- separated by @sep@+triplePP :: ( ArgVal a, ArgVal b, ArgVal c ) => Char -> ArgPrinter ( a, b, c )+triplePP sep ( x, y, z ) = pp x <> char sep <+> pp y <> char sep <+> pp z++-- | @'quadruple' sep@ creates a parser of quadruples of 'ArgVal' instances+-- separated by @sep@.+quadruple :: ( ArgVal a, ArgVal b, ArgVal c, ArgVal d ) =>+ Char -> ArgParser ( a, b, c, d )+quadruple sep str = do+ [ xStr, yStr, zStr, wStr ] <-+ if length strs == 4+ then Right strs+ else Left $ E.sepMiss sep str++ case ( parser xStr, parser yStr, parser zStr, parser wStr ) of+ ( Right x, Right y, Right z, Right w ) -> Right ( x, y, z, w )+ ( Left e, _ , _ , _ ) -> Left $ E.element "pair" xStr e+ ( _, Left e, _ , _ ) -> Left $ E.element "pair" yStr e+ ( _, _ , Left e , _ ) -> Left $ E.element "pair" zStr e+ ( _, _ , _ , Left e ) -> Left $ E.element "pair" wStr e+ where+ strs = unfoldr split str++ split [] = Nothing+ split str = Just $ splitOn sep str++-- | @'quadruplePP' sep@ creates a pretty printer of quadruples of 'ArgVal'+-- instances separated by @sep@+quadruplePP :: ( ArgVal a, ArgVal b, ArgVal c, ArgVal d ) =>+ Char -> ArgPrinter ( a, b, c, d )+quadruplePP sep ( x, y, z, w ) =+ pp x <> char sep <+> pp y <> char sep <+> pp z <> char sep <+> pp w++-- | @'quintuple' sep@ creates a parser of quintuples of 'ArgVal' instances+-- separated by @sep@.+quintuple :: ( ArgVal a, ArgVal b, ArgVal c, ArgVal d, ArgVal e ) =>+ Char -> ArgParser ( a, b, c, d, e )+quintuple sep str = do+ [ xStr, yStr, zStr, wStr, vStr ] <-+ if length strs == 3+ then Right strs+ else Left $ E.sepMiss sep str+ case ( parser xStr, parser yStr, parser zStr, parser wStr, parser vStr ) of+ ( Right x, Right y, Right z, Right w, Right v ) -> Right ( x, y, z, w, v )+ ( Left e, _ , _ , _ , _ ) ->+ Left $ E.element "pair" xStr e+ ( _, Left e, _ , _ , _ ) ->+ Left $ E.element "pair" yStr e+ ( _, _ , Left e , _ , _ ) ->+ Left $ E.element "pair" zStr e+ ( _, _ , _ , Left e , _ ) ->+ Left $ E.element "pair" wStr e+ ( _, _ , _ , _ , Left e ) ->+ Left $ E.element "pair" vStr e+ where+ strs = unfoldr split str++ split [] = Nothing+ split str = Just $ splitOn sep str++-- | @'quintuplePP' sep@ creates a pretty printer of quintuples of 'ArgVal'+-- instances separated by @sep@+quintuplePP :: ( ArgVal a, ArgVal b, ArgVal c, ArgVal d, ArgVal e ) =>+ Char -> ArgPrinter ( a, b, c, d, e )+quintuplePP sep ( x, y, z, w, v ) =+ pp x <> char sep <+> pp y <> char sep <+> pp z <> char sep <+>+ pp w <> char sep <+> pp v++invalidVal = E.invalidVal `on` text++-- | The class of values that can be parsed from the command line. Instances+-- must provide both 'parser' and 'pp'.+class ArgVal a where+ parser :: ArgParser a -- ^ A parser of instance values.+ pp :: ArgPrinter a -- ^ A pretty printer for instance values.++instance ArgVal Bool where+ parser = fromParsec onErr+ $ (True <$ string "true") <|> (False <$ string "false")+ where+ onErr str = E.invalidVal (text str) $ E.alts [ "true", "false" ]++ pp True = text "true"+ pp False = text "false"++instance ArgVal (Maybe Bool) where+ parser = just+ pp = maybePP++instance ArgVal [Char] where+ parser = Right+ pp = text++instance ArgVal (Maybe [Char]) where+ parser = just+ pp = maybePP++instance ArgVal Int where+ parser = fromParsec onErr pInteger+ where+ onErr str = invalidVal str "expected an integer"+ pp = int++instance ArgVal (Maybe Int) where+ parser = just+ pp = maybePP++instance ArgVal Integer where+ parser = fromParsec onErr pInteger+ where+ onErr str = invalidVal str "expected an integer"+ pp = integer++instance ArgVal (Maybe Integer) where+ parser = just+ pp = maybePP++instance ArgVal Float where+ parser = fromParsec onErr pFloating+ where+ onErr str = invalidVal str "expected a floating point number"+ pp = float++instance ArgVal (Maybe Float) where+ parser = just+ pp = maybePP++instance ArgVal Double where+ parser = fromParsec onErr pFloating+ where+ onErr str = invalidVal str "expected a floating point number"+ pp = double++instance ArgVal (Maybe Double) where+ parser = just+ pp = maybePP++instance ArgVal (Ratio Integer) where+ parser = fromParsec onErr+ $ read <$> concatParsers [ int <* spaces+ , string "%"+ , spaces >> int+ ]+ where+ int = concatParsers [ sign, digits ]+ onErr str =+ invalidVal str "expected a ratio in the form '<numerator> % <denominator>'"++ pp = rational++instance ArgVal (Maybe (Ratio Integer)) where+ parser = just+ pp = maybePP
+ src/System/Console/CmdTheLine/CmdLine.hs view
@@ -0,0 +1,162 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.CmdLine+ ( create, optArg, posArg ) where++import System.Console.CmdTheLine.Common+import System.Console.CmdTheLine.Err as E++import Control.Applicative+import Control.Arrow ( second )++import Text.PrettyPrint+import Text.Parsec as P++import qualified System.Console.CmdTheLine.Trie as T+import qualified Data.Map as M++import Data.List ( sort )+import Data.Function ( on )+++optArg :: CmdLine -> ArgInfo -> [( Int, String, Maybe String )]+optArg cl ai = case M.lookup ai cl of+ Nothing -> error "ArgInfo passed to optArg does not index CmdLine"+ Just arg -> case arg of+ Opt opt -> opt+ _ -> error "ArgInfo passed to optArg indexes to positional argument"++posArg :: CmdLine -> ArgInfo -> [String]+posArg cl ai = case M.lookup ai cl of+ Nothing -> error "ArgInfo passed to posArg does not index CmdLine"+ Just arg -> case arg of+ Pos opt -> opt+ _ -> error "ArgInfo passed to posArg indexes to positional argument"++{- Returns a trie mapping the names of optional arguments to their ArgInfo, a+ - list with all ArgInfo for positional arguments, and a CmdLine mapping each+ - ArgInfo to an empty list of Arg.+ -}+argInfoIndexes :: [ArgInfo] -> ( T.Trie ArgInfo, [ArgInfo], CmdLine )+argInfoIndexes = foldl go ( T.empty, [], M.empty )+ where+ go ( optTrie, posAis, cl ) ai+ | isPos ai = ( optTrie+ , ai : posAis+ , M.insert ai (Pos []) cl+ )+ | otherwise = ( foldl add optTrie $ optNames ai+ , posAis+ , M.insert ai (Opt []) cl+ )+ where+ add t name = T.add t name ai++parseOptArg :: String -> ( String, Maybe String )+parseOptArg str+ -- 'str' is a short name.+ | str !! 1 /= '-' =+ if length str == 2+ then ( str, Nothing ) -- No glued argument.+ else ( take 2 str, Just $ drop 2 str ) -- Glued argument.++ -- 'str' is a long name.+ | otherwise = case P.parse assignment "" str of+ Left _ -> ( str, Nothing ) -- No glued argument+ Right result -> result -- Glued argument+ where+ assignment = do+ label <- P.many1 $ P.satisfy (/= '=')+ value <- optionMaybe $ P.char '=' >> P.many1 P.anyChar+ return ( label, value )++{- Returns an updated CmdLine according to the options found in 'args'+ - with the trie index 'optTrie'. Positional arguments are returned in order.+ -}+parseArgs :: T.Trie ArgInfo -> CmdLine -> [String]+ -> Err ( CmdLine, [String] )+parseArgs optTrie cl args = second (reverse . (++ rest)) <$> go 1 cl [] args+ where+ -- Everything after '"--"' is a position argument.+ ( args', rest ) = splitOn "--" args+ go k cl posArgs args = case args of+ [] -> Right ( cl, posArgs )+ str : rest ->+ if isOpt str+ then asignOptValue str rest+ else go (k + 1) cl (str : posArgs) rest+ where+ isOpt str = length str > 1 && head str == '-'++ asignOptValue str rest = either handleErr addOpt $ T.lookup name optTrie+ where+ ( name, value ) = parseOptArg str++ addOpt ai = go (k + 1) cl' posArgs rest'+ where+ cl' = M.insert ai optArgs cl+ optArgs = Opt $ ( k, name, value' ) : optArg cl ai++ ( value', rest' )+ -- If the next string can't be assigned to this argument, don't+ -- skip it.+ | value /= Nothing || optKind ai == FlagKind ||+ rest == [] || isOpt (head rest) = ( value, rest )+ -- Else the next string is the value of this argument, consume it.+ | otherwise = ( Just $ head rest+ , tail rest+ )++ handleErr T.NotFound = Left $ UsageFail unknown+ handleErr T.Ambiguous = Left $ UsageFail ambiguous++ unknown = E.unknown "option" name+ ambiguous = E.ambiguous "option" name ambs+ where+ ambs = sort $ T.ambiguities optTrie name+++{- Returns an updated CmdLine in which each positional arg mentioned in the+ - list index 'posInfo', is given a value according to the list of positional+ - argument values 'args'.+ -}+processPosArgs :: [ArgInfo] -> ( CmdLine, [String] ) -> Err CmdLine+processPosArgs _ ( cl, [] ) = Right cl+processPosArgs posInfo ( cl, args )+ | last <= maxSpec = Right cl'+ | otherwise = Left $ UsageFail excess+ where+ last = length args - 1+ excess = E.posExcess . map text $ takeEnd (last - maxSpec) args++ ( cl', maxSpec ) = foldl go ( cl, -1 ) posInfo++ takeEnd n = reverse . take n . reverse++ go ( cl, maxSpec ) ai = ( cl', maxSpec' )+ where+ cl' = M.insert ai arg cl+ ( arg, maxSpec' ) = case posKind ai of+ PosAny -> ( Pos args, last )+ PosN rev pos -> result rev pos False indexPositions+ PosL rev pos -> result rev pos False take+ PosR rev pos -> result rev pos True (takeEnd . (last -))++ indexPositions pos args = [args !! pos]++ result rev pos maxIsLast getPositions+ | pos' < 0 || cmp pos' last = ( Pos [], maxSpec'' )+ | otherwise = ( Pos $ getPositions pos' args+ , maxSpec''+ )+ where+ pos' = if rev then last - pos else pos+ cmp = if maxIsLast then (>=) else (>)+ maxSpec'' = if maxIsLast then last else max pos' maxSpec++create :: [ArgInfo] -> [String] -> Err CmdLine+create ais args = processPosArgs posAis =<< parseArgs optTrie cl args+ where+ ( optTrie, posAis, cl ) = argInfoIndexes ais
+ src/System/Console/CmdTheLine/Common.hs view
@@ -0,0 +1,207 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Common where++import Data.Default+import Data.Function ( on )+import Text.PrettyPrint ( Doc )++import qualified Data.Map as M++data Absence = Absent+ | Present String+ deriving ( Eq )++data OptKind = FlagKind+ | OptKind+ | OptVal String+ deriving ( Eq )++data PosKind = PosAny+ | PosN Bool Int+ | PosL Bool Int+ | PosR Bool Int+ deriving ( Eq, Ord )++-- | Information about an argument. The following fields are exported for your+-- use.+--+-- #argName# +--+-- [@argName@] :: 'String' A name to be used in the documentation to+-- refer to the argument's value. Defaults to @\"\"@.+--+-- #argDoc# +--+-- [@argDoc@] :: 'String' A documentation string for the argument.+-- Defaults to @\"\"@.+--+-- #argSection# +--+-- [@argSection@] :: 'String' The section under which to place the argument's+-- documentation. Defaults to @\"OPTIONS\"@ for optional arguments and+-- @\"ARGUMENTS\"@ for positional arguments.+data ArgInfo = ArgInfo+ { absence :: Absence+ , argDoc :: String+ , argName :: String+ , argSection :: String+ , posKind :: PosKind+ , optKind :: OptKind+ , optNames :: [String]+ , repeatable :: Bool+ }++instance Eq ArgInfo where+ ai == ai'+ | isPos ai && isPos ai' = ((==) `on` posKind) ai ai'+ | isOpt ai && isOpt ai' = ((==) `on` optNames) ai ai'+ | otherwise = False++-- This Ord instance works for placing in 'Data.Map's, but not much else.+instance Ord ArgInfo where+ compare ai ai'+ | isPos ai && isPos ai' = (compare `on` posKind) ai ai'+ | isOpt ai && isOpt ai' = (compare `on` optNames) ai ai'+ | isOpt ai && isPos ai' = LT+ | otherwise = GT+++data Arg = Opt [( Int -- The position were the argument was found.+ , String -- The name by which the argument was supplied.+ , Maybe String -- If present, a value assigned to the argument.+ )]+ | Pos [String] -- A list of positional arguments++type CmdLine = M.Map ArgInfo Arg++isOpt, isPos :: ArgInfo -> Bool+isOpt ai = optNames ai /= []+isPos ai = optNames ai == []++{- |+ Any 'String' argument to a 'ManBlock' constructor may contain the+ following significant forms for a limited kind of meta-programing.++ * $(i,text): italicizes @text@.++ * $(b,text): bolds @text@.++ * $(mname): evaluates to the name of the default term if there are choices+ of commands, or the only term otherwise.++ * $(tname): evaluates to the name of the currently evaluating term.++ Additionally, text inside the content portion of an 'I' constructor may+ contain one of the following significant forms.++ * $(argName): evaluates to the name of the argument being documented.++-}+data ManBlock = S String -- ^ A section title.+ | P String -- ^ A paragraph.+ | I String String -- ^ A label-content pair. As in an argument+ -- definition and its accompanying+ -- documentation.+ | NoBlank -- ^ Suppress the normal blank line following+ -- a 'P' or an 'I'.+ deriving ( Eq )++type Title = ( String, Int, String, String, String )++type Page = ( Title, [ManBlock] )++-- | Information about a 'Term'. It is recommended that 'TermInfo's be+-- created by customizing the 'Data.Default' instance, as in+--+-- > termInfo = def+-- > { termName = "caroline-no"+-- > , termDoc = "carry a line off"+-- > }+data TermInfo = TermInfo+ {+ -- | The name of the command or program represented by the term. Defaults to+ -- @\"\"@.+ termName :: String++ -- | Documentation for the term. Defaults to @\"\"@.+ , termDoc :: String++ -- | The section under which to place the terms documentation.+ -- Defaults to @\"COMMANDS\"@.+ , termSection :: String++ -- | The section under which to place a term's argument's+ -- documentation by default. Defaults to @\"OPTIONS\"@.+ , stdOptSection :: String++ -- | A version string. Must be left blank for commands. Defaults to @\"\"@.+ , version :: String++ -- | A list of 'ManBlock's to append to the default @[ManBlock]@. Defaults+ -- to @[]@.+ , man :: [ManBlock]+ } deriving ( Eq )++instance Default TermInfo where+ def = TermInfo+ { termName = ""+ , version = ""+ , termDoc = ""+ , termSection = "COMMANDS"+ , stdOptSection = "OPTIONS"+ , man = []+ }++type Command = ( TermInfo, [ArgInfo] )++data EvalInfo = EvalInfo+ { term :: Command -- The chosen term for this run.+ , main :: Command -- The default term.+ , choices :: [Command] -- A list of command-terms.+ }++data EvalKind = Simple -- The program has no commands.+ | Main -- The default program is running.+ | Choice -- A command has been chosen.++-- | The format to print help in.+data HelpFormat = Pager | Plain | Groff++data Fail =+ -- | An arbitrary message to be printed on failure.+ MsgFail Doc++ -- | A message to be printed along with the usage on failure.+ | UsageFail Doc++ -- | A format to print the help in and an optional name of the term+ -- to print help for. If 'Nothing' is supplied, help will be printed+ -- for the currently evaluating term.+ | HelpFail HelpFormat (Maybe String)++-- | A monad for values in the context of possibly failing with a helpful+-- message.+type Err a = Either Fail a++type Yield a = EvalInfo -> CmdLine -> Err a++-- | The underlying Applicative of the library. A @Term@ represents a value+-- in the context of being computed from the command line arguments.+data Term a = Term [ArgInfo] (Yield a)++evalKind :: EvalInfo -> EvalKind+evalKind ei+ | choices ei == [] = Simple+ | fst (term ei) == fst (main ei) = Main+ | otherwise = Choice++descCompare :: Ord a => a -> a -> Ordering+descCompare = flip compare++splitOn sep xs = ( left, rest' )+ where+ rest' = if rest == [] then rest else tail rest -- Skip the 'sep'.+ ( left, rest ) = span (/= sep) xs
+ src/System/Console/CmdTheLine/Err.hs view
@@ -0,0 +1,116 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Err where++import System.Console.CmdTheLine.Common+import qualified System.Console.CmdTheLine.Help as H++import Text.PrettyPrint+import Data.List ( intersperse )++import System.IO++hsepMap :: (a -> Doc) -> [a] -> Doc+hsepMap f = hsep . map f++doc `leadBy` str = str $+$ nest 0 doc++errArgv = text "argv array must have at least one element"+errNotOpt = "Option argument without name"+errNotPos = "Positional argument with a name"+errHelp doc = text "term error, help requested for unknown command" <+> doc+++alts [] = error "alts called on empty list"+alts [x] = error "alts called on singleton list"+alts [x,y] = hsepMap text [ "either", x, "or", y ]+alts xs = text "one of:" <+> fsep (punctuate (char ',') (map text xs))++invalid kind s exp = hsep+ [ text "invalid", text kind, quotes s<>char ',', exp ]++invalidVal = invalid "value"++no kind s = sep [ text "no", quotes $ s, kind ]++notDir s = quotes (s) <+> text "is not a directory"++isDir s = quotes (s) <+> text "is a directory"++element kind str exp = fsep+ [ text "invalid element in", text kind, parens . quotes $ text str, exp ]++sepMiss sep str = invalidVal (text str) $+ hsep [ text "missing a", quotes $ char sep, text "separator" ]++unknown kind v = sep [ text "unknown", text kind, quotes $ text v ]++ambiguous kind s ambs = hsep+ [ text kind, quotes $ text s, text "ambiguous, could be", alts ambs ]++posExcess excess = text "too many arguments, don't know what to do with"+ <+> hsepMap prep excess+ where+ prep = (<> text ",") . quotes++flagValue f v = hsep+ [ text "option", quotes $ text f+ , text "is a flag, it cannot take the argument", quotes $ text v+ ]++optValueMissing f = hsep+ [ text "option", quotes $ text f, text "needs an argument" ]+optParseValue f e = sep [ text "option" <+> (quotes (text f)<>char ':'), e ]+optRepeated f f'+ | f == f' = hsep+ [ text "option", quotes $ text f, text "cannot be repeated" ]+ | otherwise = hsep+ [ text "options", quotes $ text f, text "and", quotes $ text f'+ , text "cannot be present at the same time"+ ]++posParseValue :: ArgInfo -> Doc -> Doc+posParseValue ai e+ | argName ai == "" = e+ | otherwise = case posKind ai of+ (PosN _ _) -> hsep [ name, arg, e ]+ _ -> hsep [ name<>text "...", arg, e ]+ where+ name = text $ argName ai+ arg = text "arguments:"++argMissing :: ArgInfo -> Doc+argMissing ai+ | isOpt ai = hsepMap text [ "required option", longName $ optNames ai ]+ | otherwise =+ if name == ""+ then text "a required argument is missing"+ else hsepMap text [ "required argument", name, "is missing" ]+ where+ name = argName ai++ longName (x : xs)+ | length x > 2 || xs == [] = x+ | otherwise = longName xs++print :: Handle -> EvalInfo -> Doc -> IO ()+print h ei e = hPrint h $ (text . termName . fst $ main ei) <> char ':' <+> e++prepTryHelp :: EvalInfo -> String+prepTryHelp ei =+ if execName == mainName+ then concat [ "Try '", execName, " --help' for more information." ]+ else concat [ "Try '", execName, " --help' or '"+ , mainName, " --help' for more information" ]+ where+ execName = H.invocation '-' ei+ mainName = termName . fst $ main ei++printUsage :: Handle -> EvalInfo -> Doc -> IO ()+printUsage h ei e = hPrint h $ sep+ [ text ((termName . fst $ main ei) ++ ":") <+> e+ , sep [ text "Usage:", text $ H.prepSynopsis ei ]+ , text $ prepTryHelp ei+ ]
+ src/System/Console/CmdTheLine/Help.hs view
@@ -0,0 +1,262 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Help where++import System.Console.CmdTheLine.Common+import qualified System.Console.CmdTheLine.Manpage as Man++import Control.Applicative+import Control.Arrow ( first, second )++import Data.Char ( toUpper, toLower )+import Data.List ( intersperse, sort, sortBy, partition )+import Data.Function ( on )+import Data.Maybe ( catMaybes )++import System.IO+++invocation :: Char -> EvalInfo -> String+invocation sep ei = case evalKind ei of+ Choice -> progName ++ [sep] ++ choiceName+ _ -> progName+ where+ progName = termName . fst $ main ei+ choiceName = termName . fst $ term ei++title :: EvalInfo -> Title+title ei = ( invocName, 1, "", leftFooter, centerHeader )+ where+ invocName = map toUpper $ invocation '-' ei++ leftFooter = prog ++ ver+ where+ ver = case version . fst $ main ei of+ "" -> ""+ str -> ' ' : str ++ centerHeader = prog ++ " Manual"++ prog = capitalize progName+ where+ capitalize = (:) <$> toUpper . head <*> drop 1+ progName = termName . fst $ main ei++nameSection :: EvalInfo -> [ManBlock]+nameSection ei =+ [ S "NAME"+ , P $ invocation '-' ei ++ prep (termDoc . fst $ term ei)+ ]+ where+ prep "" = ""+ prep doc = " - " ++ doc++synopsis :: EvalInfo -> String+synopsis ei = case evalKind ei of+ Main -> concat [ "$(b,", invocation ' ' ei, ") $(i,COMMAND) ..." ]+ _ -> concat [ "$(b,", invocation ' ' ei, ") [$(i,OPTION)]... ", args ]+ where+ args = concat . intersperse " " $ map snd args'+ where+ args' = sortBy revCmp . foldl formatPos [] . snd $ term ei++ formatPos acc ai+ | isOpt ai = acc+ | otherwise = ( posKind ai, v'' ) : acc+ where+ v | argName ai == "" = "$(i,ARG)"+ | otherwise = concat [ "$(i,", argName ai, ")" ]++ v' | absence ai == Absent = v+ | otherwise = concat [ "[", v, "]" ]++ v'' = v' ++ followedBy++ followedBy = case posKind ai of+ PosN _ _ -> ""+ _ -> "..."++ revCmp ( p, _ ) ( p', _ ) = case ( p', p ) of+ ( _, PosAny ) -> LT+ ( PosAny, _ ) -> GT+ ( PosL _ _, PosR _ _ ) -> LT+ ( PosR _ _, PosL _ _ ) -> GT+ ( p, p' ) -> bifurcate+ where+ bifurcate+ | not (getBool p) && not (getBool p') = case ( p, p' ) of+ ( PosL _ _, PosN _ _ ) -> if k <= k' then LT else GT+ ( PosN _ _, PosL _ _ ) -> if k >= k' then GT else LT+ ( PosN _ _, _ ) -> if k <= k' then LT else GT+ _ -> if k >= k' then GT else LT++ | getBool p && getBool p' = case ( p, p' ) of+ ( PosL _ _, PosN _ _ ) -> if k >= k' then LT else GT+ ( PosN _ _, PosL _ _ ) -> if k <= k' then GT else LT+ ( PosN _ _, _ ) -> if k >= k' then LT else GT+ _ -> if k <= k' then GT else LT+ where+ k = getPos p+ k' = getPos p'++ getPos x = case x of+ PosL _ pos -> pos+ PosR _ pos -> pos+ PosN _ pos -> pos++ getBool x = case x of+ PosL b _ -> b+ PosR b _ -> b+ PosN b _ -> b++synopsisSection :: EvalInfo -> [ManBlock]+synopsisSection ei = [ S "SYNOPSIS", P (synopsis ei) ]++makeArgLabel :: ArgInfo -> String+makeArgLabel ai+ | isPos ai = concat [ "$(i,", argName ai, ")" ]+ | otherwise = concat . intersperse ", " $ map (fmtName var) names+ where+ var | argName ai == "" = "VAL"+ | otherwise = argName ai++ names = sort $ optNames ai++ fmtName var = case optKind ai of+ FlagKind -> \ name -> concat [ "$(b,", name, ")" ]+ OptKind -> mkOptMacro+ OptVal _ -> mkOptValMacro+ where+ mkOptValMacro name = concat [ "$(b,", name, ")[", sep, "$(i,", var, ")]" ]+ where+ sep | length name > 2 = "="+ | otherwise = ""++ mkOptMacro name = concat [ "$(b,", name, ")", sep, "$(i,", var, ")" ]+ where+ sep | length name > 2 = "="+ | otherwise = " "++makeArgItems :: EvalInfo -> [( String, ManBlock )]+makeArgItems ei = map format xs+ where+ xs = sortBy revCmp . filter isArgItem . snd $ term ei++ isArgItem ai = not $ isPos ai && (argName ai == "" || argDoc ai == "")++ revCmp ai' ai = if secCmp /= EQ then secCmp else compare' ai ai'+ where+ secCmp = (compare `on` argSection) ai ai'++ compare' = case ( isOpt ai, isOpt ai' ) of+ ( True, True ) -> compare `on` key . optNames+ ( False, False ) -> compare `on` map toLower . argName+ ( True, False ) -> const $ const LT+ ( False, True ) -> const $ const GT++ key names+ | k !! 1 == '-' = drop 2 k+ | otherwise = k+ where+ k = map toLower . head $ sortBy descCompare names++ format ai = ( argSection ai, I label text )+ where+ label = makeArgLabel ai ++ argvDoc+ text = substDocName (argName ai) (argDoc ai)++ argvDoc = case ( absent, optvOpt ) of+ ( "", "" ) -> ""+ ( s, "" ) -> concat [ " (", s, ")" ]+ ( "", s ) -> concat [ " (", s, ")" ]+ ( s, s' ) -> concat [ " (", s, ", ", s', ")" ]+++ absent = case absence ai of+ Absent -> ""+ Present "" -> ""+ Present v -> "absent=" ++ v++ optvOpt = case optKind ai of+ OptVal v -> "default=" ++ v+ _ -> ""++ substDocName argName =+ Man.substitute (const id) [( "argName", ("$(i," ++ argName ++ ")") )]++makeCmdItems :: EvalInfo -> [( String, ManBlock )]+makeCmdItems ei = case evalKind ei of+ Simple -> []+ Choice -> []+ Main -> sortBy (descCompare `on` fst) . foldl addCmd [] $ choices ei+ where+ addCmd acc ( ti, _ ) = ( termSection ti, I (label ti) (termDoc ti) )+ : acc+ label ti = "$(b," ++ termName ti ++ ")"++mergeOrphans :: ( [( String, ManBlock )], [Maybe ManBlock] ) -> [ManBlock]+mergeOrphans ( orphans, marked ) = fst $ foldl go ( [], orphans ) marked+ where+ go ( acc, orphans ) (Just block) = ( block : acc, orphans )+ go ( acc, orphans ) Nothing = ( acc', [] ) + where+ acc' = case orphans of+ [] -> acc+ ( s, _ ) : _ -> let ( result, s' ) = foldl merge ( acc, s ) orphans+ in S s' : result++ merge ( acc, secName ) ( secName', item )+ | secName == secName' = ( item : acc, secName )+ | otherwise = ( item : S secName : acc, secName' )++mergeItems :: [( String, ManBlock )] -> [ManBlock]+ -> ( [( String, ManBlock )], [Maybe ManBlock] )+mergeItems items blocks = ( orphans, marked )+ where+ ( marked, _, _, orphans ) = foldl go ( [Nothing], [], False, items ) blocks+ + -- 'toInsert' is a list of manblocks that belong in the current section.+ go ( acc, toInsert, mark, items ) block = case block of+ sec@(S _) -> transition sec+ t -> ( Just t : acc, toInsert, mark, items )+ where+ transition sec@(S str) = ( acc', toInsert', mark', is' )+ where+ ( toInsert', is' ) = first (map snd) $ partition ((== str) . fst) items+ acc' = Just sec : marked+ mark' = str == "DESCRIPTION"++ marked = if mark then Nothing : acc' else acc'+ where+ acc' = map Just toInsert ++ acc++text :: EvalInfo -> [ManBlock]+text ei = mergeOrphans . mergeItems items . man . fst $ term ei+ where+ cmds = makeCmdItems ei+ args = makeArgItems ei+ cmp = descCompare `on` fst+ items = sortBy cmp $ cmds ++ args++eiSubst ei =+ [ ( "tname", termName . fst $ term ei )+ , ( "mname", termName . fst $ main ei )+ ]++page :: EvalInfo -> ( Title, [ManBlock] )+page ei = ( title ei, nameSection ei ++ synopsisSection ei ++ text ei )++print :: HelpFormat -> Handle -> EvalInfo -> IO ()+print fmt h ei = Man.print (eiSubst ei) fmt h (page ei)++prepSynopsis :: EvalInfo -> String+prepSynopsis ei = escape $ synopsis ei+ where+ escape = Man.substitute Man.plainEsc $ eiSubst ei++printVersion :: Handle -> EvalInfo -> IO ()+printVersion h ei = case version . fst $ main ei of+ "" -> error "printVersion called on EvalInfo without version"+ str -> hPutStrLn h str
+ src/System/Console/CmdTheLine/Manpage.hs view
@@ -0,0 +1,234 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Manpage where++import System.Console.CmdTheLine.Common++import Control.Applicative hiding ( (<|>), many, empty )++import System.Cmd ( system )+import System.Environment ( getEnv, getProgName )+import System.Directory ( findExecutable, getTemporaryDirectory, removeFile )+import System.Exit ( ExitCode(..) )+import System.IO.Error ( isDoesNotExistError )+import System.IO++import Control.Exception ( handle, throw, IOException, SomeException )++import Data.Maybe ( catMaybes )+import Data.Char ( isSpace )+import Data.List ( subsequences, words )++import Text.Parsec+import Text.PrettyPrint hiding ( char )++type Subst = [( String, String )] -- An association list of+ -- ( replacing, replacement ) pairs.++paragraphIndent = 7+labelIndent = 4++mkPrepTokens :: Bool -> String -> String+mkPrepTokens roff = either (error . ("printTokens: "++) . show) id+ . parse process ""+ where+ process = concat <$> many (squashSpaces <|> dash <|> otherChars) + squashSpaces = many1 Text.Parsec.space >> return " "+ dash = char '-' >> if roff then return "\\-" else return "-"+ otherChars = many1 $ satisfy (\ x -> not $ isSpace x || x == '-')++-- `subsitute assoc input` where `assoc` is an association list of+-- `( replacing, replacement )` pairs. Replaces all occurances of+-- `"$(" ++ replacing ++ ")"` in `input` with `replacement`.+--+-- TODO: return `Either String String` and produce more informative errors+-- downstream.+substitute :: (Char -> String -> String) -> Subst -> String -> String+substitute esc assoc = either (error . show) id . parse subst ""+ where+ subst = fmap concat scan++ scan = many $ try (string "\\$") <|> try replace <|> pure <$> anyChar++ replace = do+ string "$("+ replacement <- try escape <|> choice replacers <|> safeChars+ char ')'+ return replacement++ escape = do+ c <- anyChar+ char ','+ str <- try replace <|> safeChars+ return $ esc c str++ safeChars = many1 $ satisfy (/= ')')++ replacers = map mkReplacer assoc+ mkReplacer ( replacing, replacement ) = replacement <$ string replacing+++--+-- Plain text output+--++plainEsc :: Char -> String -> String+plainEsc 'g' _ = ""+plainEsc _ str = str++prepPlainBlocks :: Subst -> [ManBlock] -> String+prepPlainBlocks subst = show . go empty+ where+ escape = substitute plainEsc subst+ prepTokens = mkPrepTokens False . escape++ pFill = fsep . map text . words++ go :: Doc -> [ManBlock] -> Doc+ go acc [] = acc+ go acc (block : rest) = go acc' rest+ where+ acc' = case block of+ NoBlank -> acc+ P str -> acc $+$ nest paragraphIndent (pFill $ prepTokens str)+ $+$ text ""+ S str -> acc $+$ text (prepTokens str)+ I label str -> prepLabel label str++ prepLabel label str =+ acc $+$ nest paragraphIndent (text $ prepTokens label')+ `juxt` content+ $+$ text ""+ where+ juxt -- juxtapose+ | ll < labelIndent = (<+>)+ | otherwise = ($$)++ content+ | str == "" = empty+ | ll < labelIndent = doc (labelIndent - ll)+ | otherwise = doc (paragraphIndent + labelIndent)++ doc n = nest n (pFill $ prepTokens str)+ label' = escape label+ ll = length label'++printPlainPage :: Subst -> Handle -> Page -> IO ()+printPlainPage subst h ( _, blocks ) =+ hPutStrLn h $ prepPlainBlocks subst blocks+++--+-- Groff output+--++groffEsc :: Char -> String -> String+groffEsc c str = case c of+ 'i' -> "\\fI" ++ str ++ "\\fR"+ 'b' -> "\\fB" ++ str ++ "\\fR"+ 'p' -> ""+ _ -> str++prepGroffBlocks :: Subst -> [ManBlock] -> String+prepGroffBlocks subst blocks = prep =<< blocks+ where+ escape = substitute groffEsc subst+ prepTokens = mkPrepTokens True . escape+ prep block = case block of+ P str -> "\n.P\n" ++ prepTokens str+ S str -> "\n.SH " ++ prepTokens str+ I label str -> "\n.TP 4\n" ++ prepTokens label ++ "\n" ++ prepTokens str+ NoBlank -> "\n.sp -1"++printGroffPage :: Subst -> Handle -> Page -> IO ()+printGroffPage subst h page = hPutStr h $ unlines+ [ ".\\\" Pipe this output to groff -man -Tascii | less"+ , ".\\\""+ , concat [ ".TH \"", n, "\" ", show s+ , " \"", a1, "\" \"", a2, "\" \"", a3, "\"" ]+ , ".\\\" Disable hyphenation and ragged-right"+ , ".nh"+ , ".ad l" ++ prepGroffBlocks subst blocks+ ]+ where+ ( ( n, s, a1, a2, a3 ), blocks ) = page+++--+-- Pager output+--++printToTempFile :: (Handle -> Page -> IO ()) -> Page+ -> IO (Maybe String)+printToTempFile print v = handle handler $ do+ progName <- getProgName+ tempDir <- getTemporaryDirectory++ let fileName = tempDir ++ "/" ++ progName ++ ".out"++ h <- openFile fileName ReadWriteMode++ print h v+ hFlush h++ return $ Just fileName+ where+ handler :: SomeException -> IO (Maybe String)+ handler = const $ return Nothing++printToPager :: (HelpFormat -> Handle -> Page -> IO ())+ -> Handle -> Page -> IO ()+printToPager print h page = do+ pagers <- do+ name <- handle handler $ pure <$> getEnv "PAGER"++ return $ name ++ [ "less", "more" ]+ + found <- catMaybes <$> mapM findExecutable pagers++ case found of+ [] -> print Plain h page+ pager : _ -> do+ roffs <- catMaybes <$> mapM findExecutable [ "groff", "nroff" ]++ mCmd <- case roffs of+ [] -> (fmap . fmap) (naked pager)+ $ printToTempFile (print Plain) page++ roff : _ -> (fmap . fmap) (preped roff pager)+ $ printToTempFile (print Groff) page++ case mCmd of+ Nothing -> print Plain h page+ Just ( cmd, tmpFile ) -> do+ exitStatus <- system cmd+ case exitStatus of+ ExitSuccess -> return ()+ ExitFailure _ -> print Plain h page+ removeFile tmpFile+ where+ preped roff pager tmpFile = ( cmd, tmpFile )+ where+ cmd = concat [ roff, " -man -Tascii < ", tmpFile, " | ", pager ]++ naked pager tmpFile = ( cmd, tmpFile )+ where+ cmd = pager ++ " < " ++ tmpFile++ handler :: IOException -> IO [String]+ handler e+ | isDoesNotExistError e = return []+ | otherwise = throw e+++--+-- Interface+--++print :: Subst -> HelpFormat -> Handle -> Page -> IO ()+print subst fmt = case fmt of+ Pager -> printToPager (System.Console.CmdTheLine.Manpage.print subst)+ Plain -> printPlainPage subst+ Groff -> printGroffPage subst
+ src/System/Console/CmdTheLine/Term.hs view
@@ -0,0 +1,271 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Term+ (+ -- * Evaluating Terms+ -- ** Simple command line programs+ eval, exec, run++ -- ** Multi-command command line programs+ , evalChoice, execChoice, runChoice+ ) where++import System.Console.CmdTheLine.Common+import System.Console.CmdTheLine.CmdLine ( create )+import System.Console.CmdTheLine.Arg+import System.Console.CmdTheLine.ArgVal+import qualified System.Console.CmdTheLine.Err as E+import qualified System.Console.CmdTheLine.Help as H+import qualified System.Console.CmdTheLine.Trie as T++import Control.Applicative hiding ( (<|>), empty )+import Control.Arrow ( second )++import Data.List ( find, sort )+import Data.Maybe ( fromJust )++import System.Environment ( getArgs )+import System.Exit ( exitFailure )+import System.IO++import Text.PrettyPrint+import Text.Parsec+++--+-- EvalErr+--++data EvalFail = Help HelpFormat (Maybe String)+ | Usage Doc+ | Msg Doc+ | Version++type EvalErr = Either EvalFail++fromFail :: Fail -> EvalErr a+fromFail (MsgFail d) = Left $ Msg d+fromFail (UsageFail d) = Left $ Usage d+fromFail (HelpFail fmt mName) = Left $ Help fmt mName++fromErr :: Err a -> EvalErr a+fromErr = either fromFail return++printEvalErr :: EvalInfo -> EvalFail -> IO ()+printEvalErr ei fail = case fail of+ Usage doc -> do E.printUsage stderr ei doc+ exitFailure+ Msg doc -> do E.print stderr ei doc+ exitFailure+ Version -> H.printVersion stdout ei+ Help fmt mName -> either print (H.print fmt stdout) (eEi mName)+ where+ -- Either we are in the default term, or the commands name is in `mName`.+ eEi = maybe (Right ei { term = main ei }) process++ -- Either the command name exists, or else it does not and we're in trouble.+ process name = do+ cmd' <- case find (\ ( i, _ ) -> termName i == name) (choices ei) of+ Just x -> Right x+ Nothing -> Left $ E.errHelp (text name)+ return ei { term = cmd' } +++--+-- Terms as Applicative Functors+--++instance Functor Term where+ fmap = yield . result . result . fmap+ where+ yield f (Term ais y) = Term ais (f y)+ result = (.)++instance Applicative Term where+ pure v = Term [] (\ _ _ -> Right v)++ (Term args f) <*> (Term args' v) = Term (args ++ args') wrapped+ where+ wrapped ei cl = f ei cl <*> v ei cl+++--+-- Standard Options+--++instance ArgVal HelpFormat where+ parser = enum [ ( "pager", Pager )+ , ( "plain", Plain )+ , ( "groff", Groff )+ ]++ pp Pager = text "pager"+ pp Plain = text "plain"+ pp Groff = text "groff"++instance ArgVal (Maybe HelpFormat) where+ parser = just++ pp = maybePP++addStdOpts :: EvalInfo -> ( Yield (Maybe HelpFormat)+ , Maybe (Yield Bool)+ , EvalInfo+ )+addStdOpts ei = ( hLookup, vLookup, ei' )+ where+ ( args, vLookup ) = case version . fst $ main ei of+ "" -> ( [], Nothing )+ _ -> ( ais, Just lookup )+ where+ Term ais lookup = flag (optInfo ["version"])+ { argSection = section+ , argDoc = "Show version information."+ }++ ( args', hLookup ) = ( ais ++ args, lookup )+ where+ Term ais lookup = defaultOpt (Just Pager) Nothing (optInfo ["help"])+ { argSection = section+ , argName = "FMT"+ , argDoc = doc+ }++ section = stdOptSection . fst $ term ei+ doc = "Show this help in format $(argName) (pager, plain, or groff)."++ addArgs = second (args' ++)+ ei' = ei { term = addArgs $ term ei+ , main = addArgs $ main ei+ , choices = map addArgs $ choices ei+ }+++--+-- Evaluation of Terms+--++evalTerm :: EvalInfo -> Yield a -> [String] -> IO a+evalTerm ei yield args = either handleErr return $ do+ ( cl, mResult ) <- fromErr $ do+ cl <- create (snd $ term ei') args+ mResult <- helpArg ei' cl++ return ( cl, mResult )++ let success = fromErr $ yield ei' cl++ case ( mResult, versionArg ) of+ ( Just fmt, _ ) -> Left $ Help fmt mName+ ( Nothing, Just vArg ) -> case vArg ei' cl of+ Left e -> fromFail e+ Right True -> Left Version+ Right False -> success+ _ -> success+ where+ ( helpArg, versionArg, ei' ) = addStdOpts ei++ mName = if defName == evalName+ then Nothing+ else Just evalName++ defName = termName . fst $ main ei'+ evalName = termName . fst $ term ei'++ handleErr e = do printEvalErr ei' e+ exitFailure+++chooseTerm :: TermInfo -> [( TermInfo, a )] -> [String]+ -> Err ( TermInfo, [String] )+chooseTerm ti _ [] = Right ( ti, [] )+chooseTerm ti choices args@( arg : rest )+ | length arg > 1 && head arg == '-' = Right ( ti, args )++ | otherwise = case T.lookup arg index of+ Right choice -> Right ( choice, rest )+ Left T.NotFound -> Left . UsageFail $ E.unknown com arg+ Left T.Ambiguous -> Left . UsageFail $ E.ambiguous com arg ambs+ where+ index = foldl add T.empty choices+ add acc ( choice, _ ) = T.add acc (termName choice) choice++ com = "command"+ ambs = sort $ T.ambiguities index arg++mkCommand :: ( Term a, TermInfo ) -> Command+mkCommand ( Term ais _, ti ) = ( ti, ais )+ +-- Prep an EvalInfo suitable for catching errors raised by 'chooseTerm'.+chooseTermEi :: ( Term a, TermInfo ) -> [( Term a, TermInfo )] -> EvalInfo+chooseTermEi mainTerm choices = EvalInfo command command eiChoices+ where+ command = mkCommand mainTerm+ eiChoices = map mkCommand choices+++--+-- User-Facing Functionality+--++-- | 'eval' @args ( term, termInfo )@ allows the user to pass @args@ directly to+-- the evaluation mechanism. This is useful if some kind of pre-processing is+-- required. If you do not need to pre-process command line arguments, use one+-- of 'exec' or 'run'. On failure the program exits.+eval :: [String] -> ( Term a, TermInfo ) -> IO a+eval args termPair@( term, _ ) = evalTerm ei yield args+ where+ (Term _ yield) = term+ command = mkCommand termPair+ ei = EvalInfo command command []++-- | 'exec' @( term, termInfo )@ executes a command line program, directly+-- grabbing the command line arguments from the environment and returning the+-- result upon successful evaluation of @term@. On failure the program exits.+exec :: ( Term a, TermInfo ) -> IO a+exec term = do+ args <- getArgs+ eval args term++-- | 'run' @( term, termInfo )@ runs a @term@ containing an 'IO' action,+-- performs the action, and returns the result on success. On failure the+-- program exits.+run :: ( Term (IO a), TermInfo ) -> IO a+run term = do+ action <- exec term+ action++-- | 'evalChoice' @args mainTerm choices@ is analogous to 'eval', but for+-- programs that provide a choice of commands.+evalChoice :: [String] -> ( Term a, TermInfo ) -> [( Term a, TermInfo )] -> IO a+evalChoice args mainTerm@( term, termInfo ) choices = do+ ( chosen, args' ) <- either handleErr return . fromErr+ $ chooseTerm termInfo eiChoices args++ let (Term ais yield) = fst . fromJust . find ((== chosen) . snd)+ $ mainTerm : choices++ ei = EvalInfo ( chosen, ais ) mainEi eiChoices++ evalTerm ei yield args'+ where+ mainEi = mkCommand mainTerm+ eiChoices = map mkCommand choices++ -- Only handles errors caused by chooseTerm.+ handleErr e = do printEvalErr (chooseTermEi mainTerm choices) e+ exitFailure++-- | Analogous to 'exec', but for programs that provide a choice of commands.+execChoice :: ( Term a, TermInfo ) -> [( Term a, TermInfo )] -> IO a+execChoice main choices = do+ args <- getArgs+ evalChoice args main choices++-- | Analogous to 'run', but for programs that provide a choice of commands.+runChoice :: ( Term (IO a), TermInfo ) -> [( Term (IO a), TermInfo )] -> IO a+runChoice main choices = do+ action <- execChoice main choices+ action
+ src/System/Console/CmdTheLine/Trie.hs view
@@ -0,0 +1,101 @@+{- Copyright © 2012, Vincent Elisha Lee Frey. All rights reserved.+ - This is open source software distributed under a MIT license.+ - See the file 'LICENSE' for further information.+ -}+module System.Console.CmdTheLine.Trie where++import qualified Data.Map as M++{-+ - This implementation maps any non-ambiguous prefix of a key to its value.+ -}++type CMap = M.Map Char++data Value a = Pre a -- Value bound by a prefix key.+ | Key a -- Value bound by an entire key.+ | Amb -- No Value bound due to ambiguity in the key.+ | Nil -- Attempt to retrieve a Value from an empty Trie.+ deriving (Eq)++data Trie a = Trie+ { val :: Value a+ , succs :: CMap (Trie a)+ } deriving (Eq)++data LookupFail = Ambiguous | NotFound deriving (Show)++empty :: Trie a+empty = Trie Nil M.empty++isEmpty :: Eq a => Trie a -> Bool+isEmpty = (== empty)++add :: Trie a -> String -> a -> Trie a+add t k v = go t k (length k) 0 v (Pre v {- Allocate less. -})+ where+ go t k len i v preV =+ if i == len+ then Trie (Key v) (succs t)+ else Trie newVal newSuccs+ where+ newVal = case val t of+ Amb -> Amb+ Pre _ -> Amb+ v@(Key _) -> v+ Nil -> preV++ newSuccs = M.insert (k !! i) (go t' k len (i + 1) v preV) (succs t)+ where+ t' = maybe empty id $ M.lookup (k !! i) (succs t)++findNode :: String -> Trie a -> Maybe (Trie a)+findNode k t = go t k (length k) 0+ where+ go t k len i =+ if i == len+ then Just t+ else goNext =<< M.lookup (k !! i) (succs t)+ where+ goNext t' = go t' k len (i + 1)+++lookup :: String -> Trie a -> Either LookupFail a+lookup k t = case findNode k t of+ Nothing -> Left NotFound+ Just t' -> case val t' of+ Key v -> Right v+ Pre v -> Right v+ Amb -> Left Ambiguous+ Nil -> Left NotFound++ambiguities :: Trie a -> String -> [String]+ambiguities t pre = case findNode pre t of+ Nothing -> []+ Just t' -> case val t' of+ Amb -> go [] pre $ M.toList (succs t') : []+ _ -> []++ where+ go acc pre assocs = case assocs of+ [] -> error "saw lone empty list while searching for ambiguities"+ [[]] -> acc+ [] : rest -> go acc (init pre) rest+ _ -> descend assocs+ where+ descend ((top : bottom) : rest) = go acc' pre' assocs'+ where+ ( c, t'' ) = top++ assocs' = M.toList (succs t'') : bottom : rest++ pre' = pre ++ return c+ acc' = case val t'' of+ Key _ -> pre' : acc+ Nil -> error "saw Nil on descent"+ _ -> acc++fromList :: [( String, a )] -> Trie a+fromList assoc = foldl consume empty assoc+ where+ consume t ( k, v ) = add t k v