Code Monkey home page Code Monkey logo

commander-cli's Introduction

Commander CLI

Hackage Build Status

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. This extension allows us to use the @param syntax to apply an type-level argument explicitly to a function with a forall x ... in its type. This is as opposed to implicitly applying type-level arguments, as 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.

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:

usage:
name: argument-taker
|
+- subprogram: help
|
`- argument: example-argument :: [Char]

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

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" $ \arg ->
    description @"Takes the argument and prints it"
  . raw $ do
      putStrLn arg

Printing out the documentation again with argument-taker help, we see:

usage:
name: argument-taker
|
+- subprogram: help
|
`- argument: example-argument :: [Char]
   |
   `- description: Takes the argument and prints it

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"
  . 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 ()

Now, when we run argument-taker help we will see:

usage:
name: argument-taker
|
+- subprogram: help
|
`- option: -m <mode :: [Char]>
   |
   `- 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:

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:

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:

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:

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

The library is based around the following classes:

class Unrender r where
  unrender :: Text -> Maybe r

This class is what you will use to define the parsing of a type from text and can use any parsing library or whatever you want. Next, we have the class

class HasProgram p where
  data ProgramT p m a
  run :: ProgramT p IO a -> CommanderT State IO a
  hoist :: (forall x. m x -> n x) -> ProgramT p m a -> ProgramT p n a
  documentation :: Forest String

Instances of this class will define a syntactic element, a new instance of the data family ProgramT, as well as its semantics in terms of the CommanderT monad, which is something like a free backtracking monad. Users should not have to make instances of this class, as the common CLI elements are already defined as instances. Of course, you can if you want to, and it can be profitable to do so.

Similar Projects

Recommended Alternatives

  • cmdargs Command line argument processing
  • docopt A command-line interface parser that will make you smile
  • getopt-generics Create command line interfaces with ease
  • optparse-applicative Utilities and combinators for parsing command line options

Other CLI Packages

  • ReadArgs Simple command line argument parsing
  • argparser Command line parsing framework for console applications
  • cli-extras Miscellaneous utilities for building and working with command line interfaces
  • cli CLI
  • cmdtheline Declarative command-line option parsing and documentation library.
  • configifier parser for config files, shell variables, command line args.
  • configuration-tools Tools for specifying and parsing configurations
  • console-program Interpret the command line and a config file as commands and options
  • hflags Command line flag parser, very similar to Google's gflags
  • multiarg Command lines for options that take multiple arguments
  • options A powerful and easy-to-use command-line option parser.
  • parseargs Parse command-line arguments
  • quickterm An interface for describing and executing terminal applications
  • shell-utility Utility functions for writing command-line programs
  • symantic-cli Symantics for parsing and documenting a CLI

commander-cli's People

Contributors

bebesparkelsparkel avatar samuelschlesinger avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

commander-cli's Issues

literate haskell for readme examples

It seems like there is a lot of good documentation and there seems to be a lot of changes happening that could break this documentation. It would be nice to have it compiler checked as literate haskell to ensure correctness for the docs users.

combined flags

Allow combined flags like with ls

ls [-1AaCcdFfgHhikLlmnopqRrSsTtux] [file ...]

Some questions that need answering.

  • Is this an anti-pattern, and should it be implemented?
  • Should this be limited to single letter flags?
  • How should it be implemented?
    • combine flags at the type level and then split the flags on parsing

Standard CLI behavior for toplevel

It would be nice to have more standard behavior for toplevel that implements what users would expect to have with a toplevel helper cli function.

  • show help with -h
  • show help with --help
  • show help with no arguments
  • show help with parse error
  • exit non-zero status with parse error, so that scripts know that command failed

toplevel -h --help

accept the typical help commands of -h and --help to show the documentation

hoist not documented

I do not see any usage of hoist. It would be nice to have some documentation of its usage in the README.

multiple arguments

Many commands have the ability to take multiple arguments like rm and cp.

rm [-dfiPRrv] file ...
cp [-afipv] [-R [-H | -L | -P]] source ... directory

rm is the easy case but cp is a bit more difficult to parse because it must not consume the last argument directory.

Better Tests

I just expand the unit testing that I'm doing right now, and maybe include one integration tests with runningtask-manager or something.

app/Task/CLI.hs warning: [-Wmissing-signatures]

Perhaps just put under where or add signature.

app/Task/CLI.hs:45:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature:
      editTask :: FilePath
                  -> ProgramT
                       (Annotated annotation (Arg "task-name" String) & Raw) IO ()
   |
45 | editTask tasksFilePath = annotated $ arg @"task-name" $ \taskName -> raw 
   | ^^^^^^^^

app/Task/CLI.hs:46:54: warning: [-Wunused-matches]
    Defined but not used: `task'
   |
46 |   $ withTask tasksFilePath taskName $ \Context{home} task -> callProcess "vim" [home ++ "/" <> tasksFilePath <> "/" ++ taskName ++ ".task"]
   |                                                      ^^^^

app/Task/CLI.hs:48:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature:
      newTask :: FilePath
                 -> ProgramT
                      (Annotated annotation (Arg "task-name" String) & Raw) IO ()
   |
48 | newTask tasksFilePath = annotated $ arg @"task-name" $ \taskName -> raw $ do
   | ^^^^^^^

app/Task/CLI.hs:58:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature:
      closeTask :: FilePath
                   -> ProgramT
                        (Annotated annotation (Arg "task-name" String) & Raw) IO ()
   |
58 | closeTask tasksFilePath = annotated $ arg @"task-name" $ \taskName -> raw 
   | ^^^^^^^^^

app/Task/CLI.hs:59:54: warning: [-Wunused-matches]
    Defined but not used: `tasks'
   |
59 |   $ withTask tasksFilePath taskName $ \Context{home, tasks} mtask ->
   |                                                      ^^^^^

app/Task/CLI.hs:67:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature:
      listTasks :: FilePath -> ProgramT Raw IO ()
   |
67 | listTasks tasksFilePath = raw $ do
   | ^^^^^^^^^

app/Task/CLI.hs:71:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature:
      listPriorities :: FilePath -> ProgramT Raw IO ()
   |
71 | listPriorities tasksFilePath = raw $ do
   | ^^^^^^^^^^^^^^

Automated Documentation of CLI Programs

It would be great to have better automated documentation, though it is very low priority for me, as I don't find myself needing anything more than what I have now.

show example in function documentation

It would be very helpful to have an example argument in the function descriptions.

example: argument

commandName arumentValue

example: keyed argument

commandName -key value

more flexible option prefixes

A lot of CLIs have a double dash -- option with a single dash - abbreviation.

It would be nice to allow -- options.
Also, it would be nice to allow abbreviation of -- with a -.

Add support for GHC 9

Thank you for all the efforts toward building this package. However, I want to bring to your notice this error that the compiler threw when trying to build using GHC version 9.2.5

image

Do you mind if I open a PR to make a fix for this?

option combined with value

Some options are easier to use if the option is combined with the value like the sed option -i[extension].

Other CLIs use the assignment operator like HandBrakeCLI with the option -v, --verbose[=number] Be verbose (optional argument: logging level)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.