packages feed

commander-cli 0.10.0.1 → 0.10.1.0

raw patch · 4 files changed

+258/−43 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Options.Commander: annotated :: forall annotation combinator p m a. ProgramT (combinator & p) m a -> ProgramT (Annotated annotation combinator & p) m a
+ Options.Commander: data Annotated :: Symbol -> * -> *
+ Options.Commander: instance (GHC.TypeLits.KnownSymbol annotation, Options.Commander.HasProgram (combinator Options.Commander.& p)) => Options.Commander.HasProgram (Options.Commander.Annotated annotation combinator Options.Commander.& p)

Files

README.md view
@@ -3,13 +3,49 @@ [![Hackage](https://img.shields.io/hackage/v/commander-cli.svg)](https://hackage.haskell.org/package/commander-cli) [![Build Status](https://travis-ci.org/SamuelSchlesinger/commander-cli.svg?branch=master)](https://travis-ci.org/SamuelSchlesinger/commander-cli) -This library is meant to allow Haskell programs to quickly and easily construct-command line interfaces which are easy to use, especially as a Haskell user. To-learn, I suggest viewing/playing with the task-manager application which-comes with this repository. Here, we'll display a simpler example:+This library is meant to allow Haskell programmers to quickly and easily construct+command line interfaces with decent documentation. +One extension I use in these examples is `-XTypeApplications`, which uses the `@param`+syntax to apply an type-level argument explicitly to a function with a `forall x ...` in its+type, rather than implicitly, we do when we write `fmap (+ 1) [1, 2, 3]` applying the type `[]`+to `fmap`. It's because of type inference in Haskell that we don't always have to apply our+types explicitly, as many other languages force you to do using a syntax typically like `fmap<[], Int> (+ 1) [1, 2, 3]`.`.++We can go to the command line and try out this example:++```+> :set -XTypeApplications+> :t fmap @[]+fmap @[] :: (a -> b) -> [a] -> [b]+> :t fmap @[] @Int+fmap @[] @Int :: (Int -> b) -> [Int] -> [b]+> :t fmap @[] @Int @Bool+fmap @[] @Int @Bool :: (Int -> Bool) -> [Int] -> [Bool]+```++The API of `commander-cli` allows for very profitable usage of type+applications, because the description of our command line program will live+at the type level. ++Another extension we will use is `-XDataKinds`, which is only for the ability+to use strings, or the kind `Symbol`, at the type level. Kinds are just the+type of types, and so `-XDataKinds` allows us to have kinds which are actually+data in their own right, like lists, strings, numbers, and custom Haskell+data types. For us, we will use strings to represent the documentation of our+program at the type level, as well as the names of options, flags, and arguments+we want to parse. This allows us to generate documentation programs simply from+the type signature of the CLI program we build.++Our first example will show a basic command line application,+complete with help messages that display reasonable messages to the user.+ ```haskell-main = command_ . toplevel @"argument-taker" . arg @"example-argument" $ raw . putStrLn+main = command_+  . toplevel @"argument-taker"+  . arg @"example-argument" $ \arg ->+    raw $ do+      putStrLn arg ```  When you run this program with `argument-taker help`, you will see:@@ -23,21 +59,24 @@ `- argument: example-argument :: [Char] ``` -The meaning of this is that every path in the tree is a unique command. The one-we've used is the help command. If we run this program with `argument-taker hello`+The meaning of this documentation is that every path in the tree is a unique command.+The one we've used is the help command. If we run this program with `argument-taker hello` we will see:  ``` hello ``` -Okay, so we've made a program with hardly any scaffolding that gives us a-decent help message, and pipes through our argument correctly. Naturally, we-might want to expand on the documentation of this program, as its not quite+Naturally, we might want to expand on the documentation of this program, as its not quite obvious enough what it does. -```-main = command_ . toplevel @"argument-taker" . arg @"example-argument" $ (description @"Takes the argument and prints it" . raw . putStrLn)+```haskell+main = command_+  . toplevel @"argument-taker"+  . arg @"example-argument" $ \arg ->+    description @"Takes the argument and prints it"+  . raw $ do+      putStrLn arg ```  Printing out the documentation again with `argument-taker help`, we see:@@ -55,12 +94,14 @@  Okay, so we can expand the documentation. But what if I have an option to pass to the same program? Well, we can pass an option like so: -```-main = command_ . toplevel @"argument-taker" $-  opt @"m" @"mode" \mode ->+```haskell+main = command_+  . toplevel @"argument-taker"+  . optDef @"m" @"mode" "Print" $ \mode ->     arg @"example-argument" $ \arg ->-      description @"Takes the argument and prints it or not, depending on the mode" . raw $ do-        if mode == "Print" then putStrLn arg else pure ()+    description @"Takes the argument and prints it or not, depending on the mode" +  . raw $ do+      if mode == "Print" then putStrLn arg else pure () ```  Now, when we run `argument-taker help` we will see:@@ -76,6 +117,176 @@    `- argument: example-argument :: [Char]       |       `- description: Takes the argument and prints it or not, depending on the mode+```++Okay! So we can now create programs which take arguments and options, so what+else do we want in a command line program? Flags! Lets add a flag to our+example program:++```haskell+main = command_+  . toplevel @"argument-taker"+  . optDef @"m" @"mode" "Print" $ \mode ->+    arg @"example-argument" $ \arg ->+    flag @"loud" $ \loud ->+    description @"Takes the argument and prints it or not, depending on the mode and possibly loudly" +  . raw $ do+      let msg = if loud then map toUpper arg <> "!" else arg+      if mode == "Print" then putStrLn msg else pure ()+```++Running this with `argument-taker help`, we see:++```+usage:+name: argument-taker+|++- subprogram: help+|+`- option: -m <mode :: [Char]>+   |+   `- argument: example-argument :: [Char]+      |+      `- flag: ~loud+         |+         `- description: Takes the argument and prints it or not, depending on the mode and possibly loudly+```++Okay, so we've added all of the normal command line things, but we haven't yet shown how to add a new command+to our program, so lets do that. To do this, we can write:++```haskell+main = command_+  . toplevel @"argument-taker"+  $ defaultProgram <+> sub @"shriek" (raw (putStrLn "AHHHHH!!"))+  where+  defaultProgram = +      optDef @"m" @"mode" "Print" $ \mode ->+      arg @"example-argument" $ \arg ->+      flag @"loud" $ \loud ->+      description @"Takes the argument and prints it or not, depending on the mode and possibly loudly" +    . raw $ do+        let msg = if loud then map toUpper arg <> "!" else arg+        if mode == "Print" then putStrLn msg else pure ()+```++Running this program with `argument-taker help`, we can see the docs yet again:++```+usage:+name: argument-taker+|++- subprogram: help+|++- option: -m <mode :: [Char]>+|  |+|  `- argument: example-argument :: [Char]+|     |+|     `- flag: ~loud+|        |+|        `- description: Takes the argument and prints it or not, depending on the mode and possibly loudly+|+`- subprogram: shriek+```++Awesome! So we have now shown how to use the primitives of CLI programs, as well as how to+add new subprograms. One more thing I would like to show that is different from normal CLI+libraries is that I added the ability to automatically search for environment variables and+pass them to your program. I just liked this, as sometimes when I use a CLI program I forget+this or that environment variable, and the documentation generation makes this self documenting+in commander-cli. We can add this to our program by writing:++```haskell+main = command_+  . toplevel @"argument-taker"+  $ env @"ARGUMENT_TAKER_DIRECTORY" \argumentTakerDirectory ->+      defaultProgram argumentTakerDirectory+  <+> sub @"shriek" (raw $ do+        setCurrentDirectory argumentTakerDirectory +        putStrLn "AHHH!"+      )+  where+  defaultProgram argumentTakerDirectory = +      optDef @"m" @"mode" "Print" $ \mode ->+      arg @"example-argument" $ \arg ->+      flag @"loud" $ \loud ->+      description @"Takes the argument and prints it or not, depending on the mode and possibly loudly" +    . raw $ do+        setCurrentDirectory argumentTakerDirectory+        let msg = if loud then map toUpper arg <> "!" else arg+        if mode == "Print" then putStrLn msg else pure ()+```++Now, we will see `argument-taker help` as:++```+usage:+name: argument-taker+|++- subprogram: help+|+`- required env: ARGUMENT_TAKER_DIRECTORY :: [Char]+   |+   +- option: -m <mode :: [Char]>+   |  |+   |  `- argument: example-argument :: [Char]+   |     |+   |     `- flag: ~loud+   |        |+   |        `- description: Takes the argument and prints it or not, depending on the mode and possibly loudly+   |+   `- subprogram: shriek+```++We can see that it documents the usage of this environment variable in a+reasonable way, but its not clear where exactly what it does exactly. First,+you might think to use the `description` combinator, but it isn't exactly made+for describing an input, but for documenting a path of a program. We can fix this+using the `annotated` combinator, which was made for describing inputs to our+program:++```haskell+main :: IO ()+main = command_+  . toplevel @"argument-taker"+  . annotated @"the directory we will go to for the program"+  $ env @"ARGUMENT_TAKER_DIRECTORY" \argumentTakerDirectory ->+      defaultProgram argumentTakerDirectory+  <+> sub @"shriek" (raw $ do+        setCurrentDirectory argumentTakerDirectory +        putStrLn "AHHH!"+      )+  where+  defaultProgram argumentTakerDirectory = +      optDef @"m" @"mode" "Print" $ \mode ->+      arg @"example-argument" $ \arg ->+      flag @"loud" $ \loud ->+      description @"Takes the argument and prints it or not, depending on the mode" +    . raw $ do+        setCurrentDirectory argumentTakerDirectory+        let msg = if loud then map toUpper arg <> "!" else arg+        if mode == "Print" then putStrLn msg else pure ()+```++Running `argument-taker help` will result in:++```+usage:+name: argument-taker+|++- subprogram: help+|+`- required env: ARGUMENT_TAKER_DIRECTORY :: [Char], the directory we will go to for the program+   |+   +- option: -m <mode :: [Char]>+   |  |+   |  `- argument: example-argument :: [Char]+   |     |+   |     `- flag: ~loud+   |        |+   |        `- description: Takes the argument and prints it or not, depending on the mode+   |+   `- subprogram: shriek ```  ## Design
app/Main.hs view
@@ -10,6 +10,7 @@ {-# LANGUAGE TypeOperators #-} module Main where +import Data.Char (toUpper) import Control.Monad import Options.Commander  import Prelude@@ -162,16 +163,4 @@       False -> Context home [] <$ createDirectory tasksFilePath    main :: IO ()-main = command_ . toplevel @"file" $- (sub @"maybe-read" $-  arg @"filename" \filename ->-  flag @"read" \b -> raw $-    if b-      then putStrLn =<< readFile filename-      else pure ())-  <+>- (sub @"maybe-write" $-  opt @"file" @"file-to-write" \mfilename -> raw $-    case mfilename of-      Just filename -> putStrLn =<< readFile filename-      Nothing -> pure ())+main = command_ taskManager
commander-cli.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.4  name:                commander-cli-version:             0.10.0.1+version:             0.10.1.0 synopsis:            A command line argument/option parser library description:         A command line argument/option parser library. homepage:            https://github.com/SamuelSchlesinger/commander-cli
src/Options/Commander.hs view
@@ -88,7 +88,7 @@     variables as well. We also have a convenience combinator, 'toplevel',     which lets you add a name and a help command to your program using the 'usage' combinator.   -}-  arg, opt, optDef, raw, sub, named, flag, toplevel, (<+>), usage, env, envOpt, envOptDef, description,+  arg, opt, optDef, raw, sub, named, flag, toplevel, (<+>), usage, env, envOpt, envOptDef, description, annotated,   -- ** Run CLI Programs   {- |     To run a 'ProgramT' (a specification of a CLI program), you will @@ -99,7 +99,7 @@     Each 'ProgramT' has a type level description, build from these type level     combinators.   -}-  type (&), type (+), Arg, Opt, Named, Raw, Flag, Env, Optionality(Required, Optional), Description,+  type (&), type (+), Arg, Opt, Named, Raw, Flag, Env, Optionality(Required, Optional), Description, Annotated,   -- ** Interpreting CLI Programs   {- |     The 'HasProgram' class forms the backbone of this library, defining the@@ -116,6 +116,7 @@            EnvProgramT'Optional, unEnvProgramT'Optional, unEnvDefault,            EnvProgramT'Required, unEnvProgramT'Required,            DescriptionProgramT,+           AnnotatedProgramT,            (:+:)            ),   -- ** The CommanderT Monad@@ -221,36 +222,39 @@ instance Unrender Char where   unrender = find (const True) --- | The type level naming combinator, giving your program a name for the--- sake of documentation.+-- | The type level combinator for constructing 'named' programs, giving your+-- program a name at the toplevel for the sake of documentation. data Named :: Symbol -> * --- | The type level argument combinator, with a 'Symbol' designating the+-- | The type level 'arg'ument combinator, with a 'Symbol' designating the -- name of that argument. data Arg :: Symbol -> * -> * --- | The type level option combinator, with a 'Symbol' designating the+-- | The type level 'opt'ion combinator, with a 'Symbol' designating the -- option's name and another representing the metavariables name for -- documentation purposes. data Opt :: Symbol -> Symbol -> * -> * --- | The type level flag combinator, taking a name as input, allowing your+-- | The type level 'flag' combinator, taking a name as input, allowing your -- program to take flags with the syntax @~flag@. data Flag :: Symbol -> * --- | The type level environment variable combinator, taking a name as+-- | The type level 'env'ironment variable combinator, taking a name as -- input, allowing your program to take environment variables as input -- automatically. data Env :: Optionality -> Symbol -> * -> * --- | The type level raw monadic program combinator, allowing a command line+-- | The type level 'raw' monadic program combinator, allowing a command line -- program to just do some computation. data Raw :: * --- | The type level description combinator, allowing a command line program+-- | The type level 'description' combinator, allowing a command line program -- to have better documentation. data Description :: Symbol -> * +-- | The type level 'annotated' combinator, allowing a command line +data Annotated :: Symbol -> * -> *+ -- | The type level tag for whether or not a variable is required or not. data Optionality = Required | Optional @@ -387,7 +391,7 @@     (documentation @p)]  instance (KnownSymbol name, HasProgram p) => HasProgram (Named name & p) where-  newtype ProgramT (Named name &p) m a = NamedProgramT { unNamedProgramT :: ProgramT p m a }+  newtype ProgramT (Named name & p) m a = NamedProgramT { unNamedProgramT :: ProgramT p m a }   run = run . unNamedProgramT    hoist n = NamedProgramT . hoist n . unNamedProgramT   documentation = [Node@@ -395,13 +399,19 @@     (documentation @p)]  instance (KnownSymbol description, HasProgram p) => HasProgram (Description description & p) where-  newtype ProgramT (Description description &p) m a = DescriptionProgramT { unDescriptionProgramT :: ProgramT p m a }+  newtype ProgramT (Description description & p) m a = DescriptionProgramT { unDescriptionProgramT :: ProgramT p m a }   run = run . unDescriptionProgramT    hoist n = DescriptionProgramT . hoist n . unDescriptionProgramT   documentation = [Node     ("description: " <> symbolVal (Proxy @description))     []] <> documentation @p +instance (KnownSymbol annotation, HasProgram (combinator & p)) => HasProgram (Annotated annotation combinator & p) where+  newtype ProgramT (Annotated annotation combinator & p) m a = AnnotatedProgramT { unAnnotatedProgramT :: ProgramT (combinator & p) m a }+  run = run . unAnnotatedProgramT +  hoist n = AnnotatedProgramT . hoist n . unAnnotatedProgramT+  documentation = fmap (\(Node x s) -> Node (x <> ", " <> symbolVal (Proxy @annotation)) s) (documentation @(combinator & p))+ instance (KnownSymbol sub, HasProgram p) => HasProgram (sub & p) where   newtype ProgramT (sub & p) m a = SubProgramT { unSubProgramT :: ProgramT p m a }   run s = Action $ \State{..} -> do @@ -534,6 +544,11 @@ usage = raw $ do   liftIO $ putStrLn "usage:"   liftIO $ putStrLn (document @p)++-- | A combinator which augments the documentation of the next element, by+-- adding a description after its name and type.+annotated :: forall annotation combinator p m a. ProgramT (combinator & p) m a -> ProgramT (Annotated annotation combinator & p) m a+annotated = AnnotatedProgramT  -- | A combinator which takes a program, and a type-level 'Symbol' -- description of that program, and produces a program here the