Rename to butcher, Change everything else
parent
4f93f79f5f
commit
f033c9e0ab
|
@ -0,0 +1,94 @@
|
|||
# butcher
|
||||
|
||||
#### Chops a command or program invocation into digestable pieces.
|
||||
|
||||
Similar to the `optparse-applicative` package, but less features,
|
||||
more flexibility and more evil.
|
||||
|
||||
The main differences are:
|
||||
|
||||
* Provides a pure interface by default
|
||||
|
||||
* Has clearly defined semantics, see the section below.
|
||||
|
||||
* Exposes an evil monadic interface, which allows for much nicer binding of
|
||||
command part results to some variable name, where in `optparse-applicative`
|
||||
you easily lose track of what field you are modifying after the 5th `<*>`.
|
||||
|
||||
Evil, because you are not allowed to use the monad's full power in this
|
||||
case, i.e. there is a constraint that is not statically enforced.
|
||||
See below.
|
||||
|
||||
* The monadic interface allows much clearer definitions of commandparses
|
||||
with (nested) subcommands. No pesky sum-types are necessary.
|
||||
|
||||
* Additionally, it is possible to wrap everything in _another_ applicative
|
||||
(chosen by the user) and execute actions whenever specific parts are
|
||||
parsed successfully. This provides a direct interface for more advanced
|
||||
features, like `--no-foo` pendants to `--foo` flags.
|
||||
|
||||
## The evil monadic interface
|
||||
|
||||
As long as you only use Applicative or (Kleisli) Arrow, you can use the
|
||||
interface freely. When you use Monad, there is one rule: Whenever you read
|
||||
any command-parts like in
|
||||
|
||||
~~~~
|
||||
f <- addFlag ...
|
||||
p <- addParam ...
|
||||
~~~~
|
||||
|
||||
you are only allowed to use bindings bound thusly in any command's
|
||||
implemenation, i.e. inside the parameter to `addCmdImpl`. You are _not_
|
||||
allowed to force/inspect/patternmatch on them before that. _good_ usage is:
|
||||
|
||||
~~~~
|
||||
addCmdImpl $ do
|
||||
print x
|
||||
print y
|
||||
~~~~
|
||||
|
||||
while _bad_ would be
|
||||
|
||||
~~~~
|
||||
f <- addFlag
|
||||
when f $ do
|
||||
p <- addParam
|
||||
-- evil: the existence of the param `p`
|
||||
-- depends on parse result for the flag `f`.
|
||||
~~~~
|
||||
|
||||
That means that checking if a combination of flags is allowed must be done
|
||||
after parsing. (But different commands and their subcommands have separate
|
||||
sets of flags.)
|
||||
|
||||
## Package intentions
|
||||
|
||||
Consider a commandline invocation like "ghc -O -i src -Main.hs -o Main". This
|
||||
package provides a way for the programmer to simultaneously define the
|
||||
semantics of your program based on its arguments and retrieve documentation
|
||||
for the user. More specifically, i had three goals in mind:
|
||||
|
||||
1. Straight-forward description of (sub)command and flag-specific behaviour
|
||||
2. Extract understandable usage/help commandline documents/texts from that
|
||||
descriptions, think of `ghc --help` or `stack init --help`.
|
||||
3. Extract necessary information to compute commandline completion results
|
||||
from any partial input.
|
||||
|
||||
## Semantics
|
||||
|
||||
Basic elements of a command are flags, parameters and subcommands. These can
|
||||
be composed in certain ways, i.e. flags can have a (or possibly multiple?)
|
||||
parameters; parameters can be grouped into sequences, and commands can have
|
||||
subcommands.
|
||||
|
||||
Commands are essentially `String -> Either ParseError out` where `out` can
|
||||
be chosen by the user. It could for example be `IO ()`.
|
||||
|
||||
To allow more flexible composition, the parts of a command have the "classic"
|
||||
parser's type: `String -> Maybe (p, String)` where `p` depends on the part.
|
||||
Parse a prefix of the input and return something and the remaining input, or
|
||||
fail with `Nothing`.
|
||||
|
||||
A command-parser contains a sequence of parts and then a number of subcommands
|
||||
and/or some implementation.
|
|
@ -0,0 +1,134 @@
|
|||
-- Initial cmdparse-applicative.cabal generated by cabal init. For further
|
||||
-- documentation, see http://haskell.org/cabal/users-guide/
|
||||
|
||||
name: butcher
|
||||
version: 0.1.0.0
|
||||
-- synopsis:
|
||||
-- description:
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Lennart Spitzner
|
||||
maintainer: lsp@informatik.uni-kiel.de
|
||||
-- copyright:
|
||||
-- category:
|
||||
build-type: Simple
|
||||
extra-source-files: ChangeLog.md
|
||||
cabal-version: >=1.10
|
||||
|
||||
flag butcher-dev
|
||||
description: dev options
|
||||
default: False
|
||||
|
||||
library
|
||||
exposed-modules: UI.Butcher.Monadic.Types
|
||||
UI.Butcher.Monadic
|
||||
UI.Butcher.Monadic.Core
|
||||
UI.Butcher.Monadic.Param
|
||||
UI.Butcher.Monadic.Flag
|
||||
UI.Butcher.Monadic.Pretty
|
||||
UI.Butcher.Monadic.IO
|
||||
-- other-modules:
|
||||
-- other-extensions:
|
||||
build-depends:
|
||||
{ base >=4.9 && <4.10
|
||||
, free
|
||||
, unsafe
|
||||
, lens
|
||||
, qualified-prelude
|
||||
, multistate
|
||||
, pretty
|
||||
, containers
|
||||
, either
|
||||
, transformers
|
||||
, mtl
|
||||
, extra
|
||||
}
|
||||
if flag(butcher-dev) {
|
||||
build-depends:
|
||||
hspec
|
||||
}
|
||||
hs-source-dirs: src
|
||||
default-language: Haskell2010
|
||||
default-extensions: {
|
||||
CPP
|
||||
|
||||
NoImplicitPrelude
|
||||
|
||||
GADTs
|
||||
|
||||
FlexibleContexts
|
||||
FlexibleInstances
|
||||
ScopedTypeVariables
|
||||
MonadComprehensions
|
||||
LambdaCase
|
||||
MultiWayIf
|
||||
KindSignatures
|
||||
ApplicativeDo
|
||||
}
|
||||
ghc-options: {
|
||||
-Wall
|
||||
-fprof-auto -fprof-cafs -fno-spec-constr
|
||||
-j
|
||||
-fno-warn-unused-imports
|
||||
-fno-warn-orphans
|
||||
}
|
||||
if flag(butcher-dev) {
|
||||
ghc-options: -O0 -Werror
|
||||
}
|
||||
|
||||
test-suite tests
|
||||
type: exitcode-stdio-1.0
|
||||
default-language: Haskell2010
|
||||
build-depends:
|
||||
{ base >=4.9 && <4.10
|
||||
, butcher
|
||||
, free
|
||||
, unsafe
|
||||
, lens
|
||||
, qualified-prelude
|
||||
, multistate
|
||||
, pretty
|
||||
, containers
|
||||
, either
|
||||
, transformers
|
||||
, mtl
|
||||
, extra
|
||||
}
|
||||
if flag(butcher-dev) {
|
||||
buildable: True
|
||||
build-depends:
|
||||
hspec
|
||||
} else {
|
||||
buildable: False
|
||||
}
|
||||
ghc-options: -Wall
|
||||
main-is: TestMain.hs
|
||||
other-modules:
|
||||
hs-source-dirs: src-tests
|
||||
default-extensions: {
|
||||
CPP
|
||||
|
||||
NoImplicitPrelude
|
||||
|
||||
GADTs
|
||||
|
||||
FlexibleContexts
|
||||
FlexibleInstances
|
||||
ScopedTypeVariables
|
||||
MonadComprehensions
|
||||
LambdaCase
|
||||
MultiWayIf
|
||||
KindSignatures
|
||||
}
|
||||
ghc-options: {
|
||||
-Wall
|
||||
-O0
|
||||
-fprof-auto -fprof-cafs -fno-spec-constr
|
||||
-j
|
||||
-fno-warn-unused-imports
|
||||
-fno-warn-orphans
|
||||
}
|
||||
if flag(butcher-dev) {
|
||||
ghc-options: -Werror
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
-- Initial cmdparse-applicative.cabal generated by cabal init. For further
|
||||
-- documentation, see http://haskell.org/cabal/users-guide/
|
||||
|
||||
name: cmdparse-applicative
|
||||
version: 0.1.0.0
|
||||
-- synopsis:
|
||||
-- description:
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Lennart Spitzner
|
||||
maintainer: lsp@informatik.uni-kiel.de
|
||||
-- copyright:
|
||||
-- category:
|
||||
build-type: Simple
|
||||
extra-source-files: ChangeLog.md
|
||||
cabal-version: >=1.10
|
||||
|
||||
flag cmdparse-applicative-dev
|
||||
description: dev options
|
||||
default: False
|
||||
|
||||
library
|
||||
exposed-modules: UI.CmdParse.Applicative.Types
|
||||
UI.CmdParse.Applicative
|
||||
UI.CmdParse.Monadic.Types
|
||||
UI.CmdParse.Monadic
|
||||
-- other-modules:
|
||||
-- other-extensions:
|
||||
build-depends:
|
||||
{ base >=4.9 && <4.10
|
||||
, free
|
||||
, unsafe
|
||||
, lens
|
||||
, qualified-prelude
|
||||
, multistate
|
||||
, pretty
|
||||
, containers
|
||||
, either
|
||||
, transformers
|
||||
, mtl
|
||||
}
|
||||
hs-source-dirs: src
|
||||
default-language: Haskell2010
|
||||
default-extensions: {
|
||||
CPP
|
||||
|
||||
NoImplicitPrelude
|
||||
|
||||
GADTs
|
||||
|
||||
FlexibleContexts
|
||||
FlexibleInstances
|
||||
ScopedTypeVariables
|
||||
MonadComprehensions
|
||||
LambdaCase
|
||||
MultiWayIf
|
||||
KindSignatures
|
||||
ApplicativeDo
|
||||
}
|
||||
ghc-options: {
|
||||
-Wall
|
||||
-fprof-auto -fprof-cafs -fno-spec-constr
|
||||
-j
|
||||
-fno-warn-unused-imports
|
||||
-fno-warn-orphans
|
||||
}
|
||||
if flag(cmdparse-applicative-dev) {
|
||||
ghc-options: -O0 -Werror
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
module Main where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
|
||||
import Test.Hspec
|
||||
|
||||
-- import NeatInterpolation
|
||||
|
||||
import UI.Butcher.Monadic
|
||||
|
||||
|
||||
|
||||
main :: IO ()
|
||||
main = hspec $ tests
|
||||
|
||||
tests :: Spec
|
||||
tests = do
|
||||
describe "checkTests" checkTests
|
||||
describe "simpleParseTest" simpleParseTest
|
||||
describe "simpleRunTest" simpleRunTest
|
||||
|
||||
|
||||
checkTests :: Spec
|
||||
checkTests = do
|
||||
before_ pending $ it "check001" $ True `shouldBe` True
|
||||
|
||||
|
||||
simpleParseTest :: Spec
|
||||
simpleParseTest = do
|
||||
it "failed parse 001" $ cmdRunParser Nothing (InputString "foo") testCmd1
|
||||
`shouldSatisfy` Data.Either.Combinators.isLeft . snd
|
||||
it "toplevel" $ (testParse testCmd1 "" >>= _cmd_out)
|
||||
`shouldSatisfy` Maybe.isNothing
|
||||
it "hasImpl 001" $ (testParse testCmd1 "abc" >>= _cmd_out)
|
||||
`shouldSatisfy` Maybe.isJust
|
||||
it "hasImpl 002" $ (testParse testCmd1 "def" >>= _cmd_out)
|
||||
`shouldSatisfy` Maybe.isJust
|
||||
|
||||
|
||||
simpleRunTest :: Spec
|
||||
simpleRunTest = do
|
||||
it "failed run" $ testRun testCmd1 "" `shouldBe` Right Nothing
|
||||
describe "no reordering" $ do
|
||||
it "cmd 1" $ testRun testCmd1 "abc" `shouldBe` Right (Just 100)
|
||||
it "cmd 2" $ testRun testCmd1 "def" `shouldBe` Right (Just 200)
|
||||
it "flag 1" $ testRun testCmd1 "abc -f" `shouldBe` Right (Just 101)
|
||||
it "flag 2" $ testRun testCmd1 "abc --flong" `shouldBe` Right (Just 101)
|
||||
it "flag 3" $ testRun testCmd1 "abc -f -f" `shouldBe` Right (Just 101)
|
||||
it "flag 4" $ testRun testCmd1 "abc -f -g" `shouldBe` Right (Just 103)
|
||||
it "flag 5" $ testRun testCmd1 "abc -f -g -f" `shouldSatisfy` Data.Either.Combinators.isLeft -- no reordering
|
||||
it "flag 6" $ testRun testCmd1 "abc -g -f" `shouldSatisfy` Data.Either.Combinators.isLeft -- no reordering
|
||||
it "flag 7" $ testRun testCmd1 "abc -g -g" `shouldBe` Right (Just 102)
|
||||
describe "with reordering" $ do
|
||||
it "cmd 1" $ testRun testCmd2 "abc" `shouldBe` Right (Just 100)
|
||||
it "cmd 2" $ testRun testCmd2 "def" `shouldBe` Right (Just 200)
|
||||
it "flag 1" $ testRun testCmd2 "abc -f" `shouldBe` Right (Just 101)
|
||||
it "flag 2" $ testRun testCmd2 "abc --flong" `shouldBe` Right (Just 101)
|
||||
it "flag 3" $ testRun testCmd2 "abc -f -f" `shouldBe` Right (Just 101)
|
||||
it "flag 4" $ testRun testCmd2 "abc -f -g" `shouldBe` Right (Just 103)
|
||||
it "flag 5" $ testRun testCmd2 "abc -f -g -f" `shouldBe` Right (Just 103)
|
||||
it "flag 6" $ testRun testCmd2 "abc -g -f" `shouldBe` Right (Just 103)
|
||||
it "flag 7" $ testRun testCmd2 "abc -g -g" `shouldBe` Right (Just 102)
|
||||
describe "with action" $ do
|
||||
it "flag 1" $ testRunA testCmd3 "abc" `shouldBe` Right 0
|
||||
it "flag 2" $ testRunA testCmd3 "abc -f" `shouldBe` Right 1
|
||||
it "flag 3" $ testRunA testCmd3 "abc -g" `shouldBe` Right 2
|
||||
it "flag 4" $ testRunA testCmd3 "abc -f -g" `shouldBe` Right 3
|
||||
it "flag 5" $ testRunA testCmd3 "abc -g -f" `shouldBe` Right 3
|
||||
|
||||
|
||||
testCmd1 :: CmdParser Identity (WriterS.Writer (Sum Int) ()) ()
|
||||
testCmd1 = do
|
||||
addCmd "abc" $ do
|
||||
f <- addSimpleBoolFlag "f" ["flong"] mempty
|
||||
g <- addSimpleBoolFlag "g" ["glong"] mempty
|
||||
addCmdImpl $ do
|
||||
when f $ WriterS.tell 1
|
||||
when g $ WriterS.tell 2
|
||||
WriterS.tell 100
|
||||
addCmd "def" $ do
|
||||
addCmdImpl $ do
|
||||
WriterS.tell 200
|
||||
|
||||
testCmd2 :: CmdParser Identity (WriterS.Writer (Sum Int) ()) ()
|
||||
testCmd2 = do
|
||||
addCmd "abc" $ do
|
||||
reorderStart
|
||||
f <- addSimpleBoolFlag "f" ["flong"] mempty
|
||||
g <- addSimpleBoolFlag "g" ["glong"] mempty
|
||||
reorderStop
|
||||
addCmdImpl $ do
|
||||
when f $ WriterS.tell 1
|
||||
when g $ WriterS.tell 2
|
||||
WriterS.tell 100
|
||||
addCmd "def" $ do
|
||||
addCmdImpl $ do
|
||||
WriterS.tell 200
|
||||
|
||||
testCmd3 :: CmdParser (StateS.State Int) () ()
|
||||
testCmd3 = do
|
||||
addCmd "abc" $ do
|
||||
reorderStart
|
||||
addSimpleFlagA "f" ["flong"] mempty (StateS.modify (+1))
|
||||
addSimpleFlagA "g" ["glong"] mempty (StateS.modify (+2))
|
||||
reorderStop
|
||||
addCmdImpl ()
|
||||
addCmd "def" $ do
|
||||
addCmdImpl ()
|
||||
|
||||
testParse :: CmdParser Identity out () -> String -> Maybe (CommandDesc out)
|
||||
testParse cmd s = either (const Nothing) Just
|
||||
$ snd
|
||||
$ cmdRunParser Nothing (InputString s) cmd
|
||||
|
||||
testRun :: CmdParser Identity (WriterS.Writer (Sum Int) ()) () -> String -> Either ParsingError (Maybe Int)
|
||||
testRun cmd s = fmap (fmap (getSum . WriterS.execWriter) . _cmd_out)
|
||||
$ snd
|
||||
$ cmdRunParser Nothing (InputString s) cmd
|
||||
|
||||
testRunA :: CmdParser (StateS.State Int) () () -> String -> Either ParsingError Int
|
||||
testRunA cmd str = (\((_, e), s) -> e $> s)
|
||||
$ flip StateS.runState (0::Int)
|
||||
$ cmdRunParserA Nothing (InputString str) cmd
|
|
@ -0,0 +1,98 @@
|
|||
module UI.Butcher.Monadic
|
||||
( module Export
|
||||
, cmds
|
||||
-- , sample
|
||||
-- , test
|
||||
, test2
|
||||
, test3
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import UI.Butcher.Monadic.Types as Export
|
||||
import UI.Butcher.Monadic.Core as Export
|
||||
import UI.Butcher.Monadic.Flag as Export
|
||||
import UI.Butcher.Monadic.Param as Export
|
||||
import UI.Butcher.Monadic.Pretty as Export
|
||||
import UI.Butcher.Monadic.IO as Export
|
||||
|
||||
-- import qualified Options.Applicative as OPA
|
||||
|
||||
|
||||
cmds :: CmdParser Identity (IO ()) ()
|
||||
cmds = do
|
||||
addCmd "echo" $ do
|
||||
addCmdHelpStr "print its parameter to output"
|
||||
str <- addReadParam "STRING" (paramHelpStr "the string to print")
|
||||
addCmdImpl $ do
|
||||
putStrLn str
|
||||
addCmd "hello" $ do
|
||||
addCmdHelpStr "greet the user"
|
||||
reorderStart
|
||||
short <- addSimpleBoolFlag "" ["short"] mempty
|
||||
name <- addReadParam "NAME" (paramHelpStr "your name, so you can be greeted properly"
|
||||
<> paramDefault "user")
|
||||
reorderStop
|
||||
addCmdImpl $ do
|
||||
if short
|
||||
then putStrLn $ "hi, " ++ name ++ "!"
|
||||
else putStrLn $ "hello, " ++ name ++ ", welcome from butcher!"
|
||||
addCmd "foo" $ do
|
||||
addCmdHelpStr "foo"
|
||||
desc <- peekCmdDesc
|
||||
addCmdImpl $ do
|
||||
putStrLn "foo"
|
||||
print $ ppHelpShallow desc
|
||||
addCmd "help" $ do
|
||||
desc <- peekCmdDesc
|
||||
addCmdImpl $ do
|
||||
print $ ppHelpShallow $ maybe undefined snd (_cmd_mParent desc)
|
||||
|
||||
data Sample = Sample
|
||||
{ _hello :: Int
|
||||
, _s1 :: String
|
||||
, _s2 :: String
|
||||
, _quiet :: Bool
|
||||
}
|
||||
deriving Show
|
||||
|
||||
-- sample :: OPA.Parser Sample
|
||||
-- sample = Sample
|
||||
-- <$> OPA.option OPA.auto
|
||||
-- ( OPA.long "hello"
|
||||
-- <> OPA.metavar "TARGET"
|
||||
-- <> OPA.help "Target for the greeting" )
|
||||
-- <*> OPA.strArgument (OPA.metavar "S1")
|
||||
-- <*> OPA.strArgument (OPA.metavar "S2")
|
||||
-- <*> OPA.switch
|
||||
-- ( OPA.long "quiet"
|
||||
-- <> OPA.help "Whether to be quiet" )
|
||||
--
|
||||
-- test :: String -> OPA.ParserResult Sample
|
||||
-- test s = OPA.execParserPure OPA.defaultPrefs (OPA.ParserInfo sample True mempty mempty mempty (-13) True) (List.words s)
|
||||
|
||||
test2 :: IO ()
|
||||
test2 = case cmdCheckParser (Just "butcher") cmds of
|
||||
Left e -> putStrLn $ "LEFT: " ++ e
|
||||
Right desc -> do
|
||||
print $ ppUsage desc
|
||||
print $ maybe undefined id $ ppUsageAt ["hello"] desc
|
||||
|
||||
test3 :: String -> IO ()
|
||||
test3 s = case cmdRunParser (Just "butcher") (InputString s) cmds of
|
||||
(desc, Left e) -> do
|
||||
print e
|
||||
print $ ppHelpShallow desc
|
||||
_cmd_mParent desc `forM_` \(_, d) -> do
|
||||
print $ ppUsage d
|
||||
(desc, Right out) -> do
|
||||
case _cmd_out out of
|
||||
Nothing -> do
|
||||
putStrLn "command is missing implementation!"
|
||||
print $ ppHelpShallow desc
|
||||
Just f -> f
|
|
@ -0,0 +1,913 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module UI.Butcher.Monadic.Core
|
||||
( addCmdSynopsis
|
||||
, addCmdHelp
|
||||
, addCmdHelpStr
|
||||
, peekCmdDesc
|
||||
, addCmdPart
|
||||
, addCmdPartA
|
||||
, addCmdPartMany
|
||||
, addCmdPartManyA
|
||||
, addCmdPartInp
|
||||
, addCmdPartInpA
|
||||
, addCmdPartManyInp
|
||||
, addCmdPartManyInpA
|
||||
, addCmd
|
||||
, addCmdImpl
|
||||
, reorderStart
|
||||
, reorderStop
|
||||
, cmdCheckParser
|
||||
, cmdRunParser
|
||||
, cmdRunParserA
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
import Text.PrettyPrint ( (<+>), ($$), ($+$) )
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
import Data.Dynamic
|
||||
|
||||
import UI.Butcher.Monadic.Types
|
||||
|
||||
|
||||
|
||||
-- general-purpose helpers
|
||||
----------------------------
|
||||
|
||||
mModify :: MonadMultiState s m => (s -> s) -> m ()
|
||||
mModify f = mGet >>= mSet . f
|
||||
|
||||
-- sadly, you need a degree in type inference to know when we can use
|
||||
-- these operators and when it must be avoided due to type ambiguities
|
||||
-- arising around s in the signatures below. That's the price of not having
|
||||
-- the functional dependency in MonadMulti*T.
|
||||
|
||||
(.=+) :: MonadMultiState s m
|
||||
=> Lens.ASetter s s a b -> b -> m ()
|
||||
l .=+ b = mModify $ l .~ b
|
||||
|
||||
(%=+) :: MonadMultiState s m
|
||||
=> Lens.ASetter s s a b -> (a -> b) -> m ()
|
||||
l %=+ f = mModify (l %~ f)
|
||||
|
||||
-- inflateStateProxy :: (Monad m, ContainsType s ss)
|
||||
-- => p s -> StateS.StateT s m a -> MultiRWSS.MultiRWST r w ss m a
|
||||
-- inflateStateProxy _ = MultiRWSS.inflateState
|
||||
|
||||
-- more on-topic stuff
|
||||
----------------------------
|
||||
|
||||
-- instance IsHelpBuilder (CmdBuilder out) where
|
||||
-- help s = liftF $ CmdBuilderHelp s ()
|
||||
--
|
||||
-- instance IsHelpBuilder (ParamBuilder p) where
|
||||
-- help s = liftF $ ParamBuilderHelp s ()
|
||||
--
|
||||
-- instance IsHelpBuilder FlagBuilder where
|
||||
-- help s = liftF $ FlagBuilderHelp s ()
|
||||
|
||||
addCmdSynopsis :: String -> CmdParser f out ()
|
||||
addCmdSynopsis s = liftF $ CmdParserSynopsis s ()
|
||||
|
||||
addCmdHelp :: PP.Doc -> CmdParser f out ()
|
||||
addCmdHelp s = liftF $ CmdParserHelp s ()
|
||||
|
||||
addCmdHelpStr :: String -> CmdParser f out ()
|
||||
addCmdHelpStr s = liftF $ CmdParserHelp (PP.text s) ()
|
||||
|
||||
peekCmdDesc :: CmdParser f out (CommandDesc out)
|
||||
peekCmdDesc = liftF $ CmdParserPeekDesc id
|
||||
|
||||
addCmdPart
|
||||
:: (Applicative f, Typeable p)
|
||||
=> PartDesc
|
||||
-> (String -> Maybe (p, String))
|
||||
-> CmdParser f out p
|
||||
addCmdPart p f = liftF $ CmdParserPart p f (\_ -> pure ()) id
|
||||
|
||||
addCmdPartA
|
||||
:: (Typeable p)
|
||||
=> PartDesc
|
||||
-> (String -> Maybe (p, String))
|
||||
-> (p -> f ())
|
||||
-> CmdParser f out p
|
||||
addCmdPartA p f a = liftF $ CmdParserPart p f a id
|
||||
|
||||
addCmdPartMany
|
||||
:: (Applicative f, Typeable p)
|
||||
=> PartDesc
|
||||
-> (String -> Maybe (p, String))
|
||||
-> CmdParser f out [p]
|
||||
addCmdPartMany p f = liftF $ CmdParserPartMany p f (\_ -> pure ()) id
|
||||
|
||||
addCmdPartManyA
|
||||
:: (Typeable p)
|
||||
=> PartDesc
|
||||
-> (String -> Maybe (p, String))
|
||||
-> (p -> f ())
|
||||
-> CmdParser f out [p]
|
||||
addCmdPartManyA p f a = liftF $ CmdParserPartMany p f a id
|
||||
|
||||
addCmdPartInp
|
||||
:: (Applicative f, Typeable p)
|
||||
=> PartDesc
|
||||
-> (Input -> Maybe (p, Input))
|
||||
-> CmdParser f out p
|
||||
addCmdPartInp p f = liftF $ CmdParserPartInp p f (\_ -> pure ()) id
|
||||
|
||||
addCmdPartInpA
|
||||
:: (Typeable p)
|
||||
=> PartDesc
|
||||
-> (Input -> Maybe (p, Input))
|
||||
-> (p -> f ())
|
||||
-> CmdParser f out p
|
||||
addCmdPartInpA p f a = liftF $ CmdParserPartInp p f a id
|
||||
|
||||
addCmdPartManyInp
|
||||
:: (Applicative f, Typeable p)
|
||||
=> PartDesc
|
||||
-> (Input -> Maybe (p, Input))
|
||||
-> CmdParser f out [p]
|
||||
addCmdPartManyInp p f = liftF $ CmdParserPartManyInp p f (\_ -> pure ()) id
|
||||
|
||||
addCmdPartManyInpA
|
||||
:: (Typeable p)
|
||||
=> PartDesc
|
||||
-> (Input -> Maybe (p, Input))
|
||||
-> (p -> f ())
|
||||
-> CmdParser f out [p]
|
||||
addCmdPartManyInpA p f a = liftF $ CmdParserPartManyInp p f a id
|
||||
|
||||
addCmd
|
||||
:: Applicative f
|
||||
=> String
|
||||
-> CmdParser f out ()
|
||||
-> CmdParser f out ()
|
||||
addCmd str sub = liftF $ CmdParserChild str sub (pure ()) ()
|
||||
|
||||
addCmdImpl :: out -> CmdParser f out ()
|
||||
addCmdImpl o = liftF $ CmdParserImpl o ()
|
||||
|
||||
reorderStart :: CmdParser f out ()
|
||||
reorderStart = liftF $ CmdParserReorderStart ()
|
||||
|
||||
reorderStop :: CmdParser f out ()
|
||||
reorderStop = liftF $ CmdParserReorderStop ()
|
||||
|
||||
-- addPartHelp :: String -> CmdPartParser ()
|
||||
-- addPartHelp s = liftF $ CmdPartParserHelp s ()
|
||||
--
|
||||
-- addPartParserBasic :: (String -> Maybe (p, String)) -> Maybe p -> CmdPartParser p
|
||||
-- addPartParserBasic f def = liftF $ CmdPartParserCore f def id
|
||||
--
|
||||
-- addPartParserOptionalBasic :: CmdPartParser p -> CmdPartParser (Maybe p)
|
||||
-- addPartParserOptionalBasic p = liftF $ CmdPartParserOptional p id
|
||||
|
||||
data PartGatherData f
|
||||
= forall p . Typeable p => PartGatherData
|
||||
{ _pgd_id :: Int
|
||||
, _pgd_desc :: PartDesc
|
||||
, _pgd_parseF :: Either (String -> Maybe (p, String))
|
||||
(Input -> Maybe (p, Input))
|
||||
, _pgd_act :: p -> f ()
|
||||
, _pgd_many :: Bool
|
||||
}
|
||||
|
||||
type PartParsedData = Map Int [Dynamic]
|
||||
|
||||
data CmdDescStack = StackBottom [PartDesc]
|
||||
| StackLayer [PartDesc] String CmdDescStack
|
||||
|
||||
descStackAdd :: PartDesc -> CmdDescStack -> CmdDescStack
|
||||
descStackAdd d = \case
|
||||
StackBottom l -> StackBottom $ d:l
|
||||
StackLayer l s u -> StackLayer (d:l) s u
|
||||
|
||||
|
||||
cmdCheckParser :: forall f out
|
||||
. Maybe String -- top-level command name
|
||||
-> CmdParser f out ()
|
||||
-> Either String (CommandDesc ())
|
||||
cmdCheckParser mTopLevel cmdParser
|
||||
= (>>= final)
|
||||
$ MultiRWSS.runMultiRWSTNil
|
||||
$ MultiRWSS.withMultiStateAS (StackBottom [])
|
||||
$ MultiRWSS.withMultiStateS emptyCommandDesc
|
||||
$ processMain cmdParser
|
||||
where
|
||||
final :: (CommandDesc out, CmdDescStack)
|
||||
-> Either String (CommandDesc ())
|
||||
final (desc, stack)
|
||||
= case stack of
|
||||
StackBottom descs -> Right
|
||||
$ descFixParentsWithTopM (mTopLevel <&> \n -> (n, emptyCommandDesc))
|
||||
$ () <$ desc
|
||||
{ _cmd_parts = reverse descs
|
||||
, _cmd_children = reverse $ _cmd_children desc
|
||||
}
|
||||
StackLayer _ _ _ -> Left "unclosed ReorderStart or GroupStart"
|
||||
processMain :: CmdParser f out ()
|
||||
-> MultiRWSS.MultiRWST '[] '[] '[CommandDesc out, CmdDescStack] (Either String) ()
|
||||
processMain = \case
|
||||
Pure x -> return x
|
||||
Free (CmdParserHelp h next) -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
mSet $ cmd { _cmd_help = Just h }
|
||||
processMain next
|
||||
Free (CmdParserSynopsis s next) -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
mSet $ cmd { _cmd_synopsis = Just $ PP.text s }
|
||||
processMain next
|
||||
Free (CmdParserPeekDesc nextF) -> do
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserPart desc _parseF _act nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd desc descStack
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserPartInp desc _parseF _act nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd desc descStack
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserPartMany desc _parseF _act nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd (PartMany desc) descStack
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserPartManyInp desc _parseF _act nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd (PartMany desc) descStack
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserChild cmdStr sub _act next) -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
subCmd <- do
|
||||
stackCur :: CmdDescStack <- mGet
|
||||
mSet (emptyCommandDesc :: CommandDesc out)
|
||||
mSet $ StackBottom []
|
||||
processMain sub
|
||||
c <- mGet
|
||||
stackBelow <- mGet
|
||||
mSet cmd
|
||||
mSet stackCur
|
||||
subParts <- case stackBelow of
|
||||
StackBottom descs -> return $ reverse descs
|
||||
StackLayer _ _ _ -> lift $ Left "unclosed ReorderStart or GroupStart"
|
||||
return c
|
||||
{ _cmd_children = reverse $ _cmd_children c
|
||||
, _cmd_parts = subParts
|
||||
}
|
||||
mSet $ cmd
|
||||
{ _cmd_children = (cmdStr, subCmd) : _cmd_children cmd
|
||||
}
|
||||
processMain next
|
||||
Free (CmdParserImpl out next) -> do
|
||||
cmd_out .=+ Just out
|
||||
processMain $ next
|
||||
Free (CmdParserGrouped groupName next) -> do
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] groupName stackCur
|
||||
processMain $ next
|
||||
Free (CmdParserGroupEnd next) -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> do
|
||||
lift $ Left $ "butcher interface error: group end without group start"
|
||||
StackLayer _descs "" _up -> do
|
||||
lift $ Left $ "GroupEnd found, but expected ReorderStop first"
|
||||
StackLayer descs groupName up -> do
|
||||
mSet $ descStackAdd (PartRedirect groupName (PartSeq (reverse descs))) up
|
||||
processMain $ next
|
||||
Free (CmdParserReorderStop next) -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> lift $ Left $ "ReorderStop without reorderStart"
|
||||
StackLayer descs "" up -> do
|
||||
mSet $ descStackAdd (PartReorder (reverse descs)) up
|
||||
StackLayer{} -> lift $ Left $ "Found ReorderStop, but need GroupEnd first"
|
||||
processMain next
|
||||
Free (CmdParserReorderStart next) -> do
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] "" stackCur
|
||||
processMain next
|
||||
|
||||
monadMisuseError :: a
|
||||
monadMisuseError = error "CmdParser definition error - used Monad powers where only Applicative/Arrow is allowed"
|
||||
|
||||
|
||||
cmdRunParser
|
||||
:: Maybe String
|
||||
-> Input
|
||||
-> CmdParser Identity out ()
|
||||
-> (CommandDesc (), Either ParsingError (CommandDesc out))
|
||||
cmdRunParser mTopLevel inputInitial cmdParser
|
||||
= runIdentity
|
||||
$ cmdRunParserA mTopLevel inputInitial cmdParser
|
||||
|
||||
|
||||
cmdRunParserA :: forall f out
|
||||
. Applicative f
|
||||
=> Maybe String
|
||||
-> Input
|
||||
-> CmdParser f out ()
|
||||
-> f ( CommandDesc ()
|
||||
, Either ParsingError (CommandDesc out)
|
||||
)
|
||||
cmdRunParserA mTopLevel inputInitial cmdParser
|
||||
= runIdentity
|
||||
$ MultiRWSS.runMultiRWSTNil
|
||||
$ (<&> captureFinal)
|
||||
$ MultiRWSS.withMultiWriterWA
|
||||
$ MultiRWSS.withMultiStateA cmdParser
|
||||
$ MultiRWSS.withMultiStateSA (StackBottom [])
|
||||
$ MultiRWSS.withMultiStateSA inputInitial
|
||||
$ MultiRWSS.withMultiStateSA initialCommandDesc
|
||||
$ processMain cmdParser
|
||||
where
|
||||
initialCommandDesc = emptyCommandDesc
|
||||
{ _cmd_mParent = mTopLevel <&> \n -> (n, emptyCommandDesc) }
|
||||
captureFinal :: ([String], (CmdDescStack, (Input, (CommandDesc out, f()))))
|
||||
-> f (CommandDesc (), Either ParsingError (CommandDesc out))
|
||||
captureFinal (errs, (descStack, (inputRest, (cmd, act)))) =
|
||||
act $> (() <$ cmd', res)
|
||||
where
|
||||
errs' = errs ++ inputErrs ++ stackErrs
|
||||
inputErrs = case inputRest of
|
||||
InputString s | all Char.isSpace s -> []
|
||||
InputString{} -> ["could not parse input/unprocessed input"]
|
||||
InputArgs [] -> []
|
||||
InputArgs{} -> ["could not parse input/unprocessed input"]
|
||||
stackErrs = case descStack of
|
||||
StackBottom{} -> []
|
||||
_ -> ["butcher interface error: unclosed group"]
|
||||
cmd' = postProcessCmd descStack cmd
|
||||
res = if null errs'
|
||||
then Right cmd'
|
||||
else Left $ ParsingError errs' inputRest
|
||||
processMain :: CmdParser f out ()
|
||||
-> MultiRWSS.MultiRWS
|
||||
'[]
|
||||
'[[String]]
|
||||
'[CommandDesc out, Input, CmdDescStack, CmdParser f out ()]
|
||||
(f ())
|
||||
processMain = \case
|
||||
Pure () -> return $ pure $ ()
|
||||
Free (CmdParserHelp h next) -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
mSet $ cmd { _cmd_help = Just h }
|
||||
processMain next
|
||||
Free (CmdParserSynopsis s next) -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
mSet $ cmd { _cmd_synopsis = Just $ PP.text s }
|
||||
processMain next
|
||||
Free (CmdParserPeekDesc nextF) -> do
|
||||
parser <- mGet
|
||||
-- partialDesc :: CommandDesc out <- mGet
|
||||
-- partialStack :: CmdDescStack <- mGet
|
||||
-- run the rest without affecting the actual stack
|
||||
-- to retrieve the complete cmddesc.
|
||||
cmdCur :: CommandDesc out <- mGet
|
||||
let (cmd :: CommandDesc out, stack)
|
||||
= runIdentity
|
||||
$ MultiRWSS.runMultiRWSTNil
|
||||
$ MultiRWSS.withMultiStateSA emptyCommandDesc
|
||||
{ _cmd_mParent = _cmd_mParent cmdCur } -- partialDesc
|
||||
$ MultiRWSS.withMultiStateS (StackBottom []) -- partialStack
|
||||
$ iterM processCmdShallow $ parser
|
||||
processMain $ nextF $ postProcessCmd stack cmd
|
||||
Free (CmdParserPart desc parseF actF nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd desc descStack
|
||||
input <- mGet
|
||||
case input of
|
||||
InputString str -> case parseF str of
|
||||
Just (x, rest) -> do
|
||||
mSet $ InputString rest
|
||||
actRest <- processMain $ nextF x
|
||||
return $ actF x *> actRest
|
||||
Nothing -> do
|
||||
mTell ["could not parse " ++ getPartSeqDescPositionName desc]
|
||||
processMain $ nextF monadMisuseError
|
||||
InputArgs (str:strr) -> case parseF str of
|
||||
Just (x, "") -> do
|
||||
mSet $ InputArgs strr
|
||||
actRest <- processMain $ nextF x
|
||||
return $ actF x *> actRest
|
||||
_ -> do
|
||||
mTell ["could not parse " ++ getPartSeqDescPositionName desc]
|
||||
processMain $ nextF monadMisuseError
|
||||
InputArgs [] -> do
|
||||
mTell ["could not parse " ++ getPartSeqDescPositionName desc]
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserPartInp desc parseF actF nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd desc descStack
|
||||
input <- mGet
|
||||
case parseF input of
|
||||
Just (x, rest) -> do
|
||||
mSet $ rest
|
||||
actRest <- processMain $ nextF x
|
||||
return $ actF x *> actRest
|
||||
Nothing -> do
|
||||
mTell ["could not parse " ++ getPartSeqDescPositionName desc]
|
||||
processMain $ nextF monadMisuseError
|
||||
Free (CmdParserPartMany desc parseF actF nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd desc descStack
|
||||
let proc = do
|
||||
dropSpaces
|
||||
input <- mGet
|
||||
case input of
|
||||
InputString str -> case parseF str of
|
||||
Just (x, r) -> do
|
||||
mSet $ InputString r
|
||||
xr <- proc
|
||||
return $ x:xr
|
||||
Nothing -> return []
|
||||
InputArgs (str:strr) -> case parseF str of
|
||||
Just (x, "") -> do
|
||||
mSet $ InputArgs strr
|
||||
xr <- proc
|
||||
return $ x:xr
|
||||
_ -> return []
|
||||
InputArgs [] -> return []
|
||||
r <- proc
|
||||
let act = traverse actF r
|
||||
(act *>) <$> processMain (nextF $ r)
|
||||
Free (CmdParserPartManyInp desc parseF actF nextF) -> do
|
||||
do
|
||||
descStack <- mGet
|
||||
mSet $ descStackAdd desc descStack
|
||||
let proc = do
|
||||
dropSpaces
|
||||
input <- mGet
|
||||
case parseF input of
|
||||
Just (x, r) -> do
|
||||
mSet $ r
|
||||
xr <- proc
|
||||
return $ x:xr
|
||||
Nothing -> return []
|
||||
r <- proc
|
||||
let act = traverse actF r
|
||||
(act *>) <$> processMain (nextF $ r)
|
||||
f@(Free (CmdParserChild cmdStr sub act next)) -> do
|
||||
dropSpaces
|
||||
input <- mGet
|
||||
let
|
||||
mRest = case input of
|
||||
InputString str | cmdStr == str ->
|
||||
Just $ InputString ""
|
||||
InputString str | (cmdStr++" ") `isPrefixOf` str ->
|
||||
Just $ InputString $ drop (length cmdStr + 1) str
|
||||
InputArgs (str:strr) | cmdStr == str ->
|
||||
Just $ InputArgs strr
|
||||
_ -> Nothing
|
||||
case mRest of
|
||||
Nothing -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
subCmd <- MultiRWSS.withMultiStateS (emptyCommandDesc :: CommandDesc out)
|
||||
$ MultiRWSS.withMultiStateA (StackBottom [])
|
||||
$ iterM processCmdShallow sub
|
||||
mSet $ cmd { _cmd_children = (cmdStr, subCmd) : _cmd_children cmd }
|
||||
processMain next
|
||||
Just rest -> do
|
||||
iterM processCmdShallow f
|
||||
cmd <- do
|
||||
c :: CommandDesc out <- mGet
|
||||
prevStack :: CmdDescStack <- mGet
|
||||
return $ postProcessCmd prevStack c
|
||||
mSet $ rest
|
||||
mSet $ (emptyCommandDesc :: CommandDesc out)
|
||||
{ _cmd_mParent = Just (cmdStr, cmd)
|
||||
}
|
||||
mSet $ sub
|
||||
mSet $ StackBottom []
|
||||
subAct <- processMain sub
|
||||
return $ act *> subAct
|
||||
Free (CmdParserImpl out next) -> do
|
||||
cmd_out .=+ Just out
|
||||
processMain $ next
|
||||
Free (CmdParserGrouped groupName next) -> do
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] groupName stackCur
|
||||
processMain $ next
|
||||
Free (CmdParserGroupEnd next) -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> do
|
||||
mTell $ ["butcher interface error: group end without group start"]
|
||||
return $ pure () -- hard abort should be fine for this case.
|
||||
StackLayer descs groupName up -> do
|
||||
mSet $ descStackAdd (PartRedirect groupName (PartSeq (reverse descs))) up
|
||||
processMain $ next
|
||||
Free (CmdParserReorderStop next) -> do
|
||||
mTell $ ["butcher interface error: reorder stop without reorder start"]
|
||||
processMain next
|
||||
Free (CmdParserReorderStart next) -> do
|
||||
reorderData <- MultiRWSS.withMultiStateA (1::Int)
|
||||
$ MultiRWSS.withMultiWriterW
|
||||
$ iterM reorderPartGather $ next
|
||||
let
|
||||
reorderMapInit :: Map Int (PartGatherData f)
|
||||
reorderMapInit = Map.fromList $ reorderData <&> \d -> (_pgd_id d, d)
|
||||
tryParsePartData :: Input -> PartGatherData f -> First (Int, Dynamic, Input, Bool, f ())
|
||||
tryParsePartData input (PartGatherData pid _ pfe act allowMany) =
|
||||
First [ (pid, toDyn r, rest, allowMany, act r)
|
||||
| (r, rest) <- case pfe of
|
||||
Left pfStr -> case input of
|
||||
InputString str -> case pfStr str of
|
||||
Just (x, r) | r/=str -> Just (x, InputString r)
|
||||
_ -> Nothing
|
||||
InputArgs (str:strr) -> case pfStr str of
|
||||
Just (x, "") -> Just (x, InputArgs strr)
|
||||
_ -> Nothing
|
||||
InputArgs [] -> Nothing
|
||||
Right pfInp -> case pfInp input of
|
||||
Just (x, r) | r/=input -> Just (x, r)
|
||||
_ -> Nothing
|
||||
]
|
||||
parseLoop = do
|
||||
input <- mGet
|
||||
m :: Map Int (PartGatherData f) <- mGet
|
||||
case getFirst $ Data.Foldable.foldMap (tryParsePartData input) m of
|
||||
-- i will be angry if foldMap ever decides to not fold
|
||||
-- in order of keys.
|
||||
Nothing -> return $ pure ()
|
||||
Just (pid, x, rest, more, act) -> do
|
||||
mSet rest
|
||||
mModify $ Map.insertWith (++) pid [x]
|
||||
when (not more) $ do
|
||||
mSet $ Map.delete pid m
|
||||
actRest <- parseLoop
|
||||
return $ act *> actRest
|
||||
(finalMap, (fr, acts)) <- MultiRWSS.withMultiStateSA (Map.empty :: PartParsedData)
|
||||
$ MultiRWSS.withMultiStateA reorderMapInit
|
||||
$ do
|
||||
acts <- parseLoop -- filling the map
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] "" stackCur
|
||||
fr <- MultiRWSS.withMultiStateA (1::Int) $ processParsedParts next
|
||||
return (fr, acts)
|
||||
-- we check that all data placed in the map has been consumed while
|
||||
-- running the parts for which we collected the parseresults.
|
||||
-- there can only be any rest if the collection of parts changed
|
||||
-- between the reorderPartGather traversal and the processParsedParts
|
||||
-- consumption.
|
||||
if Map.null finalMap
|
||||
then do
|
||||
actRest <- processMain fr
|
||||
return $ acts *> actRest
|
||||
else monadMisuseError
|
||||
|
||||
reorderPartGather
|
||||
:: ( MonadMultiState Int m
|
||||
, MonadMultiWriter [PartGatherData f] m
|
||||
, MonadMultiWriter [String] m
|
||||
)
|
||||
=> CmdParserF f out (m ())
|
||||
-> m ()
|
||||
reorderPartGather = \case
|
||||
CmdParserPart desc parseF actF nextF -> do
|
||||
pid <- mGet
|
||||
mSet $ pid + 1
|
||||
mTell [PartGatherData pid desc (Left parseF) actF False]
|
||||
nextF $ monadMisuseError
|
||||
CmdParserPartInp desc parseF actF nextF -> do
|
||||
pid <- mGet
|
||||
mSet $ pid + 1
|
||||
mTell [PartGatherData pid desc (Right parseF) actF False]
|
||||
nextF $ monadMisuseError
|
||||
CmdParserPartMany desc parseF actF nextF -> do
|
||||
pid <- mGet
|
||||
mSet $ pid + 1
|
||||
mTell [PartGatherData pid desc (Left parseF) actF True]
|
||||
nextF $ monadMisuseError
|
||||
CmdParserPartManyInp desc parseF actF nextF -> do
|
||||
pid <- mGet
|
||||
mSet $ pid + 1
|
||||
mTell [PartGatherData pid desc (Right parseF) actF True]
|
||||
nextF $ monadMisuseError
|
||||
CmdParserReorderStop _next -> do
|
||||
return ()
|
||||
CmdParserHelp{} -> restCase
|
||||
CmdParserSynopsis{} -> restCase
|
||||
CmdParserPeekDesc{} -> restCase
|
||||
CmdParserChild{} -> restCase
|
||||
CmdParserImpl{} -> restCase
|
||||
CmdParserReorderStart{} -> restCase
|
||||
CmdParserGrouped{} -> restCase
|
||||
CmdParserGroupEnd{} -> restCase
|
||||
where
|
||||
restCase = do
|
||||
mTell ["Did not find expected ReorderStop after the reordered parts"]
|
||||
return ()
|
||||
|
||||
processParsedParts
|
||||
:: forall m r w s m0 a
|
||||
. ( MonadMultiState Int m
|
||||
, MonadMultiState PartParsedData m
|
||||
, MonadMultiState (Map Int (PartGatherData f)) m
|
||||
, MonadMultiState Input m
|
||||
, MonadMultiState (CommandDesc out) m
|
||||
, MonadMultiWriter [[Char]] m
|
||||
, m ~ MultiRWSS.MultiRWST r w s m0
|
||||
, ContainsType (CmdParser f out ()) s
|
||||
, ContainsType CmdDescStack s
|
||||
, Monad m0
|
||||
)
|
||||
=> CmdParser f out a
|
||||
-> m (CmdParser f out a)
|
||||
processParsedParts = \case
|
||||
Free (CmdParserPart desc _ _ (nextF :: p -> CmdParser f out a)) -> part desc nextF
|
||||
Free (CmdParserPartInp desc _ _ (nextF :: p -> CmdParser f out a)) -> part desc nextF
|
||||
Free (CmdParserPartMany desc _ _ nextF) -> partMany desc nextF
|
||||
Free (CmdParserPartManyInp desc _ _ nextF) -> partMany desc nextF
|
||||
Free (CmdParserReorderStop next) -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> do
|
||||
mTell ["unexpected stackBottom"]
|
||||
StackLayer descs _ up -> do
|
||||
mSet $ descStackAdd (PartReorder (reverse descs)) up
|
||||
return next
|
||||
Free (CmdParserGrouped groupName next) -> do
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] groupName stackCur
|
||||
processParsedParts $ next
|
||||
Free (CmdParserGroupEnd next) -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> do
|
||||
mTell $ ["butcher interface error: group end without group start"]
|
||||
return $ next -- hard abort should be fine for this case.
|
||||
StackLayer descs groupName up -> do
|
||||
mSet $ descStackAdd (PartRedirect groupName (PartSeq (reverse descs))) up
|
||||
processParsedParts $ next
|
||||
Pure x -> return $ return $ x
|
||||
f -> do
|
||||
mTell ["Did not find expected ReorderStop after the reordered parts"]
|
||||
return f
|
||||
where
|
||||
part
|
||||
:: forall p
|
||||
. Typeable p
|
||||
=> PartDesc
|
||||
-> (p -> CmdParser f out a)
|
||||
-> m (CmdParser f out a)
|
||||
part desc nextF = do
|
||||
do
|
||||
stackCur <- mGet
|
||||
mSet $ descStackAdd desc stackCur
|
||||
pid <- mGet
|
||||
mSet $ pid + 1
|
||||
parsedMap :: PartParsedData <- mGet
|
||||
mSet $ Map.delete pid parsedMap
|
||||
partMap :: Map Int (PartGatherData f) <- mGet
|
||||
input :: Input <- mGet
|
||||
let errorResult = do
|
||||
mTell ["could not parse expected input "
|
||||
++ getPartSeqDescPositionName desc
|
||||
++ " with remaining input: "
|
||||
++ show input
|
||||
]
|
||||
failureCurrentShallowRerun
|
||||
return $ return $ monadMisuseError -- so ugly.
|
||||
-- should be correct nonetheless.
|
||||
continueOrMisuse :: Maybe p -> m (CmdParser f out a)
|
||||
continueOrMisuse = maybe monadMisuseError
|
||||
(processParsedParts . nextF)
|
||||
case Map.lookup pid parsedMap of
|
||||
Nothing -> case Map.lookup pid partMap of
|
||||
Nothing -> monadMisuseError -- it would still be in the map
|
||||
-- if it never had been successfully
|
||||
-- parsed, as indicicated by the
|
||||
-- previous parsedMap Nothing lookup.
|
||||
Just (PartGatherData _ _ pfe _ _) -> case pfe of
|
||||
Left pf -> case pf "" of
|
||||
Nothing -> errorResult
|
||||
Just (dx, _) -> continueOrMisuse $ cast dx
|
||||
Right pf -> case pf (InputArgs []) of
|
||||
Nothing -> errorResult
|
||||
Just (dx, _) -> continueOrMisuse $ cast dx
|
||||
Just [dx] -> continueOrMisuse $ fromDynamic dx
|
||||
Just _ -> monadMisuseError
|
||||
partMany
|
||||
:: Typeable p
|
||||
=> PartDesc
|
||||
-> ([p] -> CmdParser f out a)
|
||||
-> m (CmdParser f out a)
|
||||
partMany desc nextF = do
|
||||
do
|
||||
stackCur <- mGet
|
||||
mSet $ descStackAdd (PartMany desc) stackCur
|
||||
pid <- mGet
|
||||
mSet $ pid + 1
|
||||
m :: PartParsedData <- mGet
|
||||
mSet $ Map.delete pid m
|
||||
let partDyns = case Map.lookup pid m of
|
||||
Nothing -> []
|
||||
Just r -> r
|
||||
case mapM fromDynamic partDyns of
|
||||
Nothing -> monadMisuseError
|
||||
Just xs -> processParsedParts $ nextF xs
|
||||
|
||||
-- this does no error reporting at all.
|
||||
-- user needs to use check for that purpose instead.
|
||||
processCmdShallow :: ( MonadMultiState (CommandDesc out) m
|
||||
, MonadMultiState CmdDescStack m
|
||||
)
|
||||
=> CmdParserF f out (m ())
|
||||
-> m ()
|
||||
processCmdShallow = \case
|
||||
CmdParserHelp h next -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
mSet $ cmd { _cmd_help = Just h }
|
||||
next
|
||||
CmdParserSynopsis s next -> do
|
||||
cmd :: CommandDesc out <- mGet
|
||||
mSet $ cmd { _cmd_synopsis = Just $ PP.text s }
|
||||
next
|
||||
CmdParserPeekDesc nextF -> do
|
||||
mGet >>= nextF
|
||||
CmdParserPart desc _parseF _act nextF -> do
|
||||
do
|
||||
stackCur <- mGet
|
||||
mSet $ descStackAdd desc stackCur
|
||||
nextF monadMisuseError
|
||||
CmdParserPartInp desc _parseF _act nextF -> do
|
||||
do
|
||||
stackCur <- mGet
|
||||
mSet $ descStackAdd desc stackCur
|
||||
nextF monadMisuseError
|
||||
CmdParserPartMany desc _parseF _act nextF -> do
|
||||
do
|
||||
stackCur <- mGet
|
||||
mSet $ descStackAdd (PartMany desc) stackCur
|
||||
nextF monadMisuseError
|
||||
CmdParserPartManyInp desc _parseF _act nextF -> do
|
||||
do
|
||||
stackCur <- mGet
|
||||
mSet $ descStackAdd (PartMany desc) stackCur
|
||||
nextF monadMisuseError
|
||||
CmdParserChild cmdStr _sub _act next -> do
|
||||
cmd_children %=+ ((cmdStr, emptyCommandDesc :: CommandDesc out):)
|
||||
next
|
||||
CmdParserImpl out next -> do
|
||||
cmd_out .=+ Just out
|
||||
next
|
||||
CmdParserGrouped groupName next -> do
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] groupName stackCur
|
||||
next
|
||||
CmdParserGroupEnd next -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> do
|
||||
return ()
|
||||
StackLayer _descs "" _up -> do
|
||||
return ()
|
||||
StackLayer descs groupName up -> do
|
||||
mSet $ descStackAdd (PartRedirect groupName (PartSeq (reverse descs))) up
|
||||
next
|
||||
CmdParserReorderStop next -> do
|
||||
stackCur <- mGet
|
||||
case stackCur of
|
||||
StackBottom{} -> return ()
|
||||
StackLayer descs "" up -> do
|
||||
mSet $ descStackAdd (PartReorder (reverse descs)) up
|
||||
StackLayer{} -> return ()
|
||||
next
|
||||
CmdParserReorderStart next -> do
|
||||
stackCur <- mGet
|
||||
mSet $ StackLayer [] "" stackCur
|
||||
next
|
||||
|
||||
failureCurrentShallowRerun
|
||||
:: ( m ~ MultiRWSS.MultiRWST r w s m0
|
||||
, MonadMultiState (CmdParser f out ()) m
|
||||
, MonadMultiState (CommandDesc out) m
|
||||
, ContainsType CmdDescStack s
|
||||
, Monad m0
|
||||
)
|
||||
=> m ()
|
||||
failureCurrentShallowRerun = do
|
||||
parser <- mGet
|
||||
cmd :: CommandDesc out
|
||||
<- MultiRWSS.withMultiStateS emptyCommandDesc
|
||||
$ iterM processCmdShallow parser
|
||||
mSet cmd
|
||||
|
||||
postProcessCmd :: CmdDescStack -> CommandDesc out -> CommandDesc out
|
||||
postProcessCmd descStack cmd
|
||||
= descFixParents
|
||||
$ cmd { _cmd_parts = case descStack of
|
||||
StackBottom l -> reverse l
|
||||
StackLayer{} -> []
|
||||
, _cmd_children = reverse $ _cmd_children cmd
|
||||
}
|
||||
|
||||
monadMisuseError :: a
|
||||
monadMisuseError = error "CmdParser definition error - used Monad powers where only Applicative/Arrow is allowed"
|
||||
|
||||
|
||||
getPartSeqDescPositionName :: PartDesc -> String
|
||||
getPartSeqDescPositionName = \case
|
||||
PartLiteral s -> s
|
||||
PartVariable s -> s
|
||||
PartOptional ds' -> f ds'
|
||||
PartAlts alts -> f $ head alts -- this is not optimal, but probably
|
||||
-- does not matter.
|
||||
PartDefault _ d -> f d
|
||||
PartRedirect s _ -> s
|
||||
PartMany ds -> f ds
|
||||
PartWithHelp _ d -> f d
|
||||
PartSeq ds -> List.unwords $ f <$> ds
|
||||
PartReorder ds -> List.unwords $ f <$> ds
|
||||
|
||||
where
|
||||
f = getPartSeqDescPositionName
|
||||
|
||||
dropSpaces :: MonadMultiState Input m => m ()
|
||||
dropSpaces = do
|
||||
inp <- mGet
|
||||
case inp of
|
||||
InputString s -> mSet $ InputString $ dropWhile Char.isSpace s
|
||||
InputArgs{} -> return ()
|
||||
|
||||
|
||||
-- cmdActionPartial :: CommandDesc out -> Either String out
|
||||
-- cmdActionPartial = maybe (Left err) Right . _cmd_out
|
||||
-- where
|
||||
-- err = "command is missing implementation!"
|
||||
--
|
||||
-- cmdAction :: CmdParser out () -> String -> Either String out
|
||||
-- cmdAction b s = case cmdRunParser Nothing s b of
|
||||
-- (_, Right cmd) -> cmdActionPartial cmd
|
||||
-- (_, Left (ParsingError (out:_) _)) -> Left $ out
|
||||
-- _ -> error "whoops"
|
||||
--
|
||||
-- cmdActionRun :: (CommandDesc () -> ParsingError -> out)
|
||||
-- -> CmdParser out ()
|
||||
-- -> String
|
||||
-- -> out
|
||||
-- cmdActionRun f p s = case cmdRunParser Nothing s p of
|
||||
-- (cmd, Right out) -> case _cmd_out out of
|
||||
-- Just o -> o
|
||||
-- Nothing -> f cmd (ParsingError ["command is missing implementation!"] "")
|
||||
-- (cmd, Left err) -> f cmd err
|
||||
|
||||
descFixParents :: CommandDesc a -> CommandDesc a
|
||||
descFixParents = descFixParentsWithTopM Nothing
|
||||
|
||||
-- descFixParentsWithTop :: String -> CommandDesc a -> CommandDesc a
|
||||
-- descFixParentsWithTop s = descFixParentsWithTopM (Just (s, emptyCommandDesc))
|
||||
|
||||
descFixParentsWithTopM :: Maybe (String, CommandDesc a) -> CommandDesc a -> CommandDesc a
|
||||
descFixParentsWithTopM mTop topDesc =
|
||||
go $ case mTop of
|
||||
Nothing -> topDesc
|
||||
Just top -> topDesc { _cmd_mParent = Just top }
|
||||
where
|
||||
go :: CommandDesc a -> CommandDesc a
|
||||
go desc =
|
||||
let fixedDesc = desc { _cmd_children = _cmd_children desc <&> \(n, sd) ->
|
||||
(n, go $ sd { _cmd_mParent = Just (n, fixedDesc)})
|
||||
}
|
||||
in fixedDesc
|
||||
|
||||
|
||||
_tooLongText :: Int -- max length
|
||||
-> String -- alternative if actual length is bigger than max.
|
||||
-> String -- text to print, if length is fine.
|
||||
-> PP.Doc
|
||||
_tooLongText i alt s = PP.text $ Bool.bool alt s $ null $ drop i s
|
|
@ -0,0 +1,220 @@
|
|||
module UI.Butcher.Monadic.Flag
|
||||
( Flag(..)
|
||||
, addSimpleBoolFlag
|
||||
, addSimpleCountFlag
|
||||
, addSimpleFlagA
|
||||
, addFlagReadParam
|
||||
, addFlagReadParamA
|
||||
, addFlagStringParam
|
||||
, addFlagStringParamA
|
||||
, flagHelp
|
||||
, flagHelpStr
|
||||
, flagDefault
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
import Data.Dynamic
|
||||
|
||||
import UI.Butcher.Monadic.Types
|
||||
import UI.Butcher.Monadic.Core
|
||||
|
||||
import Data.List.Extra ( firstJust )
|
||||
|
||||
|
||||
|
||||
data Flag p = Flag
|
||||
{ _flag_help :: Maybe PP.Doc
|
||||
, _flag_default :: Maybe p
|
||||
}
|
||||
|
||||
instance Monoid (Flag p) where
|
||||
mempty = Flag Nothing Nothing
|
||||
Flag a1 b1 `mappend` Flag a2 b2 = Flag (a1 <|> a2) (b1 <|> b2)
|
||||
|
||||
flagHelp :: PP.Doc -> Flag p
|
||||
flagHelp h = mempty { _flag_help = Just h }
|
||||
|
||||
flagHelpStr :: String -> Flag p
|
||||
flagHelpStr s = mempty { _flag_help = Just $ PP.text s }
|
||||
|
||||
flagDefault :: p -> Flag p
|
||||
flagDefault d = mempty { _flag_default = Just d }
|
||||
|
||||
addSimpleBoolFlag
|
||||
:: Applicative f
|
||||
=> String -> [String] -> Flag Void -> CmdParser f out Bool
|
||||
addSimpleBoolFlag shorts longs flag =
|
||||
addSimpleBoolFlagAll shorts longs flag (pure ())
|
||||
|
||||
addSimpleFlagA
|
||||
:: String -> [String] -> Flag Void -> f () -> CmdParser f out ()
|
||||
addSimpleFlagA shorts longs flag act
|
||||
= void $ addSimpleBoolFlagAll shorts longs flag act
|
||||
|
||||
addSimpleBoolFlagAll
|
||||
:: String -- short flag chars, i.e. "v" for -v
|
||||
-> [String] -- list of long names, i.e. ["verbose"]
|
||||
-> Flag Void
|
||||
-> f ()
|
||||
-> CmdParser f out Bool
|
||||
addSimpleBoolFlagAll shorts longs flag a = fmap (not . null)
|
||||
$ addCmdPartManyA desc parseF (\() -> a)
|
||||
where
|
||||
allStrs = fmap (\c -> "-"++[c]) shorts
|
||||
++ fmap (\s -> "--"++s) longs
|
||||
desc :: PartDesc
|
||||
desc = (maybe id PartWithHelp $ _flag_help flag)
|
||||
$ PartAlts $ PartLiteral <$> allStrs
|
||||
parseF :: String -> Maybe ((), String)
|
||||
parseF str = ( firstJust (\s -> [ ((), drop (length s) str) | s==str ])
|
||||
allStrs)
|
||||
<|> ( firstJust (\s -> [ ((), drop (length s + 1) str)
|
||||
| (s ++ " ") `isPrefixOf` str ])
|
||||
allStrs)
|
||||
|
||||
addSimpleCountFlag :: Applicative f
|
||||
=> String -- short flag chars, i.e. "v" for -v
|
||||
-> [String] -- list of long names, i.e. ["verbose"]
|
||||
-> Flag Void
|
||||
-> CmdParser f out Int
|
||||
addSimpleCountFlag shorts longs flag = fmap length
|
||||
$ addCmdPartMany desc parseF
|
||||
where
|
||||
-- we _could_ allow this to parse repeated short flags, like "-vvv"
|
||||
-- (meaning "-v -v -v") correctly.
|
||||
allStrs = fmap (\c -> "-"++[c]) shorts
|
||||
++ fmap (\s -> "--"++s) longs
|
||||
desc :: PartDesc
|
||||
desc = (maybe id PartWithHelp $ _flag_help flag)
|
||||
$ PartAlts $ PartLiteral <$> allStrs
|
||||
parseF :: String -> Maybe ((), String)
|
||||
parseF str = ( firstJust (\s -> [ ((), drop (length s) str) | s==str ])
|
||||
allStrs)
|
||||
<|> ( firstJust (\s -> [ ((), drop (length s + 1) str)
|
||||
| (s ++ " ") `isPrefixOf` str ])
|
||||
allStrs)
|
||||
|
||||
addFlagReadParam
|
||||
:: forall f p out
|
||||
. (Applicative f, Typeable p, Text.Read.Read p, Show p)
|
||||
=> String
|
||||
-> [String]
|
||||
-> String -- param name
|
||||
-> Flag p
|
||||
-> CmdParser f out [p]
|
||||
addFlagReadParam shorts longs name flag
|
||||
= addFlagReadParamAll shorts longs name flag (\_ -> pure ())
|
||||
|
||||
addFlagReadParamA
|
||||
:: forall f p out
|
||||
. (Typeable p, Text.Read.Read p, Show p)
|
||||
=> String
|
||||
-> [String]
|
||||
-> String -- param name
|
||||
-> Flag p
|
||||
-> (p -> f ())
|
||||
-> CmdParser f out ()
|
||||
addFlagReadParamA shorts longs name flag act
|
||||
= void $ addFlagReadParamAll shorts longs name flag act
|
||||
|
||||
addFlagReadParamAll
|
||||
:: forall f p out
|
||||
. (Typeable p, Text.Read.Read p, Show p)
|
||||
=> String
|
||||
-> [String]
|
||||
-> String -- param name
|
||||
-> Flag p
|
||||
-> (p -> f ())
|
||||
-> CmdParser f out [p]
|
||||
addFlagReadParamAll shorts longs name flag act = addCmdPartManyA desc parseF act
|
||||
where
|
||||
allStrs = fmap (\c -> "-"++[c]) shorts
|
||||
++ fmap (\s -> "--"++s) longs
|
||||
desc = (maybe id PartWithHelp $ _flag_help flag)
|
||||
$ PartSeq [desc1, desc2]
|
||||
desc1 :: PartDesc
|
||||
desc1 = PartAlts $ PartLiteral <$> allStrs
|
||||
desc2 = (maybe id (PartDefault . show) $ _flag_default flag)
|
||||
$ PartVariable name
|
||||
parseF :: String -> Maybe (p, String)
|
||||
parseF str = flip firstJust allStrs
|
||||
$ \s -> [ t
|
||||
| (s ++ " ") `isPrefixOf` str
|
||||
, t <- case Text.Read.reads $ drop (length s + 1) str of
|
||||
((x, ' ':r):_) -> Just (x, dropWhile Char.isSpace r)
|
||||
((x, []):_) -> Just (x, [])
|
||||
_ -> _flag_default flag <&> \x -> (x, s)
|
||||
]
|
||||
|
||||
addFlagStringParam
|
||||
:: forall f out
|
||||
. (Applicative f)
|
||||
=> String
|
||||
-> [String]
|
||||
-> String -- param name
|
||||
-> Flag Void
|
||||
-> CmdParser f out [String]
|
||||
addFlagStringParam shorts longs name flag
|
||||
= addFlagStringParamAll shorts longs name flag (\_ -> pure ())
|
||||
|
||||
addFlagStringParamA
|
||||
:: forall f out
|
||||
. String
|
||||
-> [String]
|
||||
-> String -- param name
|
||||
-> Flag Void
|
||||
-> (String -> f ())
|
||||
-> CmdParser f out ()
|
||||
addFlagStringParamA shorts longs name flag act
|
||||
= void $ addFlagStringParamAll shorts longs name flag act
|
||||
|
||||
addFlagStringParamAll
|
||||
:: forall f out
|
||||
. String
|
||||
-> [String]
|
||||
-> String -- param name
|
||||
-> Flag Void -- we forbid the default because it has bad interaction
|
||||
-- with the eat-anything behaviour of the string parser.
|
||||
-> (String -> f ())
|
||||
-> CmdParser f out [String]
|
||||
addFlagStringParamAll shorts longs name flag act = addCmdPartManyInpA desc parseF act
|
||||
where
|
||||
allStrs = fmap (\c -> "-"++[c]) shorts
|
||||
++ fmap (\s -> "--"++s) longs
|
||||
desc = (maybe id PartWithHelp $ _flag_help flag)
|
||||
$ PartSeq [desc1, desc2]
|
||||
desc1 :: PartDesc
|
||||
desc1 = PartAlts $ PartLiteral <$> allStrs
|
||||
desc2 = (maybe id (PartDefault . show) $ _flag_default flag)
|
||||
$ PartVariable name
|
||||
parseF :: Input -> Maybe (String, Input)
|
||||
parseF (InputString str)
|
||||
= flip firstJust allStrs
|
||||
$ \s -> [ (x, InputString rest2)
|
||||
| (s ++ " ") `isPrefixOf` str
|
||||
, let rest1 = drop (length s + 1) str
|
||||
, let (x, rest2) = break (not . Char.isSpace) rest1
|
||||
]
|
||||
parseF (InputArgs (s1:s2:sr))
|
||||
= flip firstJust allStrs
|
||||
$ \s -> [ (s2, InputArgs sr)
|
||||
| s == s1
|
||||
]
|
||||
parseF (InputArgs _) = Nothing
|
|
@ -0,0 +1,73 @@
|
|||
module UI.Butcher.Monadic.IO
|
||||
( mainFromCmdParser
|
||||
, addHelpCommand
|
||||
, addButcherDebugCommand
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
import Data.Dynamic
|
||||
|
||||
import UI.Butcher.Monadic.Types
|
||||
import UI.Butcher.Monadic.Core
|
||||
import UI.Butcher.Monadic.Pretty
|
||||
|
||||
|
||||
|
||||
mainFromCmdParser :: CmdParser Identity (IO ()) () -> IO ()
|
||||
mainFromCmdParser cmd = do
|
||||
progName <- System.Environment.getProgName
|
||||
case cmdCheckParser (Just progName) cmd of
|
||||
Left e -> do
|
||||
putStrLn $ progName ++ ": internal error: failed sanity check for butcher main command parser!"
|
||||
putStrLn $ "(" ++ e ++ ")"
|
||||
putStrLn $ "aborting."
|
||||
Right _ -> do
|
||||
args <- System.Environment.getArgs
|
||||
case cmdRunParser (Just progName) (InputArgs args) cmd of
|
||||
(desc, Left (ParsingError mess remaining)) -> do
|
||||
putStrLn $ progName ++ ": error parsing arguments: " ++ head mess
|
||||
putStrLn $ case remaining of
|
||||
InputString "" -> "at the end of input."
|
||||
InputString str -> case show str of
|
||||
s | length s < 42 -> "at: " ++ s ++ "."
|
||||
s -> "at: " ++ take 40 s ++ "..\"."
|
||||
InputArgs [] -> "at the end of input"
|
||||
InputArgs xs -> case List.unwords $ show <$> xs of
|
||||
s | length s < 42 -> "at: " ++ s ++ "."
|
||||
s -> "at: " ++ take 40 s ++ "..\"."
|
||||
putStrLn $ "usage:"
|
||||
print $ ppUsage desc
|
||||
(desc, Right out) -> case _cmd_out out of
|
||||
Nothing -> do
|
||||
putStrLn $ "usage:"
|
||||
print $ ppUsage desc
|
||||
Just a -> a
|
||||
|
||||
addHelpCommand :: Applicative f => CmdParser f (IO ()) ()
|
||||
addHelpCommand = addCmd "help" $ do
|
||||
desc <- peekCmdDesc
|
||||
addCmdImpl $ do
|
||||
print $ ppHelpShallow $ maybe undefined snd (_cmd_mParent desc)
|
||||
|
||||
addButcherDebugCommand :: Applicative f => CmdParser f (IO ()) ()
|
||||
addButcherDebugCommand = addCmd "butcherdebug" $ do
|
||||
desc <- peekCmdDesc
|
||||
addCmdImpl $ do
|
||||
print $ maybe undefined snd (_cmd_mParent desc)
|
|
@ -0,0 +1,88 @@
|
|||
module UI.Butcher.Monadic.Param
|
||||
( Param(..)
|
||||
, paramHelp
|
||||
, paramHelpStr
|
||||
, paramDefault
|
||||
, addReadParam
|
||||
, addReadParamOpt
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
import Data.Dynamic
|
||||
|
||||
import UI.Butcher.Monadic.Types
|
||||
import UI.Butcher.Monadic.Core
|
||||
|
||||
|
||||
|
||||
data Param p = Param
|
||||
{ _param_default :: Maybe p
|
||||
, _param_help :: Maybe PP.Doc
|
||||
}
|
||||
|
||||
instance Monoid (Param p) where
|
||||
mempty = Param Nothing Nothing
|
||||
Param a1 b1 `mappend` Param a2 b2 = Param (a1 `f` a2) (b1 `mappend` b2)
|
||||
where
|
||||
f Nothing x = x
|
||||
f x _ = x
|
||||
|
||||
paramHelpStr :: String -> Param p
|
||||
paramHelpStr s = mempty { _param_help = Just $ PP.text s }
|
||||
|
||||
paramHelp :: PP.Doc -> Param p
|
||||
paramHelp h = mempty { _param_help = Just h }
|
||||
|
||||
paramDefault :: p -> Param p
|
||||
paramDefault d = mempty { _param_default = Just d }
|
||||
|
||||
addReadParam :: forall f out a
|
||||
. (Applicative f, Typeable a, Show a, Text.Read.Read a)
|
||||
=> String
|
||||
-> Param a
|
||||
-> CmdParser f out a
|
||||
addReadParam name par = addCmdPart desc parseF
|
||||
where
|
||||
desc :: PartDesc
|
||||
desc = (maybe id PartWithHelp $ _param_help par)
|
||||
$ (maybe id (PartDefault . show) $ _param_default par)
|
||||
$ PartVariable name
|
||||
parseF :: String -> Maybe (a, String)
|
||||
parseF s = case Text.Read.reads s of
|
||||
((x, ' ':r):_) -> Just (x, dropWhile Char.isSpace r)
|
||||
((x, []):_) -> Just (x, [])
|
||||
_ -> _param_default par <&> \x -> (x, s)
|
||||
|
||||
addReadParamOpt :: forall f out a
|
||||
. (Applicative f, Typeable a, Text.Read.Read a)
|
||||
=> String
|
||||
-> Param a
|
||||
-> CmdParser f out (Maybe a)
|
||||
addReadParamOpt name par = addCmdPart desc parseF
|
||||
where
|
||||
desc :: PartDesc
|
||||
desc = PartOptional
|
||||
$ (maybe id PartWithHelp $ _param_help par)
|
||||
$ PartVariable name
|
||||
parseF :: String -> Maybe (Maybe a, String)
|
||||
parseF s = case Text.Read.reads s of
|
||||
((x, ' ':r):_) -> Just (Just x, dropWhile Char.isSpace r)
|
||||
((x, []):_) -> Just (Just x, [])
|
||||
_ -> Just (Nothing, s) -- TODO: we could warn about a default..
|
|
@ -0,0 +1,157 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module UI.Butcher.Monadic.Pretty
|
||||
( ppUsage
|
||||
, ppUsageAt
|
||||
, ppHelpShallow
|
||||
, ppPartDescUsage
|
||||
, ppPartDescHeader
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
import Text.PrettyPrint ( (<+>), ($$), ($+$) )
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
import Data.Dynamic
|
||||
|
||||
import UI.Butcher.Monadic.Types
|
||||
import UI.Butcher.Monadic.Core
|
||||
|
||||
|
||||
|
||||
ppUsage :: CommandDesc a
|
||||
-> PP.Doc
|
||||
ppUsage (CommandDesc mParent _help _syn parts out children) =
|
||||
pparents mParent <+> PP.fsep (partDocs ++ [subsDoc])
|
||||
where
|
||||
pparents :: Maybe (String, CommandDesc out) -> PP.Doc
|
||||
pparents Nothing = PP.empty
|
||||
pparents (Just (n, cd)) = pparents (_cmd_mParent cd) <+> PP.text n
|
||||
partDocs = parts <&> ppPartDescUsage
|
||||
subsDoc = case out of
|
||||
_ | null children -> PP.empty -- TODO: remove debug
|
||||
Nothing -> PP.parens $ subDoc
|
||||
Just{} -> PP.brackets $ subDoc
|
||||
subDoc = PP.fcat $ PP.punctuate (PP.text " | ") $ children <&> \(n, _) ->
|
||||
PP.text n
|
||||
|
||||
ppUsageAt :: [String] -- (sub)command sequence
|
||||
-> CommandDesc a
|
||||
-> Maybe PP.Doc
|
||||
ppUsageAt strings desc =
|
||||
case strings of
|
||||
[] -> Just $ ppUsage desc
|
||||
(s:sr) -> find ((s==) . fst) (_cmd_children desc) >>= snd .> ppUsageAt sr
|
||||
|
||||
ppHelpShallow :: CommandDesc a
|
||||
-> PP.Doc
|
||||
ppHelpShallow desc@(CommandDesc mParent syn help parts _out _children) =
|
||||
nameSection
|
||||
$+$ usageSection
|
||||
$+$ descriptionSection
|
||||
$+$ partsSection
|
||||
$+$ PP.text ""
|
||||
where
|
||||
nameSection = case mParent of
|
||||
Nothing -> PP.empty
|
||||
Just{} ->
|
||||
PP.text "NAME"
|
||||
$+$ PP.text ""
|
||||
$+$ PP.nest 2 (case syn of
|
||||
Nothing -> pparents mParent
|
||||
Just s -> pparents mParent <+> PP.text "-" <+> s)
|
||||
$+$ PP.text ""
|
||||
pparents :: Maybe (String, CommandDesc out) -> PP.Doc
|
||||
pparents Nothing = PP.empty
|
||||
pparents (Just (n, cd)) = pparents (_cmd_mParent cd) PP.<+> PP.text n
|
||||
usageSection =
|
||||
PP.text "USAGE"
|
||||
$+$ PP.text ""
|
||||
$+$ PP.nest 2 (ppUsage desc)
|
||||
descriptionSection = case help of
|
||||
Nothing -> PP.empty
|
||||
Just h ->
|
||||
PP.text ""
|
||||
$+$ PP.text "DESCRIPTION"
|
||||
$+$ PP.text ""
|
||||
$+$ PP.nest 2 h
|
||||
partsSection = if null partsTuples then PP.empty else
|
||||
PP.text ""
|
||||
$+$ PP.text "ARGUMENTS"
|
||||
$+$ PP.text ""
|
||||
$+$ PP.nest 2 (PP.vcat partsTuples)
|
||||
partsTuples :: [PP.Doc]
|
||||
partsTuples = parts >>= go
|
||||
where
|
||||
go = \case
|
||||
PartLiteral{} -> []
|
||||
PartVariable{} -> []
|
||||
PartOptional p -> go p
|
||||
PartAlts ps -> ps >>= go
|
||||
PartSeq ps -> ps >>= go
|
||||
PartDefault _ p -> go p
|
||||
PartRedirect s p -> [PP.text s $$ PP.nest 20 (ppPartDescUsage p)]
|
||||
++ (PP.nest 2 <$> go p)
|
||||
PartReorder ps -> ps >>= go
|
||||
PartMany p -> go p
|
||||
PartWithHelp doc p -> [ppPartDescHeader p $$ PP.nest 20 doc]
|
||||
++ go p
|
||||
|
||||
ppPartDescUsage :: PartDesc -> PP.Doc
|
||||
ppPartDescUsage = \case
|
||||
PartLiteral s -> PP.text s
|
||||
PartVariable s -> PP.text s
|
||||
PartOptional p -> PP.brackets $ rec p
|
||||
PartAlts ps -> PP.fcat $ PP.punctuate (PP.text ",") $ rec <$> ps
|
||||
PartSeq ps -> PP.fsep $ rec <$> ps
|
||||
PartDefault _ p -> PP.brackets $ rec p
|
||||
PartRedirect s _ -> PP.text s
|
||||
PartMany p -> rec p <> PP.text "+"
|
||||
PartWithHelp _ p -> rec p
|
||||
PartReorder ps ->
|
||||
let flags = [d | PartMany d <- ps]
|
||||
params = filter (\case PartMany{} -> False; _ -> True) ps
|
||||
in PP.brackets (PP.fsep $ rec <$> flags)
|
||||
<+> PP.fsep (rec <$> params)
|
||||
where
|
||||
rec = ppPartDescUsage
|
||||
|
||||
ppPartDescHeader :: PartDesc -> PP.Doc
|
||||
ppPartDescHeader = \case
|
||||
PartLiteral s -> PP.text s
|
||||
PartVariable s -> PP.text s
|
||||
PartOptional ds' -> rec ds'
|
||||
PartAlts alts -> PP.hcat $ List.intersperse (PP.text ",") $ rec <$> alts
|
||||
PartDefault _ d -> rec d
|
||||
PartRedirect s _ -> PP.text s
|
||||
PartMany ds -> rec ds
|
||||
PartWithHelp _ d -> rec d
|
||||
PartSeq ds -> PP.hsep $ rec <$> ds
|
||||
PartReorder ds -> PP.vcat $ rec <$> ds
|
||||
where
|
||||
rec = ppPartDescHeader
|
|
@ -0,0 +1,185 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
|
||||
module UI.Butcher.Monadic.Types
|
||||
( CommandDesc(..)
|
||||
, cmd_mParent
|
||||
, cmd_help
|
||||
, cmd_synopsis
|
||||
, cmd_parts
|
||||
, cmd_out
|
||||
, cmd_children
|
||||
, emptyCommandDesc
|
||||
, CmdParserF(..)
|
||||
, CmdParser
|
||||
, PartDesc(..)
|
||||
, Input (..)
|
||||
, ParsingError (..)
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
-- import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import Data.Dynamic
|
||||
|
||||
|
||||
|
||||
data Input = InputString String | InputArgs [String]
|
||||
deriving (Show, Eq)
|
||||
|
||||
data ParsingError = ParsingError
|
||||
{ _pe_messages :: [String]
|
||||
, _pe_remaining :: Input
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
data CmdParserF f out a
|
||||
= CmdParserHelp PP.Doc a
|
||||
| CmdParserSynopsis String a
|
||||
| CmdParserPeekDesc (CommandDesc out -> a)
|
||||
-- TODO: we can clean up this duplication by providing
|
||||
-- a function (String -> Maybe (p, String)) -> (Input -> Maybe (p, Input)).
|
||||
| forall p . Typeable p => CmdParserPart PartDesc (String -> Maybe (p, String)) (p -> f ()) (p -> a)
|
||||
| forall p . Typeable p => CmdParserPartMany PartDesc (String -> Maybe (p, String)) (p -> f ()) ([p] -> a)
|
||||
| forall p . Typeable p => CmdParserPartInp PartDesc (Input -> Maybe (p, Input)) (p -> f ()) (p -> a)
|
||||
| forall p . Typeable p => CmdParserPartManyInp PartDesc (Input -> Maybe (p, Input)) (p -> f ()) ([p] -> a)
|
||||
| CmdParserChild String (CmdParser f out ()) (f ()) a
|
||||
| CmdParserImpl out a
|
||||
| CmdParserReorderStart a
|
||||
| CmdParserReorderStop a
|
||||
| CmdParserGrouped String a
|
||||
| CmdParserGroupEnd a
|
||||
|
||||
|
||||
type CmdParser f out = Free (CmdParserF f out)
|
||||
|
||||
-- type CmdParser a = CmdParserM a a
|
||||
|
||||
-- data CmdPartParserF a
|
||||
-- = CmdPartParserHelp String a
|
||||
-- | forall p . CmdPartParserCore (String -> Maybe (p, String)) -- parser
|
||||
-- (Maybe p) -- optional default value
|
||||
-- (p -> a)
|
||||
-- | forall p . CmdPartParserOptional (CmdPartParser p)
|
||||
-- (Maybe p -> a)
|
||||
-- -- the idea here was to allow adding some dynamic data to each "node" of
|
||||
-- -- the output CommandDesc so the user can potentially add custom additional
|
||||
-- -- information, and write a custom pretty-printer for e.g. help output
|
||||
-- -- from that dynamically-enriched CommandDesc structure.
|
||||
-- -- disabled for now, because i am not sure what exactly "adding to every
|
||||
-- -- node" involves, because the mapping from Functor to Desc is nontrivial.
|
||||
-- -- (and because i don't have a direct use-case at the moment..)
|
||||
-- -- | CmdPartParserCustom Dynamic a
|
||||
--
|
||||
-- type CmdPartParser = Free CmdPartParserF
|
||||
|
||||
---------
|
||||
|
||||
data CommandDesc out = CommandDesc
|
||||
{ _cmd_mParent :: Maybe (String, CommandDesc out)
|
||||
, _cmd_synopsis :: Maybe PP.Doc
|
||||
, _cmd_help :: Maybe PP.Doc
|
||||
, _cmd_parts :: [PartDesc]
|
||||
, _cmd_out :: Maybe out
|
||||
, _cmd_children :: [(String, CommandDesc out)]
|
||||
}
|
||||
|
||||
-- type PartSeqDesc = [PartDesc]
|
||||
|
||||
data PartDesc
|
||||
= PartLiteral String -- expect a literal string, like "--dry-run"
|
||||
| PartVariable String -- expect some user-provided input. The
|
||||
-- string represents the name for the variable
|
||||
-- used in the documentation, e.g. "FILE"
|
||||
| PartOptional PartDesc
|
||||
| PartAlts [PartDesc]
|
||||
| PartSeq [PartDesc]
|
||||
| PartDefault String -- default representation
|
||||
PartDesc
|
||||
| PartRedirect String -- name for the redirection
|
||||
PartDesc
|
||||
| PartReorder [PartDesc]
|
||||
| PartMany PartDesc
|
||||
| PartWithHelp PP.Doc PartDesc
|
||||
deriving Show
|
||||
|
||||
{-
|
||||
command documentation structure
|
||||
1. terminals. e.g. "--dry-run"
|
||||
2. non-terminals, e.g. "FILES"
|
||||
3. sequences, e.g. "<program> FLAGS NUMBER PATH"
|
||||
-- 4. alternatives, e.g. "--date=(relative|local|iso|rfc|..)"
|
||||
5. sub-commands: git (init|commit|push|clone|..)
|
||||
compared to 4, the subcommands have their own flags and params;
|
||||
they essentially "take over".
|
||||
6. optional, e.g. "cabal run [COMPONENT]"
|
||||
7. default, e.g. "-O(LEVEL=1)"
|
||||
8. indirection, e.g. "cabal COMMAND\n\nCOMMAND: ..."
|
||||
-}
|
||||
|
||||
--
|
||||
|
||||
deriving instance Functor (CmdParserF f out)
|
||||
deriving instance Functor CommandDesc
|
||||
|
||||
--
|
||||
|
||||
emptyCommandDesc :: CommandDesc out
|
||||
emptyCommandDesc = CommandDesc Nothing Nothing Nothing [] Nothing []
|
||||
|
||||
instance Show (CommandDesc out) where
|
||||
show c = "Command help=" ++ show (_cmd_help c)
|
||||
++ " synopsis=" ++ show (_cmd_synopsis c)
|
||||
++ " mParent=" ++ show (fst <$> _cmd_mParent c)
|
||||
++ " out=" ++ maybe "(none)" (\_ -> "(smth)") (_cmd_out c)
|
||||
++ " parts.length=" ++ show (length $ _cmd_parts c)
|
||||
++ " parts=" ++ show (_cmd_parts c)
|
||||
++ " children=" ++ show (fst <$> _cmd_children c)
|
||||
|
||||
--
|
||||
|
||||
Lens.makeLenses ''CommandDesc
|
||||
Lens.makeLenses ''PartDesc
|
||||
|
||||
--
|
||||
|
||||
|
||||
|
||||
-- instance Show FlagDesc where
|
||||
-- show (FlagDesc _ short long helpM params) = show (short, long, helpM, params) -- TODO: improve
|
||||
|
||||
-- class Typeable a => IsParam a where
|
||||
-- paramParse :: String -> Maybe (a, String, String) -- value, representation, rest
|
||||
-- paramStaticDef :: a
|
||||
|
||||
-- emptyParamDesc :: ParamDesc a
|
||||
-- emptyParamDesc = ParamDesc Nothing Nothing
|
||||
|
||||
-- deriving instance Show a => Show (ParamDesc a)
|
||||
|
||||
|
||||
-- instance Show a => Show (CmdParserF out a) where
|
||||
-- show (CmdParserHelp s x) = "(CmdParserHelp " ++ show s ++ " " ++ show x ++ ")"
|
||||
-- show (CmdParserFlag shorts longs _ _) = "(CmdParserFlag -" ++ shorts ++ " " ++ show longs ++ ")"
|
||||
-- show (CmdParserParam s _ _) = "(CmdParserParam " ++ s ++ ")"
|
||||
-- show (CmdParserChild s _ _) = "(CmdParserChild " ++ s ++ ")"
|
||||
-- show (CmdParserRun _) = "CmdParserRun"
|
||||
|
|
@ -1,628 +0,0 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module UI.CmdParse.Applicative
|
||||
( -- main
|
||||
cmdActionPartial
|
||||
, cmdAction
|
||||
, cmd_run
|
||||
, CmdBuilder
|
||||
, addFlag
|
||||
, addCmd
|
||||
, addParam
|
||||
, help
|
||||
, impl
|
||||
, def
|
||||
, flagAsBool
|
||||
, cmdGetPartial
|
||||
, ppCommand
|
||||
, ppCommandShort
|
||||
, ppCommandShortHelp
|
||||
-- re-exports:
|
||||
, Command(..)
|
||||
, Flag(..)
|
||||
, Param(..)
|
||||
, cmdCheckNonStatic
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Applicative.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import UI.CmdParse.Applicative.Types
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
|
||||
|
||||
-- general-purpose helpers
|
||||
----------------------------
|
||||
|
||||
mModify :: MonadMultiState s m => (s -> s) -> m ()
|
||||
mModify f = mGet >>= mSet . f
|
||||
|
||||
-- sadly, you need a degree in type inference to know when we can use
|
||||
-- these operators and when it must be avoided due to type ambiguities
|
||||
-- arising around s in the signatures below. That's the price of not having
|
||||
-- the functional dependency in MonadMulti*T.
|
||||
|
||||
(.=+) :: MonadMultiState s m
|
||||
=> Lens.ASetter s s a b -> b -> m ()
|
||||
l .=+ b = mModify $ l .~ b
|
||||
|
||||
(%=+) :: MonadMultiState s m
|
||||
=> Lens.ASetter s s a b -> (a -> b) -> m ()
|
||||
l %=+ f = mModify (l %~ f)
|
||||
|
||||
-- inflateStateProxy :: (Monad m, ContainsType s ss)
|
||||
-- => p s -> StateS.StateT s m a -> MultiRWSS.MultiRWST r w ss m a
|
||||
-- inflateStateProxy _ = MultiRWSS.inflateState
|
||||
|
||||
-- actual CmdBuilder stuff
|
||||
----------------------------
|
||||
|
||||
instance IsHelpBuilder (CmdBuilder out) where
|
||||
help s = liftAp $ CmdBuilderHelp s ()
|
||||
|
||||
instance IsHelpBuilder (ParamBuilder p) where
|
||||
help s = liftAp $ ParamBuilderHelp s ()
|
||||
|
||||
instance IsHelpBuilder FlagBuilder where
|
||||
help s = liftAp $ FlagBuilderHelp s ()
|
||||
|
||||
addCmd :: String -> CmdBuilder out () -> CmdBuilder out ()
|
||||
addCmd s m = liftAp $ CmdBuilderChild s m ()
|
||||
|
||||
addParam :: (Show p, IsParam p) => String -> ParamBuilder p () -> CmdBuilder out p
|
||||
addParam s m = liftAp $ CmdBuilderParam s m id
|
||||
|
||||
{-# NOINLINE addFlag #-}
|
||||
addFlag :: String -> [String] -> FlagBuilder a -> CmdBuilder out [a]
|
||||
addFlag shorts longs m = Unsafe.performIO $ do
|
||||
unique <- Data.Unique.newUnique
|
||||
return $ liftAp $ CmdBuilderFlag unique shorts longs m id
|
||||
|
||||
impl :: a -> CmdBuilder a ()
|
||||
impl x = liftAp $ CmdBuilderRun x ()
|
||||
|
||||
def :: p -> ParamBuilder p ()
|
||||
def d = liftAp $ ParamBuilderDef d ()
|
||||
|
||||
-- | Does some limited "static" testing on a CmdBuilder.
|
||||
-- "static" as in: it does not read any actual input.
|
||||
-- Mostly checks that certain things are not defined multiple times,
|
||||
-- e.g. help annotations.
|
||||
cmdCheckNonStatic :: CmdBuilder out () -> Maybe String
|
||||
cmdCheckNonStatic cmdBuilder = join
|
||||
$ Data.Either.Combinators.leftToMaybe
|
||||
$ flip StateS.evalState emptyCommand
|
||||
$ runEitherT
|
||||
$ runAp iterFunc cmdBuilder
|
||||
where
|
||||
iterFunc :: CmdBuilderF out a
|
||||
-> EitherT (Maybe String)
|
||||
(StateS.State (Command out0)) a
|
||||
iterFunc = \case
|
||||
CmdBuilderHelp h r -> do
|
||||
cmd <- State.Class.get
|
||||
case _cmd_help cmd of
|
||||
Nothing ->
|
||||
cmd_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
pure r
|
||||
CmdBuilderFlag funique _shorts _longs f r -> do
|
||||
case checkFlag funique f of -- yes, this is a mapM_.
|
||||
Nothing -> pure () -- but that does not help readability.
|
||||
err -> left $ err
|
||||
pure $ r []
|
||||
CmdBuilderParam _ p r -> do
|
||||
case checkParam p of
|
||||
Nothing -> pure ()
|
||||
err -> left $ err
|
||||
pure $ r $ paramStaticDef
|
||||
CmdBuilderChild _s c r -> do
|
||||
case cmdCheckNonStatic c of
|
||||
Nothing -> pure ()
|
||||
err -> left $ err
|
||||
pure r
|
||||
CmdBuilderRun _o _r ->
|
||||
left Nothing
|
||||
checkFlag :: Unique -> FlagBuilder b -> Maybe String
|
||||
checkFlag unique flagBuilder = join
|
||||
$ Data.Either.Combinators.leftToMaybe
|
||||
$ flip StateS.evalState (Flag unique "" [] Nothing [])
|
||||
$ runEitherT
|
||||
$ runAp iterFuncFlag flagBuilder
|
||||
where
|
||||
iterFuncFlag :: FlagBuilderF b
|
||||
-> EitherT (Maybe String) (StateS.State Flag) b
|
||||
iterFuncFlag = \case
|
||||
FlagBuilderHelp h r -> do
|
||||
param <- State.Class.get
|
||||
case _flag_help param of
|
||||
Nothing ->
|
||||
flag_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
pure r
|
||||
FlagBuilderParam _s p r -> do
|
||||
case checkParam p of
|
||||
Nothing -> pure ()
|
||||
err -> left $ err
|
||||
pure $ r $ paramStaticDef
|
||||
checkParam :: Show p => ParamBuilder p () -> Maybe String
|
||||
checkParam paramBuilder = join
|
||||
$ Data.Either.Combinators.leftToMaybe
|
||||
$ flip StateS.evalState (Param Nothing Nothing)
|
||||
$ runEitherT
|
||||
$ runAp iterFuncParam paramBuilder
|
||||
where
|
||||
iterFuncParam :: Show p
|
||||
=> ParamBuilderF p a
|
||||
-> EitherT (Maybe String) (StateS.State (Param p)) a
|
||||
iterFuncParam = \case
|
||||
ParamBuilderHelp h r -> do
|
||||
param <- State.Class.get
|
||||
case _param_help param of
|
||||
Nothing ->
|
||||
param_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
pure $ r
|
||||
ParamBuilderDef d r -> do
|
||||
param <- State.Class.get
|
||||
case _param_def param of
|
||||
Nothing ->
|
||||
param_def .= Just d
|
||||
Just{} ->
|
||||
left $ Just $ "default is already defined when trying to add default \"" ++ show d ++ "\""
|
||||
pure $ r
|
||||
|
||||
cmdGetPartial :: forall out . String
|
||||
-> CmdBuilder out ()
|
||||
-> ( [String] -- errors
|
||||
, String -- remaining string
|
||||
, Command out -- current result, as far as parsing was possible.
|
||||
-- (!) take care not to run this command's action
|
||||
-- if there are errors (!)
|
||||
)
|
||||
cmdGetPartial inputStr cmdBuilder
|
||||
= runIdentity
|
||||
$ MultiRWSS.runMultiRWSTNil
|
||||
$ (<&> captureFinal)
|
||||
$ MultiRWSS.withMultiWriterWA
|
||||
$ MultiRWSS.withMultiStateSA inputStr
|
||||
$ MultiRWSS.withMultiStateS emptyCommand
|
||||
$ processMain cmdBuilder
|
||||
where
|
||||
-- make sure that all input is processed; otherwise
|
||||
-- add an error.
|
||||
-- Does not use the writer because this method does some tuple
|
||||
-- shuffling.
|
||||
captureFinal :: ([String], (String, Command out))
|
||||
-> ([String], String, Command out)
|
||||
captureFinal (errs, (s, cmd)) = (errs', s, cmd)
|
||||
where
|
||||
errs' = errs ++ if not $ all Char.isSpace s
|
||||
then ["could not parse input at " ++ s]
|
||||
else []
|
||||
|
||||
-- main "interpreter" over the free monad. not implemented as an iteration
|
||||
-- because we switch to a different interpreter (and interpret the same
|
||||
-- stuff more than once) when processing flags.
|
||||
processMain :: CmdBuilder out ()
|
||||
-> MultiRWSS.MultiRWS '[] '[[String]] '[Command out, String] ()
|
||||
processMain = \case
|
||||
Pure x -> return x
|
||||
Ap (CmdBuilderHelp h r) next -> do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd { _cmd_help = Just h }
|
||||
processMain $ ($ r) <$> next
|
||||
f@(Ap (CmdBuilderFlag{}) _) -> do
|
||||
flagData <- MultiRWSS.withMultiWriterW $ -- WriterS.execWriterT $
|
||||
runAp iterFlagGather f
|
||||
do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd { _cmd_flags = _cmd_flags cmd ++ flagData }
|
||||
parsedFlag <- MultiRWSS.withMultiStateS (Map.empty :: FlagParsedMap)
|
||||
$ parseFlags flagData
|
||||
(finalMap, fr) <- MultiRWSS.withMultiStateSA parsedFlag $ runParsedFlag f
|
||||
if Map.null finalMap
|
||||
then processMain fr
|
||||
else mTell ["internal error in application or colint library: inconsistent flag definitions."]
|
||||
Ap (CmdBuilderParam s p r) next -> do
|
||||
let param = processParam p
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd { _cmd_params = _cmd_params cmd ++ [ParamA s param] }
|
||||
str <- mGet
|
||||
x <- case (paramParse str, _param_def param) of
|
||||
(Nothing, Just x) -> do
|
||||
-- did not parse, use configured default value
|
||||
return $ x
|
||||
(Nothing, Nothing) -> do
|
||||
-- did not parse, no default value. add error, cont. with static default.
|
||||
mTell ["could not parse param at " ++ str]
|
||||
return paramStaticDef
|
||||
(Just (v, _, x), _) -> do
|
||||
-- parsed value; update the rest-string-to-parse, return value.
|
||||
mSet $ x
|
||||
return $ v
|
||||
processMain $ ($ r x) <$> next
|
||||
Ap (CmdBuilderChild s c r) next -> do
|
||||
dropSpaces
|
||||
str <- mGet
|
||||
let mRest = if
|
||||
| s == str -> Just ""
|
||||
| (s++" ") `isPrefixOf` str -> Just $ drop (length s + 1) str
|
||||
| otherwise -> Nothing
|
||||
case mRest of
|
||||
Nothing -> do
|
||||
cmd :: Command out <- mGet
|
||||
subCmd <- MultiRWSS.withMultiStateS emptyCommand
|
||||
$ runAp processCmdShallow c
|
||||
mSet $ cmd { _cmd_children = _cmd_children cmd ++ [(s, subCmd)] }
|
||||
processMain $ ($ r) <$> next
|
||||
Just rest -> do
|
||||
old :: Command out <- mGet
|
||||
mSet $ rest
|
||||
mSet $ emptyCommand {
|
||||
_cmd_mParent = Just (old, s)
|
||||
}
|
||||
processMain c
|
||||
Ap (CmdBuilderRun o r) next -> do
|
||||
cmd_run .=+ Just o
|
||||
processMain $ ($ r) <$> next
|
||||
|
||||
-- only captures some (i.e. roughly one layer) of the structure of
|
||||
-- the (remaining) builder, not parsing any input.
|
||||
processCmdShallow :: MonadMultiState (Command out) m
|
||||
=> CmdBuilderF out a
|
||||
-> m a
|
||||
processCmdShallow = \case
|
||||
CmdBuilderHelp h r -> do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd {
|
||||
_cmd_help = Just h
|
||||
}
|
||||
pure $ r
|
||||
CmdBuilderFlag _funique _shorts _longs _f r -> do
|
||||
pure $ r []
|
||||
CmdBuilderParam s p r -> do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd {
|
||||
_cmd_params = _cmd_params cmd ++ [ParamA s $ processParam p]
|
||||
}
|
||||
pure $ r $ paramStaticDef
|
||||
CmdBuilderChild s _c r -> do
|
||||
cmd_children %=+ (++[(s, emptyCommand :: Command out)])
|
||||
pure $ r
|
||||
CmdBuilderRun _o r ->
|
||||
pure $ r
|
||||
|
||||
-- extract a list of flag declarations. return [], i.e. pretend that no
|
||||
-- flag matches while doing so.
|
||||
iterFlagGather :: CmdBuilderF out a
|
||||
-> MultiRWSS.MultiRWS r ([Flag]':wr) s a
|
||||
iterFlagGather = \case
|
||||
-- x | trace ("iterFlagGather: " ++ show (x $> ())) False -> error "laksjdlkja"
|
||||
CmdBuilderFlag funique shorts longs f next -> do
|
||||
let flag = processFlag funique shorts longs f
|
||||
mTell $ [flag]
|
||||
pure $ next []
|
||||
CmdBuilderHelp _ r -> pure r
|
||||
CmdBuilderParam _ _ r -> pure $ r $ paramStaticDef
|
||||
CmdBuilderChild _ _ r -> pure r
|
||||
CmdBuilderRun _ r -> pure r
|
||||
|
||||
-- the second iteration (technically not an iterM, but close..) over flags:
|
||||
-- use the parsed flag map, so that the actual flag (values) are captured
|
||||
-- in this run.
|
||||
-- return the final CmdBuilder when a non-flag is encountered.
|
||||
runParsedFlag :: CmdBuilder out ()
|
||||
-> MultiRWSS.MultiRWS '[] '[[String]] '[FlagParsedMap, Command out, String] (CmdBuilder out ())
|
||||
runParsedFlag = \case
|
||||
Ap (CmdBuilderFlag funique _ _ f r) next -> do
|
||||
m :: FlagParsedMap <- mGet
|
||||
let flagRawStrs = case Map.lookup funique m of
|
||||
Nothing -> []
|
||||
Just x -> x
|
||||
mSet $ Map.delete funique m
|
||||
runParsedFlag $ next <&> \g -> g $ r $ reparseFlag f <$> flagRawStrs
|
||||
Pure x -> return $ pure x
|
||||
f -> return f
|
||||
|
||||
reparseFlag :: FlagBuilder b -> FlagParsedElement -> b
|
||||
reparseFlag = undefined -- TODO FIXME WHO LEFT THIS HERE
|
||||
|
||||
parseFlags :: ( MonadMultiWriter [String] m
|
||||
, MonadMultiState String m
|
||||
, MonadMultiState FlagParsedMap m
|
||||
)
|
||||
=> [Flag]
|
||||
-> m ()
|
||||
parseFlags flags = do
|
||||
dropSpaces
|
||||
str <- mGet
|
||||
case str of
|
||||
('-':'-':longRest) ->
|
||||
case getAlt $ mconcat $ flags <&> \f
|
||||
-> mconcat $ _flag_long f <&> \l
|
||||
-> let len = length l
|
||||
in Alt $ do
|
||||
guard $ isPrefixOf l longRest
|
||||
r <- case List.drop len longRest of
|
||||
"" -> return ""
|
||||
(' ':r) -> return r
|
||||
_ -> mzero
|
||||
return $ (l, r, f) of
|
||||
Nothing -> mTell ["could not understand flag at --" ++ longRest]
|
||||
Just (flagStr, flagRest, flag) ->
|
||||
if length (_flag_params flag) /= 0
|
||||
then error "flag params not supported yet!"
|
||||
else do
|
||||
mSet flagRest
|
||||
mModify $ Map.insertWith (++)
|
||||
(_flag_unique flag)
|
||||
[FlagParsedElement [flagStr]]
|
||||
('-':shortRest) ->
|
||||
case shortRest of
|
||||
(c:' ':r) ->
|
||||
case getAlt $ mconcat $ flags <&> \f
|
||||
-> mconcat $ _flag_short f <&> \s
|
||||
-> Alt $ do
|
||||
guard $ c==s
|
||||
r' <- case r of
|
||||
(' ':r') -> return r'
|
||||
_ -> mzero
|
||||
return (c, r', f) of
|
||||
Nothing -> mTell ["could not understand flag at -" ++ shortRest]
|
||||
Just (flagChr, flagRest, flag) ->
|
||||
if length (_flag_params flag) /= 0
|
||||
then error "flag params not supported yet!"
|
||||
else do
|
||||
mSet flagRest
|
||||
mModify $ Map.insertWith (++)
|
||||
(_flag_unique flag)
|
||||
[FlagParsedElement ["-"++[flagChr]]]
|
||||
_ -> mTell ["could not parse flag at -" ++ shortRest]
|
||||
_ -> pure ()
|
||||
dropSpaces :: MonadMultiState String m => m ()
|
||||
dropSpaces = mModify $ dropWhile Char.isSpace
|
||||
processFlag :: Unique -> [Char] -> [String] -> FlagBuilder b -> Flag
|
||||
processFlag unique shorts longs flagBuilder
|
||||
= flip StateS.execState (Flag unique shorts longs Nothing [])
|
||||
$ runAp iterFuncFlag flagBuilder
|
||||
where
|
||||
iterFuncFlag :: FlagBuilderF a
|
||||
-> (StateS.State Flag) a
|
||||
iterFuncFlag = \case
|
||||
FlagBuilderHelp h r -> (flag_help .= Just h) $> r
|
||||
FlagBuilderParam s p r -> do
|
||||
let param = processParam p
|
||||
flag_params %= (++ [ParamA s param])
|
||||
pure $ r $ paramStaticDef
|
||||
processParam :: Show p => ParamBuilder p () -> Param p
|
||||
processParam paramBuilder = flip StateS.execState emptyParam
|
||||
$ runEitherT
|
||||
$ runAp iterFuncParam paramBuilder
|
||||
where
|
||||
iterFuncParam :: Show p
|
||||
=> ParamBuilderF p a
|
||||
-> EitherT (Maybe String) (StateS.State (Param p)) a
|
||||
iterFuncParam = \case
|
||||
ParamBuilderHelp h r -> do
|
||||
param <- State.Class.get
|
||||
case _param_help param of
|
||||
Nothing ->
|
||||
param_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
pure $ r
|
||||
ParamBuilderDef d r -> do
|
||||
param <- State.Class.get
|
||||
case _param_def param of
|
||||
Nothing ->
|
||||
param_def .= Just d
|
||||
Just{} ->
|
||||
left $ Just $ "default is already defined when trying to add default \"" ++ show d ++ "\""
|
||||
pure $ r
|
||||
|
||||
cmdActionPartial :: Command out -> Either String out
|
||||
cmdActionPartial = maybe (Left err) Right . _cmd_run
|
||||
where
|
||||
err = "command is missing implementation!"
|
||||
|
||||
cmdAction :: String -> CmdBuilder out () -> Either String out
|
||||
cmdAction s b = case cmdGetPartial s b of
|
||||
([], _, cmd) -> cmdActionPartial cmd
|
||||
((out:_), _, _) -> Left $ out
|
||||
|
||||
ppCommand :: Command out -> String
|
||||
ppCommand cmd
|
||||
= PP.render
|
||||
$ PP.vcat
|
||||
[ case _cmd_help cmd of
|
||||
Nothing -> PP.empty
|
||||
Just x -> PP.text x
|
||||
, case _cmd_children cmd of
|
||||
[] -> PP.empty
|
||||
cs -> PP.text "commands:" PP.$$ PP.nest 2 (PP.vcat $ commandShort <$> cs)
|
||||
, case _cmd_flags cmd of
|
||||
[] -> PP.empty
|
||||
fs -> PP.text "flags:" PP.$$ PP.nest 2 (PP.vcat $ flagShort <$> fs)
|
||||
]
|
||||
where
|
||||
commandShort :: (String, Command out) -> PP.Doc
|
||||
commandShort (s, c)
|
||||
= PP.text (s ++ ((_cmd_params c) >>= \(ParamA ps _) -> " " ++ ps))
|
||||
PP.<> case _cmd_help c of
|
||||
Nothing -> PP.empty
|
||||
Just h -> PP.text ":" PP.<+> PP.text h
|
||||
flagShort :: Flag -> PP.Doc
|
||||
flagShort f = PP.hsep (PP.text . ("-"++) . return <$> _flag_short f)
|
||||
PP.<+> PP.hsep (PP.text . ("--"++) <$> _flag_long f)
|
||||
PP.<+> case _flag_help f of
|
||||
Nothing -> PP.empty
|
||||
Just h -> PP.text h
|
||||
|
||||
ppCommandShort :: Command out -> String
|
||||
ppCommandShort cmd
|
||||
= PP.render
|
||||
$ printParent cmd
|
||||
PP.<+>
|
||||
case _cmd_flags cmd of
|
||||
[] -> PP.empty
|
||||
fs -> tooLongText 20 "[FLAGS]" $ List.unwords $ fs <&> \f ->
|
||||
"["
|
||||
++ (List.unwords $ (_flag_short f <&> \c -> ['-', c])
|
||||
++ (_flag_long f <&> \l -> "--" ++ l)
|
||||
)
|
||||
++ "]"
|
||||
PP.<+>
|
||||
case _cmd_params cmd of
|
||||
[] -> PP.empty
|
||||
ps -> PP.text $ List.unwords $ ps <&> \(ParamA s _) -> Char.toUpper <$> s
|
||||
PP.<+>
|
||||
case _cmd_children cmd of
|
||||
[] -> PP.empty
|
||||
cs -> PP.text
|
||||
$ if Maybe.isJust $ _cmd_run cmd
|
||||
then "[<" ++ intercalate "|" (fst <$> cs) ++ ">]"
|
||||
else "<" ++ intercalate "|" (fst <$> cs) ++ ">"
|
||||
where
|
||||
printParent :: Command out -> PP.Doc
|
||||
printParent c = case _cmd_mParent c of
|
||||
Nothing -> PP.empty
|
||||
Just (p, x) -> printParent p PP.<+> PP.text x
|
||||
|
||||
ppCommandShortHelp :: Command out -> String
|
||||
ppCommandShortHelp cmd
|
||||
= PP.render
|
||||
$ printParent cmd
|
||||
PP.<+>
|
||||
case _cmd_flags cmd of
|
||||
[] -> PP.empty
|
||||
fs -> tooLongText 20 "[FLAGS]" $ List.unwords $ fs <&> \f ->
|
||||
"["
|
||||
++ (List.unwords $ (_flag_short f <&> \c -> ['-', c])
|
||||
++ (_flag_long f <&> \l -> "--" ++ l)
|
||||
)
|
||||
++ "]"
|
||||
PP.<+>
|
||||
case _cmd_params cmd of
|
||||
[] -> PP.empty
|
||||
ps -> PP.text $ List.unwords $ ps <&> \(ParamA s _) -> Char.toUpper <$> s
|
||||
PP.<+>
|
||||
case _cmd_children cmd of
|
||||
[] -> PP.empty
|
||||
cs -> PP.text
|
||||
$ if Maybe.isJust $ _cmd_run cmd
|
||||
then "[<" ++ intercalate "|" (fst <$> cs) ++ ">]"
|
||||
else "<" ++ intercalate "|" (fst <$> cs) ++ ">"
|
||||
PP.<>
|
||||
case _cmd_help cmd of
|
||||
Nothing -> PP.empty
|
||||
Just h -> PP.text ":" PP.<+> PP.text h
|
||||
where
|
||||
printParent :: Command out -> PP.Doc
|
||||
printParent c = case _cmd_mParent c of
|
||||
Nothing -> PP.empty
|
||||
Just (p, x) -> printParent p PP.<+> PP.text x
|
||||
|
||||
tooLongText :: Int -- max length
|
||||
-> String -- alternative if actual length is bigger than max.
|
||||
-> String -- text to print, if length is fine.
|
||||
-> PP.Doc
|
||||
tooLongText i alt s = PP.text $ Bool.bool alt s $ null $ drop i s
|
||||
|
||||
-- TODO
|
||||
{-
|
||||
cmds :: CmdBuilder (IO ()) ()
|
||||
cmds = do
|
||||
_ <- addCmd "echo" $ do
|
||||
_ <- help "print its parameter to output"
|
||||
str <- addParam "string" $ do
|
||||
_ <- help "the string to print"
|
||||
pure ()
|
||||
-- def "foo"
|
||||
_ <- impl $ do
|
||||
putStrLn str
|
||||
pure ()
|
||||
addCmd "hello" $ do
|
||||
help "prints some greeting"
|
||||
short <- flagAsBool $ addFlag "" ["short"] $ pure ()
|
||||
name <- addParam "name" $ do
|
||||
_ <- help "your name, so you can be greeted properly"
|
||||
_ <- def "user"
|
||||
pure ()
|
||||
impl $ do
|
||||
if short
|
||||
then putStrLn $ "hi, " ++ name ++"!"
|
||||
else putStrLn $ "hello, " ++ name ++", welcome to colint!"
|
||||
pure ()
|
||||
pure ()
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
case cmdCheckNonStatic cmds of
|
||||
Just err -> do
|
||||
putStrLn "error building commands!!"
|
||||
putStrLn err
|
||||
Nothing -> do
|
||||
forever $ do
|
||||
putStr "> "
|
||||
hFlush stdout
|
||||
input <- System.IO.getLine
|
||||
let (errs, _, partial) = cmdGetPartial input cmds
|
||||
print partial
|
||||
putStrLn $ ppCommand $ partial
|
||||
case (errs, cmdActionPartial partial) of
|
||||
(err:_, _) -> print err
|
||||
([], eEff) -> case eEff of
|
||||
Left err -> do
|
||||
putStrLn $ "could not interpret input: " ++ err
|
||||
Right eff -> do
|
||||
eff
|
||||
-}
|
||||
|
||||
flagAsBool :: CmdBuilder m [a] -> CmdBuilder m Bool
|
||||
flagAsBool = fmap (not . null)
|
||||
|
||||
-- ----
|
||||
|
||||
instance IsParam String where
|
||||
paramParse s = do
|
||||
let s1 = dropWhile Char.isSpace s
|
||||
let (param, rest) = List.span (not . Char.isSpace) s1
|
||||
guard $ not $ null param
|
||||
pure $ (param, param, rest) -- we remove trailing whitespace, evil as we are.
|
||||
paramStaticDef = ""
|
||||
|
||||
instance IsParam () where
|
||||
paramParse s = do
|
||||
let s1 = dropWhile Char.isSpace s
|
||||
rest <- List.stripPrefix "()" s1
|
||||
pure $ ((), "()", rest)
|
||||
paramStaticDef = ()
|
|
@ -1,155 +0,0 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
|
||||
module UI.CmdParse.Applicative.Types
|
||||
( Command(..)
|
||||
, cmd_mParent
|
||||
, cmd_help
|
||||
, cmd_flags
|
||||
, cmd_params
|
||||
, cmd_children
|
||||
, cmd_run
|
||||
, flag_help
|
||||
, flag_long
|
||||
, flag_params
|
||||
, flag_unique
|
||||
, flag_short
|
||||
, param_def
|
||||
, param_help
|
||||
, emptyCommand
|
||||
, emptyParam
|
||||
, FlagParsedMap
|
||||
, FlagParsedElement(..)
|
||||
, IsParam(..)
|
||||
, IsHelpBuilder(..)
|
||||
, CmdBuilderF(..)
|
||||
, CmdBuilder
|
||||
, ParamBuilderF(..)
|
||||
, ParamBuilder
|
||||
, FlagBuilderF(..)
|
||||
, FlagBuilder
|
||||
, Flag(..)
|
||||
, Param(..)
|
||||
, ParamA(..)
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Applicative.Free
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
|
||||
|
||||
|
||||
data Command out = Command
|
||||
{ _cmd_mParent :: Maybe (Command out, String) -- parent command
|
||||
-- , substring that leads to $this.
|
||||
-- (kinda wonky, i know.)
|
||||
, _cmd_help :: Maybe String
|
||||
, _cmd_flags :: [Flag]
|
||||
, _cmd_params :: [ParamA]
|
||||
, _cmd_children :: [(String, Command out)]
|
||||
, _cmd_run :: Maybe out
|
||||
}
|
||||
|
||||
emptyCommand :: Command out
|
||||
emptyCommand = Command Nothing Nothing [] [] [] Nothing
|
||||
|
||||
instance Show (Command out) where
|
||||
show c = "Command help=" ++ show (_cmd_help c)
|
||||
++ " flags=" ++ show (_cmd_flags c)
|
||||
++ " params=" ++ show (_cmd_params c)
|
||||
++ " children=" ++ show (_cmd_children c)
|
||||
++ " run=" ++ case _cmd_run c of Nothing -> "Nothing"; Just{} -> "Just{..}"
|
||||
|
||||
-- class IsFlag a where
|
||||
-- flagParse :: String -> Maybe (a, String)
|
||||
-- staticDef :: a
|
||||
|
||||
data Flag = Flag
|
||||
{ _flag_unique :: Unique
|
||||
, _flag_short :: String
|
||||
, _flag_long :: [String]
|
||||
, _flag_help :: Maybe String
|
||||
, _flag_params :: [ParamA]
|
||||
}
|
||||
|
||||
instance Show Flag where
|
||||
show (Flag _ short long helpM params) = show (short, long, helpM, params) -- TODO: improve
|
||||
|
||||
type FlagParsedMap = Map Unique [FlagParsedElement]
|
||||
|
||||
data FlagParsedElement = FlagParsedElement [String]
|
||||
deriving Show
|
||||
|
||||
data ParamA = forall p . (IsParam p, Show p) => ParamA String (Param p)
|
||||
|
||||
deriving instance Show ParamA
|
||||
|
||||
class IsParam a where
|
||||
paramParse :: String -> Maybe (a, String, String) -- value, representation, rest
|
||||
paramStaticDef :: a
|
||||
|
||||
data Param a = Param
|
||||
{ _param_help :: Maybe String
|
||||
, _param_def :: Maybe a
|
||||
}
|
||||
|
||||
emptyParam :: Param a
|
||||
emptyParam = Param Nothing Nothing
|
||||
|
||||
deriving instance Show a => Show (Param a)
|
||||
|
||||
data CmdBuilderF out a
|
||||
= CmdBuilderHelp String a
|
||||
| forall b . CmdBuilderFlag Unique String [String] (FlagBuilder b) ([b] -> a)
|
||||
| forall p . (Show p, IsParam p) => CmdBuilderParam String (ParamBuilder p ()) (p -> a)
|
||||
| CmdBuilderChild String (CmdBuilder out ()) a
|
||||
| CmdBuilderRun out a
|
||||
|
||||
deriving instance Functor (CmdBuilderF out)
|
||||
|
||||
instance Show a => Show (CmdBuilderF out a) where
|
||||
show (CmdBuilderHelp s x) = "(CmdBuilderHelp " ++ show s ++ " " ++ show x ++ ")"
|
||||
show (CmdBuilderFlag _ shorts longs _ _) = "(CmdBuilderFlag -" ++ shorts ++ " " ++ show longs ++ ")"
|
||||
show (CmdBuilderParam s _ _) = "(CmdBuilderParam " ++ s ++ ")"
|
||||
show (CmdBuilderChild s _ _) = "(CmdBuilderChild " ++ s ++ ")"
|
||||
show (CmdBuilderRun _ _) = "CmdBuilderRun"
|
||||
|
||||
type CmdBuilder out = Ap (CmdBuilderF out)
|
||||
|
||||
data FlagBuilderF a
|
||||
= FlagBuilderHelp String a
|
||||
| forall p . (Show p, IsParam p) => FlagBuilderParam String (ParamBuilder p ()) (p -> a)
|
||||
|
||||
deriving instance Functor FlagBuilderF
|
||||
|
||||
type FlagBuilder = Ap FlagBuilderF
|
||||
|
||||
data ParamBuilderF p a
|
||||
= ParamBuilderHelp String a
|
||||
| ParamBuilderDef p a
|
||||
|
||||
deriving instance Functor (ParamBuilderF p)
|
||||
|
||||
type ParamBuilder p = Ap (ParamBuilderF p)
|
||||
|
||||
class IsHelpBuilder m where
|
||||
help :: String -> m ()
|
||||
|
||||
Lens.makeLenses ''Command
|
||||
Lens.makeLenses ''Flag
|
||||
Lens.makeLenses ''Param
|
|
@ -1,616 +0,0 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module UI.CmdParse.Monadic
|
||||
( main
|
||||
, cmdActionPartial
|
||||
, cmdAction
|
||||
, cmd_run
|
||||
, CmdBuilder
|
||||
, addFlag
|
||||
, addCmd
|
||||
, addParam
|
||||
, help
|
||||
, impl
|
||||
, def
|
||||
, flagAsBool
|
||||
, cmdGetPartial
|
||||
, ppCommand
|
||||
, ppCommandShort
|
||||
, ppCommandShortHelp
|
||||
-- re-exports:
|
||||
, Command(..)
|
||||
, Flag(..)
|
||||
, Param(..)
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiRWS.Strict as MultiRWSS
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
import Control.Lens ( (.=), (%=), (%~), (.~) )
|
||||
|
||||
import qualified Text.PrettyPrint as PP
|
||||
|
||||
import UI.CmdParse.Monadic.Types
|
||||
|
||||
import Data.HList.ContainsType
|
||||
|
||||
|
||||
|
||||
-- general-purpose helpers
|
||||
----------------------------
|
||||
|
||||
mModify :: MonadMultiState s m => (s -> s) -> m ()
|
||||
mModify f = mGet >>= mSet . f
|
||||
|
||||
-- sadly, you need a degree in type inference to know when we can use
|
||||
-- these operators and when it must be avoided due to type ambiguities
|
||||
-- arising around s in the signatures below. That's the price of not having
|
||||
-- the functional dependency in MonadMulti*T.
|
||||
|
||||
(.=+) :: MonadMultiState s m
|
||||
=> Lens.ASetter s s a b -> b -> m ()
|
||||
l .=+ b = mModify $ l .~ b
|
||||
|
||||
(%=+) :: MonadMultiState s m
|
||||
=> Lens.ASetter s s a b -> (a -> b) -> m ()
|
||||
l %=+ f = mModify (l %~ f)
|
||||
|
||||
-- inflateStateProxy :: (Monad m, ContainsType s ss)
|
||||
-- => p s -> StateS.StateT s m a -> MultiRWSS.MultiRWST r w ss m a
|
||||
-- inflateStateProxy _ = MultiRWSS.inflateState
|
||||
|
||||
-- actual CmdBuilder stuff
|
||||
----------------------------
|
||||
|
||||
instance IsHelpBuilder (CmdBuilder out) where
|
||||
help s = liftF $ CmdBuilderHelp s ()
|
||||
|
||||
instance IsHelpBuilder (ParamBuilder p) where
|
||||
help s = liftF $ ParamBuilderHelp s ()
|
||||
|
||||
instance IsHelpBuilder FlagBuilder where
|
||||
help s = liftF $ FlagBuilderHelp s ()
|
||||
|
||||
addCmd :: String -> CmdBuilder out () -> CmdBuilder out ()
|
||||
addCmd s m = liftF $ CmdBuilderChild s m ()
|
||||
|
||||
addParam :: (Show p, IsParam p) => String -> ParamBuilder p () -> CmdBuilder out p
|
||||
addParam s m = liftF $ CmdBuilderParam s m id
|
||||
|
||||
{-# NOINLINE addFlag #-}
|
||||
addFlag :: String -> [String] -> FlagBuilder a -> CmdBuilder out [a]
|
||||
addFlag shorts longs m = Unsafe.performIO $ do
|
||||
unique <- Data.Unique.newUnique
|
||||
return $ liftF $ CmdBuilderFlag unique shorts longs m id
|
||||
|
||||
impl :: a -> CmdBuilder a ()
|
||||
impl x = liftF $ CmdBuilderRun x
|
||||
|
||||
def :: p -> ParamBuilder p ()
|
||||
def d = liftF $ ParamBuilderDef d ()
|
||||
|
||||
-- | Does some limited "static" testing on a CmdBuilder.
|
||||
-- "static" as in: it does not read any actual input.
|
||||
-- Mostly checks that certain things are not defined multiple times,
|
||||
-- e.g. help annotations.
|
||||
cmdCheckNonStatic :: CmdBuilder out () -> Maybe String
|
||||
cmdCheckNonStatic cmdBuilder = join
|
||||
$ Data.Either.Combinators.leftToMaybe
|
||||
$ flip StateS.evalState emptyCommand
|
||||
$ runEitherT
|
||||
$ iterM iterFunc cmdBuilder
|
||||
where
|
||||
iterFunc :: CmdBuilderF out
|
||||
(EitherT (Maybe String)
|
||||
(StateS.State (Command out0)) a)
|
||||
-> EitherT (Maybe String)
|
||||
(StateS.State (Command out0)) a
|
||||
iterFunc = \case
|
||||
CmdBuilderHelp h next -> do
|
||||
cmd <- State.Class.get
|
||||
case _cmd_help cmd of
|
||||
Nothing ->
|
||||
cmd_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
next
|
||||
CmdBuilderFlag funique _shorts _longs f next -> do
|
||||
case checkFlag funique f of -- yes, this is a mapM_.
|
||||
Nothing -> pure () -- but that does not help readability.
|
||||
err -> left $ err
|
||||
next []
|
||||
CmdBuilderParam _ p next -> do
|
||||
case checkParam p of
|
||||
Nothing -> pure ()
|
||||
err -> left $ err
|
||||
next $ paramStaticDef
|
||||
CmdBuilderChild _s c next -> do
|
||||
case cmdCheckNonStatic c of
|
||||
Nothing -> pure ()
|
||||
err -> left $ err
|
||||
next
|
||||
CmdBuilderRun _o ->
|
||||
left Nothing
|
||||
checkFlag :: Unique -> FlagBuilder b -> Maybe String
|
||||
checkFlag unique flagBuilder = join
|
||||
$ Data.Either.Combinators.leftToMaybe
|
||||
$ flip StateS.evalState (Flag unique "" [] Nothing [])
|
||||
$ runEitherT
|
||||
$ iterM iterFuncFlag flagBuilder
|
||||
where
|
||||
iterFuncFlag :: FlagBuilderF (EitherT (Maybe String) (StateS.State Flag) b)
|
||||
-> EitherT (Maybe String) (StateS.State Flag) b
|
||||
iterFuncFlag = \case
|
||||
FlagBuilderHelp h next -> do
|
||||
param <- State.Class.get
|
||||
case _flag_help param of
|
||||
Nothing ->
|
||||
flag_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
next
|
||||
FlagBuilderParam _s p next -> do
|
||||
case checkParam p of
|
||||
Nothing -> pure ()
|
||||
err -> left $ err
|
||||
next $ paramStaticDef
|
||||
checkParam :: Show p => ParamBuilder p () -> Maybe String
|
||||
checkParam paramBuilder = join
|
||||
$ Data.Either.Combinators.leftToMaybe
|
||||
$ flip StateS.evalState (Param Nothing Nothing)
|
||||
$ runEitherT
|
||||
$ iterM iterFuncParam paramBuilder
|
||||
where
|
||||
iterFuncParam :: Show p
|
||||
=> ParamBuilderF p (EitherT (Maybe String) (StateS.State (Param p)) ())
|
||||
-> EitherT (Maybe String) (StateS.State (Param p)) ()
|
||||
iterFuncParam = \case
|
||||
ParamBuilderHelp h next -> do
|
||||
param <- State.Class.get
|
||||
case _param_help param of
|
||||
Nothing ->
|
||||
param_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
next
|
||||
ParamBuilderDef d next -> do
|
||||
param <- State.Class.get
|
||||
case _param_def param of
|
||||
Nothing ->
|
||||
param_def .= Just d
|
||||
Just{} ->
|
||||
left $ Just $ "default is already defined when trying to add default \"" ++ show d ++ "\""
|
||||
next
|
||||
|
||||
cmdGetPartial :: forall out . String
|
||||
-> CmdBuilder out ()
|
||||
-> ( [String] -- errors
|
||||
, String -- remaining string
|
||||
, Command out -- current result, as far as parsing was possible.
|
||||
-- (!) take care not to run this command's action
|
||||
-- if there are errors (!)
|
||||
)
|
||||
cmdGetPartial inputStr cmdBuilder
|
||||
= runIdentity
|
||||
$ MultiRWSS.runMultiRWSTNil
|
||||
$ (<&> captureFinal)
|
||||
$ MultiRWSS.withMultiWriterWA
|
||||
$ MultiRWSS.withMultiStateSA inputStr
|
||||
$ MultiRWSS.withMultiStateS emptyCommand
|
||||
$ processMain cmdBuilder
|
||||
where
|
||||
-- make sure that all input is processed; otherwise
|
||||
-- add an error.
|
||||
-- Does not use the writer because this method does some tuple
|
||||
-- shuffling.
|
||||
captureFinal :: ([String], (String, Command out))
|
||||
-> ([String], String, Command out)
|
||||
captureFinal (errs, (s, cmd)) = (errs', s, cmd)
|
||||
where
|
||||
errs' = errs ++ if not $ all Char.isSpace s
|
||||
then ["could not parse input at " ++ s]
|
||||
else []
|
||||
|
||||
-- main "interpreter" over the free monad. not implemented as an iteration
|
||||
-- because we switch to a different interpreter (and interpret the same
|
||||
-- stuff more than once) when processing flags.
|
||||
processMain :: CmdBuilder out ()
|
||||
-> MultiRWSS.MultiRWS '[] '[[String]] '[Command out, String] ()
|
||||
processMain = \case
|
||||
Pure x -> return x
|
||||
Free (CmdBuilderHelp h next) -> do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd { _cmd_help = Just h }
|
||||
processMain next
|
||||
f@(Free (CmdBuilderFlag{})) -> do
|
||||
flagData <- MultiRWSS.withMultiWriterW $ -- WriterS.execWriterT $
|
||||
iterM iterFlagGather f
|
||||
do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd { _cmd_flags = _cmd_flags cmd ++ flagData }
|
||||
parsedFlag <- MultiRWSS.withMultiStateS (Map.empty :: FlagParsedMap)
|
||||
$ parseFlags flagData
|
||||
(finalMap, fr) <- MultiRWSS.withMultiStateSA parsedFlag $ runParsedFlag f
|
||||
if Map.null finalMap
|
||||
then processMain fr
|
||||
else mTell ["internal error in application or colint library: inconsistent flag definitions."]
|
||||
Free (CmdBuilderParam s p next) -> do
|
||||
let param = processParam p
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd { _cmd_params = _cmd_params cmd ++ [ParamA s param] }
|
||||
str <- mGet
|
||||
x <- case (paramParse str, _param_def param) of
|
||||
(Nothing, Just x) -> do
|
||||
-- did not parse, use configured default value
|
||||
return $ x
|
||||
(Nothing, Nothing) -> do
|
||||
-- did not parse, no default value. add error, cont. with static default.
|
||||
mTell ["could not parse param at " ++ str]
|
||||
return paramStaticDef
|
||||
(Just (v, _, r), _) -> do
|
||||
-- parsed value; update the rest-string-to-parse, return value.
|
||||
mSet $ r
|
||||
return $ v
|
||||
processMain $ next $ x
|
||||
Free (CmdBuilderChild s c next) -> do
|
||||
dropSpaces
|
||||
str <- mGet
|
||||
let mRest = if
|
||||
| s == str -> Just ""
|
||||
| (s++" ") `isPrefixOf` str -> Just $ drop (length s + 1) str
|
||||
| otherwise -> Nothing
|
||||
case mRest of
|
||||
Nothing -> do
|
||||
cmd :: Command out <- mGet
|
||||
subCmd <- MultiRWSS.withMultiStateS emptyCommand
|
||||
$ iterM processCmdShallow c
|
||||
mSet $ cmd { _cmd_children = _cmd_children cmd ++ [(s, subCmd)] }
|
||||
processMain next
|
||||
Just rest -> do
|
||||
old :: Command out <- mGet
|
||||
mSet $ rest
|
||||
mSet $ emptyCommand {
|
||||
_cmd_mParent = Just (old, s)
|
||||
}
|
||||
processMain c
|
||||
Free (CmdBuilderRun o) -> cmd_run .=+ Just o
|
||||
|
||||
-- only captures some (i.e. roughly one layer) of the structure of
|
||||
-- the (remaining) builder, not parsing any input.
|
||||
processCmdShallow :: MonadMultiState (Command out) m
|
||||
=> CmdBuilderF out (m ())
|
||||
-> m ()
|
||||
processCmdShallow = \case
|
||||
CmdBuilderHelp h next -> do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd {
|
||||
_cmd_help = Just h
|
||||
}
|
||||
next
|
||||
CmdBuilderFlag _funique _shorts _longs _f next -> do
|
||||
next []
|
||||
CmdBuilderParam s p next -> do
|
||||
cmd :: Command out <- mGet
|
||||
mSet $ cmd {
|
||||
_cmd_params = _cmd_params cmd ++ [ParamA s $ processParam p]
|
||||
}
|
||||
next $ paramStaticDef
|
||||
CmdBuilderChild s _c next -> do
|
||||
cmd_children %=+ (++[(s, emptyCommand :: Command out)])
|
||||
next
|
||||
CmdBuilderRun _o ->
|
||||
return ()
|
||||
|
||||
-- extract a list of flag declarations. return [], i.e. pretend that no
|
||||
-- flag matches while doing so.
|
||||
iterFlagGather :: CmdBuilderF out (MultiRWSS.MultiRWS r ([Flag]':wr) s ())
|
||||
-> MultiRWSS.MultiRWS r ([Flag]':wr) s ()
|
||||
iterFlagGather = \case
|
||||
-- x | trace ("iterFlagGather: " ++ show (x $> ())) False -> error "laksjdlkja"
|
||||
CmdBuilderFlag funique shorts longs f next -> do
|
||||
let flag = processFlag funique shorts longs f
|
||||
mTell $ [flag]
|
||||
next []
|
||||
_ -> pure ()
|
||||
|
||||
-- the second iteration (technically not an iterM, but close..) over flags:
|
||||
-- use the parsed flag map, so that the actual flag (values) are captured
|
||||
-- in this run.
|
||||
-- return the final CmdBuilder when a non-flag is encountered.
|
||||
runParsedFlag :: CmdBuilder out ()
|
||||
-> MultiRWSS.MultiRWS '[] '[[String]] '[FlagParsedMap, Command out, String] (CmdBuilder out ())
|
||||
runParsedFlag = \case
|
||||
Free (CmdBuilderFlag funique _ _ f next) -> do
|
||||
m :: FlagParsedMap <- mGet
|
||||
let flagRawStrs = case Map.lookup funique m of
|
||||
Nothing -> []
|
||||
Just r -> r
|
||||
mSet $ Map.delete funique m
|
||||
runParsedFlag $ next $ reparseFlag f <$> flagRawStrs
|
||||
Pure x -> return $ return x
|
||||
f -> return f
|
||||
|
||||
reparseFlag :: FlagBuilder b -> FlagParsedElement -> b
|
||||
reparseFlag = undefined -- TODO FIXME WHO LEFT THIS HERE
|
||||
|
||||
parseFlags :: ( MonadMultiWriter [String] m
|
||||
, MonadMultiState String m
|
||||
, MonadMultiState FlagParsedMap m
|
||||
)
|
||||
=> [Flag]
|
||||
-> m ()
|
||||
parseFlags flags = do
|
||||
dropSpaces
|
||||
str <- mGet
|
||||
case str of
|
||||
('-':'-':longRest) ->
|
||||
case getAlt $ mconcat $ flags <&> \f
|
||||
-> mconcat $ _flag_long f <&> \l
|
||||
-> let len = length l
|
||||
in Alt $ do
|
||||
guard $ isPrefixOf l longRest
|
||||
r <- case List.drop len longRest of
|
||||
"" -> return ""
|
||||
(' ':r) -> return r
|
||||
_ -> mzero
|
||||
return $ (l, r, f) of
|
||||
Nothing -> mTell ["could not understand flag at --" ++ longRest]
|
||||
Just (flagStr, flagRest, flag) ->
|
||||
if length (_flag_params flag) /= 0
|
||||
then error "flag params not supported yet!"
|
||||
else do
|
||||
mSet flagRest
|
||||
mModify $ Map.insertWith (++)
|
||||
(_flag_unique flag)
|
||||
[FlagParsedElement [flagStr]]
|
||||
('-':shortRest) ->
|
||||
case shortRest of
|
||||
(c:' ':r) ->
|
||||
case getAlt $ mconcat $ flags <&> \f
|
||||
-> mconcat $ _flag_short f <&> \s
|
||||
-> Alt $ do
|
||||
guard $ c==s
|
||||
r' <- case r of
|
||||
(' ':r') -> return r'
|
||||
_ -> mzero
|
||||
return (c, r', f) of
|
||||
Nothing -> mTell ["could not understand flag at -" ++ shortRest]
|
||||
Just (flagChr, flagRest, flag) ->
|
||||
if length (_flag_params flag) /= 0
|
||||
then error "flag params not supported yet!"
|
||||
else do
|
||||
mSet flagRest
|
||||
mModify $ Map.insertWith (++)
|
||||
(_flag_unique flag)
|
||||
[FlagParsedElement ["-"++[flagChr]]]
|
||||
_ -> mTell ["could not parse flag at -" ++ shortRest]
|
||||
_ -> pure ()
|
||||
dropSpaces :: MonadMultiState String m => m ()
|
||||
dropSpaces = mModify $ dropWhile Char.isSpace
|
||||
processFlag :: Unique -> [Char] -> [String] -> FlagBuilder b -> Flag
|
||||
processFlag unique shorts longs flagBuilder
|
||||
= flip StateS.execState (Flag unique shorts longs Nothing [])
|
||||
$ iterM iterFuncFlag flagBuilder
|
||||
where
|
||||
iterFuncFlag :: FlagBuilderF ((StateS.State Flag) a)
|
||||
-> (StateS.State Flag) a
|
||||
iterFuncFlag = \case
|
||||
FlagBuilderHelp h next -> flag_help .= Just h >> next
|
||||
FlagBuilderParam s p next -> do
|
||||
let param = processParam p
|
||||
flag_params %= (++ [ParamA s param])
|
||||
next $ paramStaticDef
|
||||
processParam :: Show p => ParamBuilder p () -> Param p
|
||||
processParam paramBuilder = flip StateS.execState emptyParam
|
||||
$ runEitherT
|
||||
$ iterM iterFuncParam paramBuilder
|
||||
where
|
||||
iterFuncParam :: Show p
|
||||
=> ParamBuilderF p (EitherT (Maybe String) (StateS.State (Param p)) ())
|
||||
-> EitherT (Maybe String) (StateS.State (Param p)) ()
|
||||
iterFuncParam = \case
|
||||
ParamBuilderHelp h next -> do
|
||||
param <- State.Class.get
|
||||
case _param_help param of
|
||||
Nothing ->
|
||||
param_help .= Just h
|
||||
Just{} ->
|
||||
left $ Just $ "help is already defined when trying to add help \"" ++ h ++ "\""
|
||||
next
|
||||
ParamBuilderDef d next -> do
|
||||
param <- State.Class.get
|
||||
case _param_def param of
|
||||
Nothing ->
|
||||
param_def .= Just d
|
||||
Just{} ->
|
||||
left $ Just $ "default is already defined when trying to add default \"" ++ show d ++ "\""
|
||||
next
|
||||
|
||||
cmdActionPartial :: Command out -> Either String out
|
||||
cmdActionPartial = maybe (Left err) Right . _cmd_run
|
||||
where
|
||||
err = "command is missing implementation!"
|
||||
|
||||
cmdAction :: String -> CmdBuilder out () -> Either String out
|
||||
cmdAction s b = case cmdGetPartial s b of
|
||||
([], _, cmd) -> cmdActionPartial cmd
|
||||
((out:_), _, _) -> Left $ out
|
||||
|
||||
ppCommand :: Command out -> String
|
||||
ppCommand cmd
|
||||
= PP.render
|
||||
$ PP.vcat
|
||||
[ case _cmd_help cmd of
|
||||
Nothing -> PP.empty
|
||||
Just x -> PP.text x
|
||||
, case _cmd_children cmd of
|
||||
[] -> PP.empty
|
||||
cs -> PP.text "commands:" PP.$$ PP.nest 2 (PP.vcat $ commandShort <$> cs)
|
||||
, case _cmd_flags cmd of
|
||||
[] -> PP.empty
|
||||
fs -> PP.text "flags:" PP.$$ PP.nest 2 (PP.vcat $ flagShort <$> fs)
|
||||
]
|
||||
where
|
||||
commandShort :: (String, Command out) -> PP.Doc
|
||||
commandShort (s, c)
|
||||
= PP.text (s ++ ((_cmd_params c) >>= \(ParamA ps _) -> " " ++ ps))
|
||||
PP.<> case _cmd_help c of
|
||||
Nothing -> PP.empty
|
||||
Just h -> PP.text ":" PP.<+> PP.text h
|
||||
flagShort :: Flag -> PP.Doc
|
||||
flagShort f = PP.hsep (PP.text . ("-"++) . return <$> _flag_short f)
|
||||
PP.<+> PP.hsep (PP.text . ("--"++) <$> _flag_long f)
|
||||
PP.<+> case _flag_help f of
|
||||
Nothing -> PP.empty
|
||||
Just h -> PP.text h
|
||||
|
||||
ppCommandShort :: Command out -> String
|
||||
ppCommandShort cmd
|
||||
= PP.render
|
||||
$ printParent cmd
|
||||
PP.<+>
|
||||
case _cmd_flags cmd of
|
||||
[] -> PP.empty
|
||||
fs -> tooLongText 20 "[FLAGS]" $ List.unwords $ fs <&> \f ->
|
||||
"["
|
||||
++ (List.unwords $ (_flag_short f <&> \c -> ['-', c])
|
||||
++ (_flag_long f <&> \l -> "--" ++ l)
|
||||
)
|
||||
++ "]"
|
||||
PP.<+>
|
||||
case _cmd_params cmd of
|
||||
[] -> PP.empty
|
||||
ps -> PP.text $ List.unwords $ ps <&> \(ParamA s _) -> Char.toUpper <$> s
|
||||
PP.<+>
|
||||
case _cmd_children cmd of
|
||||
[] -> PP.empty
|
||||
cs -> PP.text
|
||||
$ if Maybe.isJust $ _cmd_run cmd
|
||||
then "[<" ++ intercalate "|" (fst <$> cs) ++ ">]"
|
||||
else "<" ++ intercalate "|" (fst <$> cs) ++ ">"
|
||||
where
|
||||
printParent :: Command out -> PP.Doc
|
||||
printParent c = case _cmd_mParent c of
|
||||
Nothing -> PP.empty
|
||||
Just (p, x) -> printParent p PP.<+> PP.text x
|
||||
|
||||
ppCommandShortHelp :: Command out -> String
|
||||
ppCommandShortHelp cmd
|
||||
= PP.render
|
||||
$ printParent cmd
|
||||
PP.<+>
|
||||
case _cmd_flags cmd of
|
||||
[] -> PP.empty
|
||||
fs -> tooLongText 20 "[FLAGS]" $ List.unwords $ fs <&> \f ->
|
||||
"["
|
||||
++ (List.unwords $ (_flag_short f <&> \c -> ['-', c])
|
||||
++ (_flag_long f <&> \l -> "--" ++ l)
|
||||
)
|
||||
++ "]"
|
||||
PP.<+>
|
||||
case _cmd_params cmd of
|
||||
[] -> PP.empty
|
||||
ps -> PP.text $ List.unwords $ ps <&> \(ParamA s _) -> Char.toUpper <$> s
|
||||
PP.<+>
|
||||
case _cmd_children cmd of
|
||||
[] -> PP.empty
|
||||
cs -> PP.text
|
||||
$ if Maybe.isJust $ _cmd_run cmd
|
||||
then "[<" ++ intercalate "|" (fst <$> cs) ++ ">]"
|
||||
else "<" ++ intercalate "|" (fst <$> cs) ++ ">"
|
||||
PP.<>
|
||||
case _cmd_help cmd of
|
||||
Nothing -> PP.empty
|
||||
Just h -> PP.text ":" PP.<+> PP.text h
|
||||
where
|
||||
printParent :: Command out -> PP.Doc
|
||||
printParent c = case _cmd_mParent c of
|
||||
Nothing -> PP.empty
|
||||
Just (p, x) -> printParent p PP.<+> PP.text x
|
||||
|
||||
tooLongText :: Int -- max length
|
||||
-> String -- alternative if actual length is bigger than max.
|
||||
-> String -- text to print, if length is fine.
|
||||
-> PP.Doc
|
||||
tooLongText i alt s = PP.text $ Bool.bool alt s $ null $ drop i s
|
||||
|
||||
cmds :: CmdBuilder (IO ()) ()
|
||||
cmds = do
|
||||
addCmd "echo" $ do
|
||||
help "print its parameter to output"
|
||||
str <- addParam "string" $ do
|
||||
help "the string to print"
|
||||
-- def "foo"
|
||||
impl $ do
|
||||
putStrLn str
|
||||
addCmd "hello" $ do
|
||||
help "prints some greeting"
|
||||
short <- flagAsBool $ addFlag "" ["short"] $ return ()
|
||||
name <- addParam "name" $ do
|
||||
help "your name, so you can be greeted properly"
|
||||
def "user"
|
||||
impl $ do
|
||||
if short
|
||||
then putStrLn $ "hi, " ++ name ++"!"
|
||||
else putStrLn $ "hello, " ++ name ++", welcome to colint!"
|
||||
|
||||
flagAsBool :: CmdBuilder m [a] -> CmdBuilder m Bool
|
||||
flagAsBool = fmap (not . null)
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
case cmdCheckNonStatic cmds of
|
||||
Just err -> do
|
||||
putStrLn "error building commands!!"
|
||||
putStrLn err
|
||||
Nothing -> do
|
||||
forever $ do
|
||||
putStr "> "
|
||||
hFlush stdout
|
||||
input <- System.IO.getLine
|
||||
let (errs, _, partial) = cmdGetPartial input cmds
|
||||
print partial
|
||||
putStrLn $ ppCommand $ partial
|
||||
case (errs, cmdActionPartial partial) of
|
||||
(err:_, _) -> print err
|
||||
([], eEff) -> case eEff of
|
||||
Left err -> do
|
||||
putStrLn $ "could not interpret input: " ++ err
|
||||
Right eff -> do
|
||||
eff
|
||||
|
||||
-- ----
|
||||
|
||||
instance IsParam String where
|
||||
paramParse s = do
|
||||
let s1 = dropWhile Char.isSpace s
|
||||
let (param, rest) = List.span (not . Char.isSpace) s1
|
||||
guard $ not $ null param
|
||||
pure $ (param, param, rest) -- we remove trailing whitespace, evil as we are.
|
||||
paramStaticDef = ""
|
||||
|
||||
instance IsParam () where
|
||||
paramParse s = do
|
||||
let s1 = dropWhile Char.isSpace s
|
||||
rest <- List.stripPrefix "()" s1
|
||||
pure $ ((), "()", rest)
|
||||
paramStaticDef = ()
|
|
@ -1,158 +0,0 @@
|
|||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MonadComprehensions #-}
|
||||
|
||||
module UI.CmdParse.Monadic.Types
|
||||
( Command(..)
|
||||
, cmd_mParent
|
||||
, cmd_help
|
||||
, cmd_flags
|
||||
, cmd_params
|
||||
, cmd_children
|
||||
, cmd_run
|
||||
, flag_help
|
||||
, flag_long
|
||||
, flag_params
|
||||
, flag_unique
|
||||
, flag_short
|
||||
, param_def
|
||||
, param_help
|
||||
, emptyCommand
|
||||
, emptyParam
|
||||
, FlagParsedMap
|
||||
, FlagParsedElement(..)
|
||||
, IsParam(..)
|
||||
, IsHelpBuilder(..)
|
||||
, CmdBuilderF(..)
|
||||
, CmdBuilder
|
||||
, ParamBuilderF(..)
|
||||
, ParamBuilder
|
||||
, FlagBuilderF(..)
|
||||
, FlagBuilder
|
||||
, Flag(..)
|
||||
, Param(..)
|
||||
, ParamA(..)
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
|
||||
#include "qprelude/bundle-gamma.inc"
|
||||
import Control.Monad.Free
|
||||
import qualified Control.Monad.Trans.MultiState.Strict as MultiStateS
|
||||
import Data.Unique (Unique)
|
||||
import qualified System.Unsafe as Unsafe
|
||||
|
||||
import qualified Control.Lens.TH as LensTH
|
||||
import qualified Control.Lens as Lens
|
||||
|
||||
|
||||
|
||||
data Command out = Command
|
||||
{ _cmd_mParent :: Maybe (Command out, String) -- parent command
|
||||
-- , substring that leads to $this.
|
||||
-- (kinda wonky, i know.)
|
||||
, _cmd_help :: Maybe String
|
||||
, _cmd_flags :: [Flag]
|
||||
, _cmd_params :: [ParamA]
|
||||
, _cmd_children :: [(String, Command out)]
|
||||
, _cmd_run :: Maybe out
|
||||
}
|
||||
|
||||
emptyCommand :: Command out
|
||||
emptyCommand = Command Nothing Nothing [] [] [] Nothing
|
||||
|
||||
instance Show (Command out) where
|
||||
show c = "Command help=" ++ show (_cmd_help c)
|
||||
++ " flags=" ++ show (_cmd_flags c)
|
||||
++ " params=" ++ show (_cmd_params c)
|
||||
++ " children=" ++ show (_cmd_children c)
|
||||
++ " run=" ++ case _cmd_run c of Nothing -> "Nothing"; Just{} -> "Just{..}"
|
||||
|
||||
-- class IsFlag a where
|
||||
-- flagParse :: String -> Maybe (a, String)
|
||||
-- staticDef :: a
|
||||
|
||||
data Flag = Flag
|
||||
{ _flag_unique :: Unique
|
||||
, _flag_short :: String
|
||||
, _flag_long :: [String]
|
||||
, _flag_help :: Maybe String
|
||||
, _flag_params :: [ParamA]
|
||||
}
|
||||
|
||||
instance Show Flag where
|
||||
show (Flag _ short long helpM params) = show (short, long, helpM, params) -- TODO: improve
|
||||
|
||||
type FlagParsedMap = Map Unique [FlagParsedElement]
|
||||
|
||||
data FlagParsedElement = FlagParsedElement [String]
|
||||
deriving Show
|
||||
|
||||
data ParamA = forall p . (IsParam p, Show p) => ParamA String (Param p)
|
||||
|
||||
deriving instance Show ParamA
|
||||
|
||||
class IsParam a where
|
||||
paramParse :: String -> Maybe (a, String, String) -- value, representation, rest
|
||||
paramStaticDef :: a
|
||||
|
||||
data Param a = Param
|
||||
{ _param_help :: Maybe String
|
||||
, _param_def :: Maybe a
|
||||
}
|
||||
|
||||
emptyParam :: Param a
|
||||
emptyParam = Param Nothing Nothing
|
||||
|
||||
deriving instance Show a => Show (Param a)
|
||||
|
||||
data CmdBuilderF out a
|
||||
= CmdBuilderHelp String a
|
||||
| forall b . CmdBuilderFlag Unique String [String] (FlagBuilder b) ([b] -> a)
|
||||
| forall p . (Show p, IsParam p) => CmdBuilderParam String (ParamBuilder p ()) (p -> a)
|
||||
| CmdBuilderChild String (CmdBuilder out ()) a
|
||||
| CmdBuilderRun out -- TODO: why do we "abort" here? (i.e. no `a`)
|
||||
-- this is not actually enforced when writing
|
||||
-- CmdBuilders, is it? if it is not, this would result
|
||||
-- in rather nasty silent-ignoring.
|
||||
|
||||
deriving instance Functor (CmdBuilderF out)
|
||||
|
||||
instance Show a => Show (CmdBuilderF out a) where
|
||||
show (CmdBuilderHelp s x) = "(CmdBuilderHelp " ++ show s ++ " " ++ show x ++ ")"
|
||||
show (CmdBuilderFlag _ shorts longs _ _) = "(CmdBuilderFlag -" ++ shorts ++ " " ++ show longs ++ ")"
|
||||
show (CmdBuilderParam s _ _) = "(CmdBuilderParam " ++ s ++ ")"
|
||||
show (CmdBuilderChild s _ _) = "(CmdBuilderChild " ++ s ++ ")"
|
||||
show (CmdBuilderRun _) = "CmdBuilderRun"
|
||||
|
||||
type CmdBuilder out = Free (CmdBuilderF out)
|
||||
|
||||
data FlagBuilderF a
|
||||
= FlagBuilderHelp String a
|
||||
| forall p . (Show p, IsParam p) => FlagBuilderParam String (ParamBuilder p ()) (p -> a)
|
||||
|
||||
deriving instance Functor FlagBuilderF
|
||||
|
||||
type FlagBuilder = Free FlagBuilderF
|
||||
|
||||
data ParamBuilderF p a
|
||||
= ParamBuilderHelp String a
|
||||
| ParamBuilderDef p a
|
||||
|
||||
deriving instance Functor (ParamBuilderF p)
|
||||
|
||||
type ParamBuilder p = Free (ParamBuilderF p)
|
||||
|
||||
class IsHelpBuilder m where
|
||||
help :: String -> m ()
|
||||
|
||||
Lens.makeLenses ''Command
|
||||
Lens.makeLenses ''Flag
|
||||
Lens.makeLenses ''Param
|
Loading…
Reference in New Issue