mangrove-cli 0.2.0.0 → 0.3.0.0
raw patch · 12 files changed
+399/−696 lines, 12 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Mangrove: [Help] :: forall (s :: Type -> Type) r. SupportsHelp s => !Text -> Result s r
- Mangrove: class (Scheme s, HelpSupport s ~ 'Helpful) => SupportsHelp (s :: Type -> Type)
- Mangrove: instance GHC.Show.Show Mangrove.ProgramInfo
- Mangrove: type HelpHandler (s :: Type -> Type) r = HelpContinuation HelpSupport s s r
- Mangrove.Parser: -- It is <a>Silent</a> by default, but must be set to <a>Helpful</a> if
- Mangrove.Parser: -- the scheme will implement an instance of <a>SupportsHelp</a>.
- Mangrove.Parser: Helpful :: HelpCapability
- Mangrove.Parser: Silent :: HelpCapability
- Mangrove.Parser: [onHelpRequest] :: StreamHandler (s :: Type -> Type) a r -> HelpHandler s r
- Mangrove.Parser: class (Scheme s, HelpSupport s ~ 'Helpful) => SupportsHelp (s :: Type -> Type)
- Mangrove.Parser: data HelpCapability
- Mangrove.Parser: instance (Mangrove.Separable.Separable s, Mangrove.Valency.Valency s) => Mangrove.Separable.Separable (Mangrove.Parser.ParseTree s)
- Mangrove.Parser: instance GHC.Base.Functor (Mangrove.Parser.HelpContinuation 'Mangrove.Parser.Helpful s)
- Mangrove.Parser: instance GHC.Base.Functor (Mangrove.Parser.HelpContinuation 'Mangrove.Parser.Silent s)
- Mangrove.Parser: requestHelp :: forall (s :: Type -> Type) a. HelpSupport s ~ 'Helpful => StreamParser s a
- Mangrove.Parser: type HelpHandler (s :: Type -> Type) r = HelpContinuation HelpSupport s s r
- Mangrove.Parser: type HelpSupport s :: Type -> Type = 'Silent;
- Mangrove.Scheme.Sub: instance Mangrove.Separable.Separable Mangrove.Scheme.Sub.SubScheme
- Mangrove.Scheme.Unix: HelpOption :: !OptionInfo -> UnixScheme r
- Mangrove.Scheme.Unix: instance Mangrove.Parser.SupportsHelp Mangrove.Scheme.Unix.UnixScheme
- Mangrove.Scheme.Unix: instance Mangrove.Separable.Separable Mangrove.Scheme.Unix.UnixScheme
- Mangrove.Separable: Exhibit :: Maybe a -> [Modal a] -> Exhibit a
- Mangrove.Separable: Modal :: !Bool -> a -> Modal a
- Mangrove.Separable: class Functor s => Separable (s :: Type -> Type)
- Mangrove.Separable: data Exhibit a
- Mangrove.Separable: data Modal a
- Mangrove.Separable: exhibitToList :: Exhibit a -> [a]
- Mangrove.Separable: instance GHC.Base.Applicative Mangrove.Separable.Modal
- Mangrove.Separable: instance GHC.Base.Functor Mangrove.Separable.Exhibit
- Mangrove.Separable: instance GHC.Base.Functor Mangrove.Separable.Modal
- Mangrove.Separable: separate :: Separable s => s r -> Exhibit (s r)
- Mangrove.Separable: usesTerseOutput :: Modal a -> Bool
+ Mangrove: HelpRequest :: RequestType
+ Mangrove: VersionRequest :: RequestType
+ Mangrove: [Response] :: forall (s :: Type -> Type) r. SupportsResponse s => !Text -> Result s r
+ Mangrove: [programVersion] :: ProgramInfo (s :: Type -> Type) -> !Version
+ Mangrove: class (Scheme s, RequestSupport s ~ 'True) => SupportsResponse (s :: Type -> Type)
+ Mangrove: data RequestType
+ Mangrove: type RequestHandler (s :: Type -> Type) r = ReqContinuation RequestSupport s s r
+ Mangrove.Parser: -- <a>SupportsResponse</a> instance should be provided for
+ Mangrove.Parser: -- <tt>scheme</tt>.
+ Mangrove.Parser: -- When <tt>RequestSupport scheme</tt> is <tt>True</tt>, a
+ Mangrove.Parser: -- information.
+ Mangrove.Parser: HelpRequest :: RequestType
+ Mangrove.Parser: ProgramInfo :: !Text -> !Version -> !Text -> ProgramInfo (s :: Type -> Type)
+ Mangrove.Parser: VersionRequest :: RequestType
+ Mangrove.Parser: [onRequest] :: StreamHandler (s :: Type -> Type) a r -> RequestHandler s r
+ Mangrove.Parser: [programDesc] :: ProgramInfo (s :: Type -> Type) -> !Text
+ Mangrove.Parser: [programName] :: ProgramInfo (s :: Type -> Type) -> !Text
+ Mangrove.Parser: [programVersion] :: ProgramInfo (s :: Type -> Type) -> !Version
+ Mangrove.Parser: class (Scheme s, RequestSupport s ~ 'True) => SupportsResponse (s :: Type -> Type)
+ Mangrove.Parser: data ProgramInfo (s :: Type -> Type)
+ Mangrove.Parser: data RequestType
+ Mangrove.Parser: instance GHC.Base.Functor (Mangrove.Parser.ReqContinuation 'GHC.Types.False s)
+ Mangrove.Parser: instance GHC.Base.Functor (Mangrove.Parser.ReqContinuation 'GHC.Types.True s)
+ Mangrove.Parser: instance GHC.Classes.Eq Mangrove.Parser.RequestType
+ Mangrove.Parser: instance GHC.Show.Show (Mangrove.Parser.ProgramInfo s)
+ Mangrove.Parser: instance GHC.Show.Show Mangrove.Parser.RequestType
+ Mangrove.Parser: makeVersionInfo :: SupportsResponse s => ProgramInfo s -> Text
+ Mangrove.Parser: request :: forall (s :: Type -> Type) a. RequestSupport s ~ 'True => RequestType -> StreamParser s a
+ Mangrove.Parser: type RequestHandler (s :: Type -> Type) r = ReqContinuation RequestSupport s s r
+ Mangrove.Parser: type RequestSupport s :: Type -> Type = 'False;
+ Mangrove.Scheme.Unix: RequestOption :: !OptionInfo -> !RequestType -> UnixScheme r
+ Mangrove.Scheme.Unix: instance Mangrove.Parser.SupportsResponse Mangrove.Scheme.Unix.UnixScheme
+ Mangrove.TextParser: instance Mangrove.TextParser.DefaultParser Data.Text.Internal.Builder.Builder
+ Mangrove.TextParser: instance Mangrove.TextParser.DefaultParser Data.Text.Internal.Lazy.Text
+ Mangrove.TextParser: parseBool :: TextParser Bool
+ Mangrove.TextParser: parseChar :: TextParser Char
+ Mangrove.TextParser: parseDouble :: TextParser Double
+ Mangrove.TextParser: parseFloat :: TextParser Float
+ Mangrove.TextParser: parseInt :: TextParser Int
+ Mangrove.TextParser: parseInteger :: TextParser Integer
+ Mangrove.TextParser: parseLazyText :: TextParser Text
+ Mangrove.TextParser: parseLazyTextBuilder :: TextParser Builder
+ Mangrove.TextParser: parseString :: TextParser String
+ Mangrove.TextParser: parseText :: TextParser Text
+ Mangrove.TextParser: parseWord :: TextParser Word
+ Mangrove.Unix: requestOption :: NonEmpty Flag -> Text -> RequestType -> UnixParser a
- Mangrove: ProgramInfo :: !Text -> !Text -> ProgramInfo
+ Mangrove: ProgramInfo :: !Text -> !Version -> !Text -> ProgramInfo (s :: Type -> Type)
- Mangrove: [programDesc] :: ProgramInfo -> !Text
+ Mangrove: [programDesc] :: ProgramInfo (s :: Type -> Type) -> !Text
- Mangrove: [programName] :: ProgramInfo -> !Text
+ Mangrove: [programName] :: ProgramInfo (s :: Type -> Type) -> !Text
- Mangrove: data ProgramInfo
+ Mangrove: data ProgramInfo (s :: Type -> Type)
- Mangrove: data family HelpContinuation (cap :: HelpCapability) (s :: Type -> Type) r
+ Mangrove: data family ReqContinuation (cap :: Bool) (s :: Type -> Type) r
- Mangrove: parseArguments :: forall (s :: Type -> Type) r a. SupportsHelp s => ProgramInfo -> ParseTree s r -> (r -> IO a) -> IO a
+ Mangrove: parseArguments :: forall (s :: Type -> Type) r a. SupportsResponse s => ProgramInfo s -> ParseTree s r -> (r -> IO a) -> IO a
- Mangrove: runArgumentParser :: forall (s :: Type -> Type) r a. Scheme s => ParseTree s r -> [Text] -> ([Text] -> r -> a) -> (Text -> a) -> HelpHandler s a -> a
+ Mangrove: runArgumentParser :: forall (s :: Type -> Type) r a. Scheme s => ParseTree s r -> [Text] -> ([Text] -> r -> a) -> (Text -> a) -> RequestHandler s a -> a
- Mangrove: runArgumentParser' :: forall (s :: Type -> Type) r a. Scheme s => ParseTree s r -> StreamState s -> ([Text] -> r -> a) -> (Text -> a) -> HelpHandler s a -> a
+ Mangrove: runArgumentParser' :: forall (s :: Type -> Type) r a. Scheme s => ParseTree s r -> StreamState s -> ([Text] -> r -> a) -> (Text -> a) -> RequestHandler s a -> a
- Mangrove: runHelpfulParser :: forall (s :: Type -> Type) r. SupportsHelp s => ProgramInfo -> ParseTree s r -> [Text] -> Result s r
+ Mangrove: runHelpfulParser :: forall (s :: Type -> Type) r. SupportsResponse s => ProgramInfo s -> ParseTree s r -> [Text] -> Result s r
- Mangrove: runHelpfulParser' :: forall (s :: Type -> Type) r. SupportsHelp s => ProgramInfo -> ParseTree s r -> StreamState s -> Result s r
+ Mangrove: runHelpfulParser' :: forall (s :: Type -> Type) r. SupportsResponse s => ProgramInfo s -> ParseTree s r -> StreamState s -> Result s r
- Mangrove: runHelpfulParser_ :: forall (s :: Type -> Type) r. SupportsHelp s => ParseTree s r -> [Text] -> Result s r
+ Mangrove: runHelpfulParser_ :: forall (s :: Type -> Type) r. SupportsResponse s => ParseTree s r -> [Text] -> Result s r
- Mangrove: runSilentParser :: forall (s :: Type -> Type) r. (Scheme s, HelpSupport s ~ 'Silent) => ParseTree s r -> [Text] -> Result s r
+ Mangrove: runSilentParser :: forall (s :: Type -> Type) r. (Scheme s, RequestSupport s ~ 'False) => ParseTree s r -> [Text] -> Result s r
- Mangrove: runSilentParser' :: forall (s :: Type -> Type) r. (Scheme s, HelpSupport s ~ 'Silent) => ParseTree s r -> StreamState s -> Result s r
+ Mangrove: runSilentParser' :: forall (s :: Type -> Type) r. (Scheme s, RequestSupport s ~ 'False) => ParseTree s r -> StreamState s -> Result s r
- Mangrove.Parser: -- | This type indicates whether a parsing scheme supports help output.
+ Mangrove.Parser: -- | This type indicates whether a parsing scheme accepts requests for
- Mangrove.Parser: StreamHandler :: (StreamState s -> a -> r) -> (StreamState s -> r) -> (StreamState s -> Builder -> r) -> HelpHandler s r -> StreamHandler (s :: Type -> Type) a r
+ Mangrove.Parser: StreamHandler :: (StreamState s -> a -> r) -> (StreamState s -> r) -> (StreamState s -> Builder -> r) -> RequestHandler s r -> StreamHandler (s :: Type -> Type) a r
- Mangrove.Parser: data family HelpContinuation (cap :: HelpCapability) (s :: Type -> Type) r
+ Mangrove.Parser: data family ReqContinuation (cap :: Bool) (s :: Type -> Type) r
- Mangrove.Parser: makeHelpInfo :: SupportsHelp s => ParseTree s r -> [Token s] -> Text -> Text -> Text
+ Mangrove.Parser: makeHelpInfo :: SupportsResponse s => ParseTree s r -> [Token s] -> ProgramInfo s -> Text
Files
- CHANGELOG.md +31/−0
- README.md +11/−426
- mangrove-cli.cabal +1/−2
- src/Mangrove.hs +39/−40
- src/Mangrove/Parser.hs +52/−70
- src/Mangrove/Scheme/Sub.hs +0/−4
- src/Mangrove/Scheme/Unix.hs +115/−30
- src/Mangrove/Separable.hs +0/−67
- src/Mangrove/TextParser.hs +120/−46
- src/Mangrove/Unix.hs +15/−2
- test/General.hs +12/−6
- test/Mangrove/ParserSpec.hs +3/−3
CHANGELOG.md view
@@ -8,6 +8,37 @@ ## Unreleased +## 0.3.0.0 - 2026-08-27++### Added++- Request options that trigger a request when matched+- Literate Haskell mkuser tutorial that demonstrates how to build+ parsers for options and parameters+- Literate Haskell pkgtool tutorial that demonstrates how to build+ command parsers+- Individual text parsers for common data types++### Changed++- Generalize help requests to help or version requests+- Rewrite mkuser tutorial as a Literate Haskell program+- Add version information to ProgramInfo+- Move tutorials from the inside the README to dedicated pages on the+ GitHub wiki which are built from Literate Haskell files in the `doc`+ directory++### Fixed++- Include commands with no options in help output+- Factor out duplicate code in Mangrove.Unix.optionPure++### Removed++- Separable module and Separable typeclass+- Exhibit type+- Modal type+ ## 0.2.0.0 - 2026-08-14 ### Added
README.md view
@@ -10,6 +10,17 @@ `--mount src=/webroot,dst=/var/www,rw`). It is also extensible, so you can define alternative command line syntaxes. +## Documentation++The API documentation is available on Hackage:+<https://hackage.haskell.org/package/mangrove-cli>++There is a tutorial to help with getting started:+<https://github.com/quytelda/mangrove/wiki/Tutorial>++Commands are covered in a separate tutorial:+<https://github.com/quytelda/mangrove/wiki/Commands>+ ## Obtaining Mangrove is available on Hackage as `mangrove-cli`:@@ -45,429 +56,3 @@ - Profiling & optimization Once these are addressed, a 1.0.0 release will be appropriate.--# Tutorial--## A Metaphor--Imagine the roots of a plant branching out like a tree as they-descend. Eventually, they dip into a stream. The roots collect water-and nutrients from the flowing stream. These resources travel back up-the structure toward the plant, combining along the way.--This is kind of like how the Mangrove library works. We build a-tree-shaped parser from simple applicative combinators, then feed it a-sequence of CLI arguments. Simple parsers stationed at the bottom of-the tree consume these arguments and produce values which are then-passed back up the tree and combined with the results of other parsers-until a final result is reached.--## Example--__NOTE__: See the full example file in `doc/MkUser.hs`.--Suppose we are writing a simple program that creates new user-accounts - we'll call it "mkuser". The goal will be to provide a-command line interface with the following syntax:--```-mkuser [--uid=INT] [--system] [--groups={GROUP...}] USERNAME-```--First, let's create a new record that captures the program's runtime-configuration.--```haskell-data Settings = Settings- { userId :: Maybe Int -- ^ An optional target user ID- , userSystem :: Bool -- ^ Is this a system user?- , userGroups :: [Text] -- ^ Groups the new user will be in- , userName :: Text -- ^ Username for the new user- } deriving (Show)-```--Let's also pretend that our program's logic lives inside a function-`run :: Settings -> IO ()`. We pass it the settings we want, and it-runs the program accordingly. However, since this is just an example-program, we won't actually create any user accounts; instead we'll-just have the program print its settings to `stdout`.--```haskell-run :: Settings -> IO ()-run = print-```--Now we need to construct a parser that reads a list of arguments and-yields a `Settings`. Our parser will have the type `UnixParser-Settings`.--__NOTE__: This example uses the language extensions `OverloadedLists`-and `OverloadedStrings` since we need to write lots of `NonEmpty` list-and `Text` literals.--__NOTE__: `UnixParser` is just a convenient type synonym for-`ParseTree UnixScheme`. This tells us that we will build a `ParseTree`-by combining parsers from the UNIX scheme.--## Positional Parameters--A "positional parameter" is a positional input that accepts the first-non-flag argument it encounters. In Mangrove, positional parameters-are usually just referred to as "parameters" since other kinds of-parameters have their own names. Consider an example program called-`substring` whose command line syntax is `substring START END STRING`.-`START`, `END`, and `STRING` would be parameters. If we invoke-`substring 1 3 "example"`, we know that `START` is `1`, `END` is `3`,-and `STRING` is `"example"` because of the order in which they appear.--Our program will have just one parameter: a username. Here is how we-define a parser for it:--```haskell-prm_name :: UnixParser Text-prm_name = parameter defaultParser-```--The `parameter` function creates a parameter parser out of a-`TextParser`.--### TextParsers--A `TextParser r` is just a wrapper around a function that parses-`Text` into a value of type `r`. It also contains a "hint" string used-for displaying usage information.--Many common data types have a reasonable default `TextParser`-implementation. Types that are instances of the `DefaultParser` class-implement `defaultParser :: DefaultParser a => TextParser a`, letting-us automatically select the correct parser based on the required type.--In the example above, `Text` has a very simple `DefaultParser`-instance that just returns its input unchanged.--## Options--An "option" is a construct representing a named input. Options begin-with a flag followed by an optional subargument string.--A "flag" is special symbol that signals the beginning of a particular-option. Per UNIX tradition there are long flags (e.g. `--foo`) and-short flags (e.g `-f`).--To prevent ambiguity, sometimes an equals sign is used to separate a-long flag from its subargument string (instead of a space). For-example, `--uid=1000` is an option that begins with the `--uid` flag-and is followed by the subargument string `1000`. Similarly, an-option's short flag can be directly concatenated with its argument,-e.g. `-u 1000` can be written `-u1000`.--Let's define a parser for the `--uid` option:--```haskell-opt_uid :: UnixParser Int-opt_uid = option ["--uid", "-u"]- "Specify a user ID"- $ subparameter defaultParser-```--The `option` function creates a parser for CLI options. It takes three-arguments:--1. A `NonEmpty` list of `Flag`s that trigger the option, in this case- "--uid" and "-u". `Flag` is an instance of `IsString`, so we can- just write the string representation instead of `LongFlag "uid"`- and `ShortFlag 'u'`.-2. A human readable description. This will be displayed when help- output is triggered.-3. A subparser tree (`SubParser r`) that will parse any subparameters or- suboptions. In this case, we declare a single subparameter (an- integer).--The `subparameter` function behaves just like `parameter` from-earlier, except it creates a `SubParser` instead of a `UnixParser`. We-also use `defaultParser` to automatically select an appropriate-`TextParser` for `Int`.--__NOTE__: `SubParser` is a type synonym for `ParseTree SubScheme`.-That means we build a `SubParser` by combining `SubScheme` parsers.-`SubScheme` provides parsers for handling subarguments to options.--You might notice that our `Settings` record requires a `Maybe Int`,-not an `Int`. However, since `UnixParser` is an instance of-`Alternative`, we can use `optional` from `Control.Applicative`.-`optional opt_uid :: UnixParser (Maybe Int)` describes an option that-is not required and might be absent (which should give us `Nothing`).--### Switches--The `--system` option is simpler because because it doesn't accept any-subarguments - it is either present (`True`) or absent (`False`). This-special type of option is a "switch", and we can use the `switch`-function to create a parser:--```haskell-opt_system :: UnixParser Bool-opt_system = switch ["--system", "-s"] "Create a system user"---- If we defined this without 'switch' it would look like this:--- opt_system = option ["--system", "-s"]--- "Create a system user"--- (pure True)--- <|> pure False--```--### Options with Multiple Subparameters--Let's deal with the `--groups` option. This option is a bit different-from the `--uid` option because we want the user to be able to specify-a list of groups for the new user to join. Thus, we want to create an-option that accepts one or more subarguments.--Thankfully, `SubParser` is also an `Alternative` instance. We can use-`some` (from `Control.Applicative`) to convert a `SubParser r` into a-`SubParser [r]` that will expect to parse one or more `r` values.--```haskell-opt_groups :: UnixParser [Text]-opt_groups =- option ["--groups", "-g"]- "Specify what groups the user is part of"- $ some $ subparameter defaultParser-```--Mangrove recognizes that the subparser `some $ subparameter-defaultParser :: SubParser [Text]` can consume multiple subarguments,-so it splits those subarguments apart by comma. This allows us to pass-a list of group names like so: `--groups=wheel,audio,input`, and the-parser will yield `["wheel","audio","input"]`.--By using `some` instead of the similar function `many`, we have-created a subparser that will fail if no subarguments are provided-(e.g. `mkuser alice --groups`).--What if the `--groups` option isn't present at all? We still need a-`[Text]` value for our `Settings` record. In that case, an empty list-makes sense. Just like with our `--uid` option, we use `Alternative`-to define what happens if our parser never finds applicable input.--```-opt_groups <|> pure [] :: UnixParser [Text]-```--__NOTE__: There is an important distinction between a parser that-never finds relevant input and a parser that fails. In an expression-like `opt_groups <|> pure []`, if `opt_groups` never finds relevant-input, the alternative provides a default value. However, if-`opt_groups` *does* find applicable input, but parsing it fails, an-error will be thrown instead.--More generally, if `p` and `q` are parsers, then `p <|> q` is a parser-that yields the result from whichever parser finds applicable input-first. If neither parser finds input, we first try resolving `p` and-then `q` with no input and yield the first result we get. If neither-succeeds, we throw an error.--## Applicative--We are now ready to construct our `Settings` parser using `<$>` and-`<*>`:--```haskell-parseSettings :: UnixParser Settings-parseSettings =- Settings- <$> optional opt_uid- <*> opt_system- <*> (opt_groups <|> pure [])- <*> prm_name-```--Now we can inspect the automatically generated usage information for-our parser in GHCi using `render` from `Mangrove.Text`:--```-ghci> render parseSettings-"[--uid=INT] [--system] [--groups={STRING...}] STRING"-```--This output indicates that our parser accepts (but does not require) a-`--uid` option with an integer subargument, a `--system` option, and a-`--groups` option with a list of string subarguments. Finally, it-requires a single parameter, which is a string. We'll see how to-improve those type hints later.--## Program Metadata--The last thing we need to define before running our parser is a-structure with some metadata about the program:--```haskell-programInfo :: ProgramInfo-programInfo = ProgramInfo- { programName = "mkuser" -- The name of the program- , programDesc = "Create user accounts" -- A short description of the program- }-```--This information is used to display nice, human-readable help and-usage information, which will is discussed in more detail in the [Help-Options](#help-options) section.--## Running the Parser--The `parseArguments` function will run our parser with the arguments-passed to our program by the operating system.--```haskell-main :: IO ()-main = parseArguments programInfo parseSettings run-```--`parseArguments` takes three arguments: the program metadata (for help-output), a `UnixParser r`, and a function of type `r -> IO a`. When-the parser completes successfully, this function will be called with-the result. Otherwise, `parseArguments` will print error messages or-help information as appropriate, and then exit.--If you want to run an argument parser without using `IO`, or you want-to pass your own argument list, check out `runHelpfulParser` from-`Mangrove`.--Now we have a complete program we can build and run to show the-argument parser in action!--```-$ ghc -o mkuser MkUser.hs-[1 of 2] Compiling Main ( MkUser.hs, MkUser.o )-[2 of 2] Linking mkuser--$ ./mkuser --system --groups audio,input bilbo-Settings {userId = Nothing, userSystem = True, userGroups = ["audio","input"], userName = "bilbo"}--$ ./mkuser --badinput-unexpected --badinput--$ ./mkuser --system-expected: STRING--$ ./mkuser --uid=InvalidNumber bilbo---uid=InvalidNumber: InvalidNumber: input does not start with a digit-```--## Help Options--Currently, our CLI interface is missing something important: an option-for displaying help and usage information. Let's create a new-`Settings` parser that recognizes `--help` as a request for help-information.--```haskell-parseSettings' :: UnixParser Settings-parseSettings' = addHelpOptions ["--help"]- "Display help and usage information"- parseSettings--main :: IO ()-main = parseArguments programInfo parseSettings' run-```--Now if we invoke our program with the `--help` option, it will display-a nice summary of how to use it:--```-./mkuser --help-Usage:-mkuser [--uid=INT] [--system] [--groups={STRING...}] STRING-mkuser --help--Create user accounts-- --help Display help and usage information--g --groups {STRING...} Specify what groups the user is part of--s --system Create a system user--u --uid INT Specify a user ID-```--__NOTE__: If an interface defines any commands (see below),-`addHelpOptions` will add a help option at the root of the parse tree-as well as the root of every command subtree. This is so that you can-invoke `myprogram --help` to get general help or `myprogram-somecommand --help` to get help information specifically for-`somecommand`.--## Hints--Type hints are displayed as placeholders for parameters in help and-usage information. They are a hint to the user about what kind of-information is expected by that input. For example, `--uid=INT`-indicates the `--uid` option expects an integer as a subargument.-Hints stored inside the `parserHint` field of a `TextParser`.--Our program uses the generic hints defined in the `DefaultParser`-instances for `Int` and `Text`. These defaults are often reasonable,-but we can also tailor hints more specifically for our use case. All-we need to do is alter the value of `parserHint` for the relevant-`TextParser`.--```-opt_groups :: UnixParser [Text]-opt_groups =- option ["--groups", "-g"]- "Specify what groups the user is part of"- $ some $ subparameter defaultParser {parserHint = "GROUP"}--prm_name :: UnixParser Text-prm_name = parameter defaultParser {parserHint = "USERNAME"}-```--Now our help output looks like this:--```-$ ./mkuser --help-Usage:-mkuser [--uid=INT] [--system] [--groups={GROUP...}] USERNAME-mkuser --help--Create user accounts-- --help Display help and usage information--g --groups {GROUP...} Specify what groups the user is part of--s --system Create a system user--u --uid INT Specify a user ID-```--## Commands--A "command" is a special argument changes the context of a parser.-When a command is encountered, the parser begins using the parse tree-associated with that command as a new context until it completes.-Commands are usually used as a way to invoke different modes of-functionality for a single program. For example, `git` supports-various commands like `commit` or `pull`.--Suppose we are creating a basic version control system similar to-`git`. Our program will have several runtime modes for doing-operations like `commit` or `pull`. Here is how we might define a-parser that recognizes the corresponding commands (for the full code,-see `doc/VersionControl.hs`):--```haskell-data Mode- = CommitMode CommitSettings- | PullMode PullSettings- -- ... and probably other modes too- deriving (Show)--parseMode :: UnixParser Mode-parseMode = cmd_commit <|> cmd_pull- where- cmd_commit =- command ["commit"]- "Make a new commit"- $ CommitMode <$> parseCommitSettings- cmd_pull =- command ["pull"]- "Download remote changes"- $ PullMode <$> parsePullSettings-```
mangrove-cli.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: mangrove-cli-version: 0.2.0.0+version: 0.3.0.0 synopsis: Build CLI argument parsers using Applicative. description: Please see the README on GitHub at <https://github.com/quytelda/mangrove#readme> category: CLI, Options, Parsing@@ -32,7 +32,6 @@ Mangrove.Resolve Mangrove.Scheme.Sub Mangrove.Scheme.Unix- Mangrove.Separable Mangrove.Text Mangrove.TextParser Mangrove.Unix
src/Mangrove.hs view
@@ -10,7 +10,8 @@ Copyright : (c) Quytelda Kahja, 2026 License : BSD-3-Clause -This module contains an API (types and functions) for running argument parsers.+This module contains types and functions necessary for running+argument parsers. -} module Mangrove ( -- * Standard Interface@@ -21,10 +22,11 @@ , ParseTree , Scheme , Result(..)- , SupportsHelp+ , SupportsResponse , StreamState- , HelpHandler- , HelpContinuation(..)+ , RequestType(..)+ , RequestHandler+ , ReqContinuation(..) -- * Pure Interface -- ** Helpful Parsers@@ -52,25 +54,19 @@ import Mangrove.Resolve import Mangrove.Text --- | Program metadata for displaying help output.-data ProgramInfo = ProgramInfo- { programName :: !Text -- ^ The program name- , programDesc :: !Text -- ^ A description of the program- } deriving (Show)- -- | The results of a parsing operation. ----- Only parsing schemes that support generating help output will yield--- 'Help' values.+-- Only parsing schemes that support generating responses can use the+-- 'Response' constructor. data Result s r where -- | A successful parsing operation yields a list of leftover -- arguments and a result value. Success :: ![Text] -> !r -> Result s r -- | A failed parsing operation yields an error message. Failure :: !Text -> Result s r- -- | A request for help yields human-readable help output (for+ -- | A request for information yields a human-readable response (for -- parsers that support it).- Help :: SupportsHelp s => !Text -> Result s r+ Response :: SupportsResponse s => !Text -> Result s r deriving instance Show r => Show (Result s r) deriving instance Eq r => Eq (Result s r)@@ -80,9 +76,9 @@ argsToState args = StreamState args [] False -- | Attempt to parse a value of type @r@ from a list of arguments,--- where the parser @ParseTree s r@ doesn't support help output.+-- where the parser @ParseTree s r@ doesn't support requests. runSilentParser- :: (Scheme s, HelpSupport s ~ 'Silent)+ :: (Scheme s, RequestSupport s ~ 'False) => ParseTree s r -- ^ Argument parser -> [Text] -- ^ Input arguments -> Result s r@@ -91,18 +87,18 @@ -- | A more general form of 'runSilentParser' that accepts a custom -- stream starting state. runSilentParser'- :: (Scheme s, HelpSupport s ~ 'Silent)+ :: (Scheme s, RequestSupport s ~ 'False) => ParseTree s r -- ^ Argument parser -> StreamState s -- ^ Initial stream state -> Result s r runSilentParser' tree state =- runArgumentParser' tree state Success Failure NoHelp+ runArgumentParser' tree state Success Failure NoRequests -- | Attempt to parse a value of type @r@ from a list of arguments,--- where the parser @ParseTree s r@ supports help output.+-- where the parser @ParseTree s r@ supports requests. runHelpfulParser- :: SupportsHelp s- => ProgramInfo -- ^ Program metadata+ :: SupportsResponse s+ => ProgramInfo s -- ^ Program metadata -> ParseTree s r -- ^ Argument parser -> [Text] -- ^ Input arguments -> Result s r@@ -111,39 +107,42 @@ -- | A more general form of 'runHelpfulParser' that accepts a custom -- stream starting state. runHelpfulParser'- :: SupportsHelp s- => ProgramInfo -- ^ Program metadata+ :: SupportsResponse s+ => ProgramInfo s -- ^ Program metadata -> ParseTree s r -- ^ Argument parser -> StreamState s -- ^ Initial stream state -> Result s r runHelpfulParser' info tree state =- runArgumentParser' tree state Success Failure (OnHelp _onHelpRequest)+ runArgumentParser' tree state Success Failure (OnRequest _onRequest) where- _onHelpRequest state' =- Help $ makeHelpInfo tree (streamContext state') (programName info) (programDesc info)+ _onRequest state' HelpRequest =+ Response $ makeHelpInfo tree (streamContext state') info+ _onRequest _ VersionRequest =+ Response $ makeVersionInfo info --- | A variant of 'runHelpfulParser' that treats help requests as--- failures.+-- | A variant of 'runHelpfulParser' that treats requests as failures.+--+-- This is useful if you know that no requests will ever be made. runHelpfulParser_- :: SupportsHelp s+ :: SupportsResponse s => ParseTree s r -- ^ Argument parser -> [Text] -- ^ Input arguments -> Result s r runHelpfulParser_ tree args =- runArgumentParser' tree (argsToState args) Success Failure (OnHelp _onHelpRequest)+ runArgumentParser' tree (argsToState args) Success Failure (OnRequest _onRequest) where- _onHelpRequest state' = Failure $+ _onRequest state' _ = Failure $ formatError (streamContext state') "help requested" -- | Parse the command line arguments passed to the program, then -- invoke the program's entrypoint with the results of the parsing. If -- parsing fails, we instead display an error to stderr and exit.--- Alternatively, if help was requested, we abandon parsing and print--- the relevant help output to stdout, then exit without indicating an--- error.+-- Alternatively, if information was requested, we abandon parsing and+-- print the relevant response to stdout, then exit without indicating+-- an error. parseArguments- :: SupportsHelp s- => ProgramInfo -- ^ Program metadata+ :: SupportsResponse s+ => ProgramInfo s -- ^ Program metadata -> ParseTree s r -- ^ Argument parser -> (r -> IO a) -- ^ Program Entrypoint -> IO a@@ -157,7 +156,7 @@ Failure err -> do TIO.hPutStrLn stderr err exitFailure- Help output -> do+ Response output -> do TIO.putStr output exitSuccess @@ -169,7 +168,7 @@ -> [Text] -- ^ Input arguments -> ([Text] -> r -> a) -- ^ Success handler -> (Text -> a) -- ^ Failure handler- -> HelpHandler s a -- ^ Help request handler+ -> RequestHandler s a -- ^ Request handler -> a runArgumentParser tree = runArgumentParser' tree . argsToState @@ -181,7 +180,7 @@ -> StreamState s -- ^ Initial stream state -> ([Text] -> r -> a) -- ^ Success handler -> (Text -> a) -- ^ Failure handler- -> HelpHandler s a -- ^ Help request handler+ -> RequestHandler s a -- ^ Request handler -> a runArgumentParser' tree state cok cerr hhelp = runStreamParser (satiate tree) handler state@@ -197,5 +196,5 @@ { onSuccess = _onSuccess , onFailure = _onFailure , onEmpty = flip _onFailure "empty"- , onHelpRequest = hhelp+ , onRequest = hhelp }
src/Mangrove/Parser.hs view
@@ -44,18 +44,19 @@ -- * Parsing Schemes , Scheme(..)- , HelpCapability(..)- , SupportsHelp(..)+ , ProgramInfo(..)+ , SupportsResponse(..) -- * Stream Parser , StreamParser(..) , StreamHandler(..) , StreamState(..)- , HelpHandler- , HelpContinuation(..)+ , RequestHandler+ , ReqContinuation(..) - -- ** Help- , requestHelp+ -- ** Requests+ , RequestType(..)+ , request -- ** Escaping , setEscaped@@ -80,14 +81,13 @@ import Control.Monad.Except import Data.Kind import qualified Data.List as List-import Data.Maybe import Data.Proxy import Data.Text (Text) import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TLB+import Data.Version import Mangrove.Resolve-import Mangrove.Separable import Mangrove.Text import Mangrove.Valency @@ -225,39 +225,9 @@ -- Constant nodes that don't accept input have no usage. render _ = "" -instance (Separable s, Valency s) => Separable (ParseTree s) where- separate (SumNode l r) = Exhibit norm (modalsL <> modalsR)- where- Exhibit normL modalsL = separate l- Exhibit normR modalsR = separate r- norm = liftA2 SumNode normL normR- <|> normL- <|> normR- separate (ProdNode f l r) = Exhibit norm modals- where- Exhibit normL modalsL = separate l- Exhibit normR modalsR = separate r- node = ProdNode f- norm = liftA2 node normL normR- cross g modalTrees normalTrees =- [ g (if usesTerseOutput m && isOptional n then empty else n) <$> m- | m <- modalTrees- , n <- normalTrees- ]- modals = cross (flip node) modalsL (maybeToList normR)- <> cross node modalsR (maybeToList normL)- <> [liftA2 node u v | u <- modalsL, v <- modalsR]- separate (ParseNode p) = ParseNode <$> separate p- separate n = Exhibit (Just n) []- -------------------------------------------------------------------------------- -- Parsing Schemes --- | A marker that distinguishes "silent" schemes (which produce no--- help output) from "helpful" schemes, which support the production--- of help output.-data HelpCapability = Silent | Helpful- -- | A scheme is a system of parsers and tokens. It parses a sequence -- of arguments into tokens and values. class (Functor s, Resolve s, Eq (Token s), Render (Token s), Show (Token s)) => Scheme (s :: Type -> Type) where@@ -265,13 +235,13 @@ -- string under this parsing scheme. data Token s - -- | This type indicates whether a parsing scheme supports help- -- output.+ -- | This type indicates whether a parsing scheme accepts requests+ -- for information. --- -- It is 'Silent' by default, but must be set to 'Helpful' if the- -- scheme will implement an instance of 'SupportsHelp'.- type HelpSupport s :: HelpCapability- type HelpSupport s = 'Silent+ -- When @RequestSupport scheme@ is @True@, a 'SupportsResponse'+ -- instance should be provided for @scheme@.+ type RequestSupport s :: Bool+ type RequestSupport s = 'False -- | 'delimiter' is the character that separates argument strings in -- combined string representation. For example, arguments in the CLI@@ -294,13 +264,19 @@ -- parser. usageInfo :: s r -> Builder --- | A class for schemes that support human-readable help output.------ NOTE: In order to define a 'SupportsHelp' instance for some @Scheme--- s@, @HelpSupport s@ must be set to 'Helpful'.-class (Scheme s, HelpSupport s ~ 'Helpful) => SupportsHelp s where- makeHelpInfo :: ParseTree s r -> [Token s] -> Text -> Text -> Text+-- | Program metadata for displaying help output.+data ProgramInfo (s :: Type -> Type) = ProgramInfo+ { programName :: !Text -- ^ The program name+ , programVersion :: !Version -- ^ The program version+ , programDesc :: !Text -- ^ A description of the program+ } deriving (Show) +-- | A class for schemes that support human-readable responses to+-- requests for help or version information.+class (Scheme s, RequestSupport s ~ 'True) => SupportsResponse s where+ makeVersionInfo :: ProgramInfo s -> Text+ makeHelpInfo :: ParseTree s r -> [Token s] -> ProgramInfo s -> Text+ -------------------------------------------------------------------------------- -- Stream Parser @@ -327,38 +303,44 @@ deriving instance Scheme s => Show (StreamState s) deriving instance Scheme s => Eq (StreamState s) --- | A handler for when help is requested.+-- | What information is being requested?+data RequestType+ = VersionRequest -- ^ A request for version information+ | HelpRequest -- ^ A request for help and usage information+ deriving (Eq, Show)++-- | A handler for when information is requested. -- -- This will hold a continuation function for helpful parsing -- schemes, or a placeholder value for silent schemes.-data family HelpContinuation (cap :: HelpCapability) (s :: Type -> Type) r+data family ReqContinuation (cap :: Bool) (s :: Type -> Type) r -data instance HelpContinuation 'Silent s r- = NoHelp+data instance ReqContinuation 'False s r+ = NoRequests deriving (Functor) -newtype instance HelpContinuation 'Helpful s r- = OnHelp (StreamState s -> r)+newtype instance ReqContinuation 'True s r+ = OnRequest (StreamState s -> RequestType -> r) deriving (Functor) --- | A handler for when help is requested.+-- | A handler for when information is requested. -- -- This will hold a continuation function for helpful parsing -- schemes, or a placeholder value for silent schemes.-type HelpHandler s r = HelpContinuation (HelpSupport s) s r+type RequestHandler s r = ReqContinuation (RequestSupport s) s r -- | A collection of continuations to be called for each situation a -- stream parser might encounter. data StreamHandler s a r = StreamHandler- { onSuccess :: StreamState s -> a -> r -- ^ Success Continuation- , onEmpty :: StreamState s -> r -- ^ Empty continuation- , onFailure :: StreamState s -> Builder -> r -- ^ Failure Continuation- , onHelpRequest :: HelpHandler s r -- ^ Help Continuation+ { onSuccess :: StreamState s -> a -> r -- ^ Success Continuation+ , onEmpty :: StreamState s -> r -- ^ Empty continuation+ , onFailure :: StreamState s -> Builder -> r -- ^ Failure Continuation+ , onRequest :: RequestHandler s r -- ^ Request Continuation } -- | The amazing stream parsing monad! This monad tracks the stream--- state and context. It short-circuits when exceptions or--- help-requests are raised.+-- state and context. It short-circuits when exceptions or requests+-- are raised. newtype StreamParser s a = StreamParser { runStreamParser :: forall r. StreamHandler s a r@@ -408,12 +390,12 @@ getEscaped = StreamParser $ \handler state -> onSuccess handler state (streamEscaped state) --- | Signal that help information is requested. Short-circuits any--- further operations.-requestHelp :: HelpSupport s ~ 'Helpful => StreamParser s a-requestHelp = StreamParser $ \handler state ->- case onHelpRequest handler of- OnHelp h -> h state+-- | Signal that information is requested. Short-circuits any further+-- operations.+request :: RequestSupport s ~ 'True => RequestType -> StreamParser s a+request requestType = StreamParser $ \handler state ->+ case onRequest handler of+ OnRequest h -> h state requestType -- | Get a list representing the current context stack. getContext :: StreamParser s [Token s]
src/Mangrove/Scheme/Sub.hs view
@@ -26,7 +26,6 @@ import Mangrove.Parser import Mangrove.Resolve-import Mangrove.Separable import Mangrove.Text import Mangrove.TextParser import Mangrove.Valency@@ -45,9 +44,6 @@ ExpectedError [render hint] resolve (Option key (TextParser hint _)) = ExpectedError [render key <> "=" <> render hint]--instance Separable SubScheme where- separate s = Exhibit (Just s) [] instance Scheme SubScheme where data Token SubScheme
src/Mangrove/Scheme/Unix.hs view
@@ -44,13 +44,14 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TLB+import Data.Version+import Data.Void import Mangrove import Mangrove.Parser import Mangrove.Resolve import Mangrove.Scheme.Sub (SubScheme) import qualified Mangrove.Scheme.Sub as Sub-import Mangrove.Separable import Mangrove.Text import Mangrove.TextParser import Mangrove.Valency@@ -112,34 +113,26 @@ | Command !CommandInfo (ParseTree UnixScheme r) -- | A named option that might support suboptions | Option !OptionInfo (ParseTree SubScheme r)- -- | A special option that requests help information- | HelpOption !OptionInfo+ -- | A special option that raises a request for information+ | RequestOption !OptionInfo !RequestType deriving (Functor) instance Valency UnixScheme where valency (Parameter _) = Just 1 valency (Command _ subtree) = fmap (+1) (valency subtree) valency (Option _ subtree) = fmap (max 2) (valency subtree)- valency (HelpOption _) = Just 1+ valency (RequestOption {}) = Just 1 instance Resolve UnixScheme where resolve (Parameter (TextParser hint _)) = ExpectedError [render hint] resolve (Option info _) = ExpectedError [render $ optHead info]- resolve (HelpOption info) =+ resolve (RequestOption info _) = ExpectedError [render $ optHead info] resolve (Command info _) = ExpectedError [render $ cmdHead info] -instance Separable UnixScheme where- separate p@(HelpOption _) = Exhibit Nothing [Modal True p]- separate (Command info subtree) =- Exhibit Nothing $ (Modal False <$> maybeToList mregular) <> modals- where- Exhibit mregular modals = Command info <$> separate subtree- separate p = Exhibit (Just p) []- -- | A parser for interpreting options. An option always begins with a -- flag, followed optionally by an "=" sign and a bound argument. The -- strings "--" and "-" are not treated as options.@@ -170,7 +163,7 @@ | UnixOption Flag (Maybe Text) deriving (Eq, Show) - type HelpSupport UnixScheme = 'Helpful+ type RequestSupport UnixScheme = 'True delimiter _ = ' ' @@ -224,7 +217,7 @@ runArgumentParser' subtree (initState args) (curry pure) (throwError . render)- NoHelp+ NoRequests withContext (UnixOption flag mbound) $ do -- If a bound argument (e.g. --floop=blah) is provided, we@@ -259,7 +252,7 @@ (_, result) <- parseSubargs [] pure result - activate (HelpOption info) = do+ activate (RequestOption info requestType) = do -- Arguments should never be interpreted as options when escaped. getEscaped >>= guard . not @@ -267,8 +260,8 @@ guard $ flag `elem` optFlags info pop_ - withContext (UnixOption flag mbound)- requestHelp+ withContext (UnixOption flag mbound) $+ request requestType activate (Command info subtree) = do -- Arguments should never be interpreted as commands when escaped.@@ -295,7 +288,7 @@ separator = case flag of LongFlag _ -> "=" _ -> ""- usageInfo (HelpOption info) =+ usageInfo (RequestOption info _) = render (optHead info) instance Render (Token UnixScheme) where@@ -305,15 +298,107 @@ render (UnixOption f@(LongFlag _) (Just v)) = render f <> "=" <> render v render (UnixOption f@(ShortFlag _) (Just v)) = render f <> render v -instance SupportsHelp UnixScheme where- makeHelpInfo tree context name desc = renderText+-- | A factored group of subtrees (branches) representing different+-- usage modes.+data Usages a = Usages+ [ParseTree UnixScheme Void] -- ^ Request branches+ (Maybe (ParseTree UnixScheme a)) -- ^ Uncategorized branch+ [ParseTree UnixScheme a] -- ^ Command branches++-- | Factor a 'ParseTree' into several independant subtrees+-- (branches), potentially filtered to specific commands.+--+-- Each branch can be thought of as corresponding to one particular+-- mode of operation, in that it contains at least one command or+-- option that conflicts with commands or options in other branches.+--+-- We can select only branches that correspond to a particular+-- subcommand by passing the components of that subcommand as a list:+--+-- > decomposeTree tree [] -- No filtering+-- > decomposeTree tree ["stash", "list"] -- Select "stash list" command+decomposeTree :: ParseTree UnixScheme r -> [Text] -> Usages r+decomposeTree (ParseNode (RequestOption info requestType)) commands =+ -- If we're currently searching for a specific command, then+ -- this request option is irrelevant.+ let node = ParseNode (RequestOption info requestType)+ in Usages (if null commands then [node] else []) Nothing []++decomposeTree (ParseNode (Command info subtree)) commands+ | commandMismatch =+ -- We are looking for a specific command and it's not this+ -- one, so don't return any trees.+ Usages [] Nothing []+ | otherwise =+ -- Either this is the command we're looking for, or we're not+ -- looking for a command.+ let Usages req misc cmd = decomposeTree subtree (drop 1 commands)+ req' = ParseNode . Command info <$> req+ cmd' = ParseNode . Command info <$> maybeToList misc <> cmd+ in Usages req' Nothing cmd'+ where+ commandMismatch =+ case commands of+ (command : _) -> not $ command `elem` cmdNames info+ [] -> False++decomposeTree (SumNode l r) commands =+ let Usages reqLs miscL cmdLs = decomposeTree l commands+ Usages reqRs miscR cmdRs = decomposeTree r commands++ -- When both subtrees yield uncategorized branches, then we+ -- want to sum them normally. However, if only one subtree+ -- yields an uncategorized branch, we can just replace sum+ -- with that branch.+ misc = liftA2 SumNode miscL miscR+ <|> miscL+ <|> miscR+ in Usages (reqLs <> reqRs) misc (cmdLs <> cmdRs)++decomposeTree (ProdNode f l r) commands =+ let Usages reqLs miscL cmdLs = decomposeTree l commands+ Usages reqRs miscR cmdRs = decomposeTree r commands+ prod = ProdNode f++ -- Requests prevent any further parsing, so if one of the+ -- subtrees yields request branches, the other subtree is+ -- irrelevant. If somehow both subtrees yield request+ -- branches, then a product node behaves effectively like a+ -- sum node because we could never actually trigger both+ -- requests.+ reqs = reqRs <> reqLs+ misc = liftA2 prod miscL miscR+ cmds = liftA2 prod (maybeToList miscL) cmdRs <>+ liftA2 prod cmdLs (maybeToList miscR)+ in Usages reqs misc cmds++decomposeTree tree _ = Usages [] (Just tree) []++formatUsages :: Text -> Usages r -> Builder+formatUsages progName (Usages reqs misc cmds) =+ mconcat+ $ List.intersperse "\n"+ $ map (\t -> TLB.fromText progName <> " " <> render t) usageModes+ where+ usageModes = map vacuous reqs <> maybeToList misc <> cmds++instance SupportsResponse UnixScheme where+ makeVersionInfo info = renderText+ $ render (programName info)+ <> " version "+ <> renderVersion (programVersion info)+ <> "\n"+ where+ renderVersion = TLB.fromString . showVersion++ makeHelpInfo tree context info = renderText $ "Usage:\n"- <> renderUsages tree <> "\n"- <> render desc <> "\n"+ <> formatUsages (programName info) usages <> "\n\n"+ <> render (programDesc info) <> "\n" <> renderHelp tree context where- renderUsageLine s = render name <> " " <> render s <> "\n"- renderUsages = foldMap renderUsageLine . exhibitToList . separate+ commandContext = [cmd | UnixCommand cmd <- context]+ usages = decomposeTree tree commandContext -- | Convenient type alias for Unix-flavored parse trees. type UnixParser = ParseTree UnixScheme@@ -331,7 +416,7 @@ addHelpOptions flags desc tree = ParseNode helpOption <|> go tree where helpOption :: UnixScheme a- helpOption = HelpOption $ OptionInfo flags desc+ helpOption = RequestOption (OptionInfo flags desc) HelpRequest go :: ParseTree UnixScheme a -> ParseTree UnixScheme a go (ParseNode (Command info subtree)) =@@ -369,15 +454,15 @@ -- | Enumerate descriptive information for all options available in a -- parse tree, indexed by the set of commands under which they exist. collectOptions :: ParseTree UnixScheme r -> Map [CommandInfo] [OptionHelp]-collectOptions tree = go tree mempty+collectOptions tree = go tree (Map.singleton [] []) where go :: ParseTree UnixScheme r -> Map [CommandInfo] [OptionHelp] -> Map [CommandInfo] [OptionHelp] go (ParseNode (Option info subtree)) =- Map.insertWith (<>) [] [makeOptionHelp info subtree]- go (ParseNode (HelpOption info)) =- Map.insertWith (<>) [] [makeOptionHelp info empty]+ Map.adjust (makeOptionHelp info subtree :) []+ go (ParseNode (RequestOption info _)) =+ Map.adjust (makeOptionHelp info empty :) [] go (ParseNode (Command info subtree)) = Map.union $ Map.mapKeys (info :) $ collectOptions subtree go (ProdNode _ l r) = go r . go l
− src/Mangrove/Separable.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE KindSignatures #-}--{-|-Module : Mangrove.Separable-Copyright : (c) Quytelda Kahja, 2026-License : BSD-3-Clause--Tools for decomposing parsers into different modal subparsers.--}-module Mangrove.Separable- ( Separable(..)- , Modal(..)- , usesTerseOutput- , Exhibit(..)- , exhibitToList- ) where--import Data.Kind-import Data.Maybe---- | A modal branch of a parser or parse tree is a particular subtree--- that, when triggered, excludes the rest of the tree from receiving--- input. This can happen because the parsing context changed (for--- example, when a command is recognized) or when the parsing is--- exited entirely (for example, when a help option is encountered).-data Modal a = Modal !Bool a- deriving (Functor)--instance Applicative Modal where- pure = Modal False- Modal terse1 f <*> Modal terse2 x = Modal (terse1 && terse2) (f x)---- | Modal trees that exit the parsing flow entirely have no--- opportunity to make use of optional parsers, since their values--- will never be evaluated. Thus, we mark those trees as having--- "terse" output so that we can omit the optional subtrees when--- generating help information.-usesTerseOutput :: Modal a -> Bool-usesTerseOutput (Modal terseOutput _) = terseOutput---- | A representation of an object whose modal sub-components have--- been split off for the purpose of better help output.------ Every parser and parse tree can be decomposed into one regular tree--- and a list of modal trees.-data Exhibit a = Exhibit (Maybe a) [Modal a]- deriving (Functor)---- | Convert an t'Exhibit' to a regular list of regular and modal--- components.-exhibitToList :: Exhibit a -> [a]-exhibitToList (Exhibit mnorm modals) =- maybeToList mnorm <> [t | Modal _ t <- modals]---- | A 'Separable' parser is one that can be decomposed into regular--- and modal subparsers.------ We do this so that we can render usage information for each parser--- mode separately. This makes the usage of complex commands--- significantly easier to read.------ NOTE: Decomposed subparsers are intended for display purposes--- (hence the t'Exhibit' type). Trying to parse input with them is--- likely to fail.-class Functor s => Separable (s :: Type -> Type) where- separate :: s r -> Exhibit (s r)
src/Mangrove/TextParser.hs view
@@ -15,8 +15,24 @@ -} module Mangrove.TextParser- ( TextParser(..)+ ( -- * TextParser+ TextParser(..) , runTextParser++ -- * Parsers for Common Types+ , parseBool+ , parseInt+ , parseInteger+ , parseWord+ , parseChar+ , parseFloat+ , parseDouble+ , parseText+ , parseLazyText+ , parseLazyTextBuilder+ , parseString++ -- * Automatic Parser Selection , DefaultParser(..) ) where @@ -24,6 +40,7 @@ import Data.Bifunctor import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TLB import qualified Data.Text.Read as TR @@ -55,65 +72,122 @@ Right (result, "") -> pure result Right (_, leftover) -> throwError $ "unexpected input: " <> leftover +-- | Parses a boolean value. This parser accepts @"true"@, @"false"@,+-- @"yes"@, or @"no"@ as input.+parseBool :: TextParser Bool+parseBool = TextParser+ { parserHint = "BOOL"+ , parserRun = parse+ }+ where+ parse "true" = pure True+ parse "false" = pure False+ parse "yes" = pure True+ parse "no" = pure False+ parse _ = throwError "expected true|false|yes|no"+ instance DefaultParser Bool where- defaultParser = TextParser- { parserHint = "BOOL"- , parserRun = parse- }- where- parse "true" = pure True- parse "false" = pure False- parse "yes" = pure True- parse "no" = pure False- parse _ = throwError "expected true|false|yes|no"+ defaultParser = parseBool +-- | Parse a signed 'Int' value in base-10.+parseInt :: TextParser Int+parseInt = TextParser+ { parserHint = "INT"+ , parserRun = exactly TR.decimal+ }+ instance DefaultParser Int where- defaultParser = TextParser- { parserHint = "INT"- , parserRun = exactly TR.decimal- }+ defaultParser = parseInt +-- | Parse a signed 'Integer' value in base-10.+parseInteger :: TextParser Integer+parseInteger = TextParser+ { parserHint = "INT"+ , parserRun = exactly TR.decimal+ }+ instance DefaultParser Integer where- defaultParser = TextParser- { parserHint = "INT"- , parserRun = exactly TR.decimal- }+ defaultParser = parseInteger +-- | Parse an unsigned `Word` value in base-10.+parseWord :: TextParser Word+parseWord = TextParser+ { parserHint = "INT"+ , parserRun = exactly TR.decimal+ }+ instance DefaultParser Word where- defaultParser = TextParser- { parserHint = "INT"- , parserRun = exactly TR.decimal- }+ defaultParser = parseWord +-- | Parse exactly one character. If the input is longer than 1 character, the parser fails.+parseChar :: TextParser Char+parseChar = TextParser+ { parserHint = "CHAR"+ , parserRun = parse+ }+ where+ parse (T.unpack -> [c]) = pure c+ parse _ = throwError "input contains multiple characters"+ instance DefaultParser Char where- defaultParser = TextParser- { parserHint = "CHAR"- , parserRun = parse- }- where- parse (T.unpack -> [c]) = pure c- parse _ = throwError "input contains multiple characters"+ defaultParser = parseChar +-- | Parse a floating point value in base-10.+parseFloat :: TextParser Float+parseFloat = TextParser+ { parserHint = "FLOAT"+ , parserRun = exactly TR.rational+ }+ instance DefaultParser Float where- defaultParser = TextParser- { parserHint = "FLOAT"- , parserRun = exactly TR.rational- }+ defaultParser = parseFloat +-- | Parse a double width value in base-10.+parseDouble :: TextParser Double+parseDouble = TextParser+ { parserHint = "DOUBLE"+ , parserRun = exactly TR.rational+ }+ instance DefaultParser Double where- defaultParser = TextParser- { parserHint = "DOUBLE"- , parserRun = exactly TR.rational- }+ defaultParser = parseDouble +-- | Parse a strict 'Text' value.+--+-- Since the input is already strict 'Text', this parser simply returns it for free.+parseText :: TextParser Text+parseText = TextParser+ { parserHint = "STRING"+ , parserRun = pure+ }+ instance DefaultParser Text where- defaultParser = TextParser- { parserHint = "STRING"- , parserRun = pure- }+ defaultParser = parseText +parseLazyText :: TextParser TL.Text+parseLazyText = TextParser+ { parserHint = "STRING"+ , parserRun = pure . TL.fromStrict+ }++instance DefaultParser TL.Text where+ defaultParser = parseLazyText++parseLazyTextBuilder :: TextParser TLB.Builder+parseLazyTextBuilder = TextParser+ { parserHint = "STRING"+ , parserRun = pure . TLB.fromText+ }++instance DefaultParser TLB.Builder where+ defaultParser = parseLazyTextBuilder++-- | Parse a Haskell 'String' (i.e. @[Char]@) value.+parseString :: TextParser String+parseString = TextParser+ { parserHint = "STRING"+ , parserRun = pure . T.unpack+ }+ instance DefaultParser String where- defaultParser = TextParser- { parserHint = "STRING"- , parserRun = pure . T.unpack- }+ defaultParser = parseString
src/Mangrove/Unix.hs view
@@ -24,6 +24,7 @@ , option , optionPure , switch+ , requestOption , command , subparameter , suboption@@ -37,7 +38,7 @@ import Data.Text (Text) import Mangrove.Parser-import Mangrove.Scheme.Sub (SubScheme, SubParser)+import Mangrove.Scheme.Sub (SubParser, SubScheme) import qualified Mangrove.Scheme.Sub as Sub import Mangrove.Scheme.Unix import Mangrove.TextParser@@ -65,12 +66,24 @@ -> Text -> a -> UnixParser a-optionPure flags help = ParseNode . Option (OptionInfo flags help) . pure+optionPure flags help = option flags help . pure -- | Define a CLI option which produces 'True' if present and 'False' -- otherwise. switch :: NonEmpty Flag -> Text -> UnixParser Bool switch flags help = optionPure flags help True <|> pure False++-- | A special option that triggers a request for information.+--+-- When a request option is encountered in the command line, a+-- "request" is raised and parsing is abandoned in favor of yielding a+-- human-readable response.+requestOption+ :: NonEmpty Flag+ -> Text+ -> RequestType+ -> UnixParser a+requestOption flags help = ParseNode . RequestOption (OptionInfo flags help) -- | Define a CLI subcommand with it's own parsing subtree. command
test/General.hs view
@@ -5,6 +5,7 @@ import Control.Applicative +import Data.Version import Test.Hspec import Mangrove@@ -113,19 +114,24 @@ `shouldBe` Success [] "value=asdf" describe "help options" $ do- let progInfo = ProgramInfo "example" "description"- isHelpResult (Help _) = True- isHelpResult _ = False+ let progInfo = ProgramInfo+ { programName = "example"+ , programVersion = makeVersion [1,0]+ , programDesc = "description"+ } :: ProgramInfo s + isResponse (Response {}) = True+ isResponse _ = False+ context "when a help option is present" $ do it "requests help" $ do runHelpfulParser progInfo (withHelp opt_example_unit) ["--help"]- `shouldSatisfy` isHelpResult+ `shouldSatisfy` isResponse it "works for subcommands" $ do runHelpfulParser progInfo (withHelp cmd_example_tree) ["example", "--help"]- `shouldSatisfy` isHelpResult+ `shouldSatisfy` isResponse runHelpfulParser progInfo (withHelp cmd_example_tree) ["example", "asdf", "--help"]- `shouldSatisfy` isHelpResult+ `shouldSatisfy` isResponse context "when a help option is absent" $ do it "doesn't request help" $ do
test/Mangrove/ParserSpec.hs view
@@ -120,12 +120,12 @@ = SSuccess r | SEmpty | SFailure Builder- | SHelpReq+ | SRequest RequestType deriving (Eq, Show) -- | Sink the results of a 'StreamParser' into a data type for easier inspection. runStreamParser'- :: SupportsHelp s+ :: SupportsResponse s => StreamParser s r -> StreamState s -> (StreamState s, StreamResult r)@@ -136,7 +136,7 @@ { onSuccess = \s result -> (s, SSuccess result) , onEmpty = \s -> (s, SEmpty) , onFailure = \s err -> (s, SFailure err)- , onHelpRequest = OnHelp $ \s -> (s, SHelpReq)+ , onRequest = OnRequest $ \s t -> (s, SRequest t) } initState_empty :: StreamState s