Git Product home page Git Product logo

purescript-optlicative's Introduction

purescript-optlicative

An applicative-style CLI option parsing lib with accumulative errors and type-based command parsing.

Usage by example

Let's say we have a CLI program called p, and we want it to accept a flag that determines whether its output will be colored or not.

We want users to express this intention by writing p --color.

Somewhere in our code we have a Config type that represents options passed in:

type Config r = {color :: Boolean | r}

To parse this, we use the flag combinator:

parseConfig :: Optlicative (Config ())
parseConfig = {color: _} <$> flag "color" Nothing

If we want to extend Config to include an --output option that takes a filename argument, that's easy too:

type Config' r = Config (output :: String | r)

parseConfig' :: Optlicative (Config' ())
parseConfig' = {color: _, output: _}
  <$> flag "color" Nothing
  <*> string "output" Nothing

Suddenly we think of several more boolean flags we want to support:

type Config'' r = Config' (humanReadable :: Boolean, metricUnits :: Boolean | r)

parseConfig'' :: Optlicative (Config'' ())
parseConfig'' = {color: _, output: _, humanReadable: _, metricUnits: _}
  <$> flag "color" Nothing
  <*> string "output" Nothing
  <*> flag "human-readable" Nothing
  <*> flag "metric-units" Nothing

But now if we want users to use every one of these flags, we require them to write something like p --color --human-readable --metric-units --output "./output.txt". That's way too long!

If we want to allow single-hyphen, single-character options we just change a few Nothing's:

parseConfig2 :: Optlicative (Config'' ())
parseConfig2 = {color: _, humanReadable: _, metricUnits: _, output: _}
  <$> flag "color" (Just 'c')
  <*> flag "human-readable" (Just 'H')
  <*> flag "metric-units" (Just 'm')
  <*> string "output" Nothing

Now our users can write p -cHm --output "./output.txt". Much better!

Error messages

By default, if the --output option is missing the following error will be generated: "Missing option: Option 'output' is required."

Our flags won't produce any error messages, since if a user doesn't supply a flag we assume they want it to be false.

But we can also change the error message, for example by changing our --output parser to string "output" (Just "I need to know where to place my output!")

Error messages are accumulated via the semigroup-based V applicative functor, meaning that if the user gives input that causes multiple errors, each one can be shown.

Optional values

What if we want to provide a default output directory, and don't want to require the user to always supply it? We can use optional, withDefault or withDefaultM:

parseConfig4 :: Optlicative (Config'' ())
parseConfig4 = {color: _, _, humanReadable: _, metricUnits: _, output: _}
  <$> flag "color" (Just 'c')
  <*> flag "human-readable" (Just 'H')
  <*> flag "metric-units" (Just 'm')
  <*> withDefault "./output.txt" (string "output" Nothing)

Note that none of these three combinators will fail.

Custom data-types

If we have a way of reading values from a String (specifically a function f of type String -> F a) then we can use optF f to read such a value. Any errors in the F monad get turned into OptErrors in the Optlicative functor.

Example:

readTupleString :: String -> F (Tuple Int Int)

optTuple :: Optlicative (Tuple Int Int)
optTuple = optF readTupleString "point" (Just "Points must be in the form '(x,y)'")

Then the option --point (3,5) won't error if and only if readTupleString "(3,5)" does not error.

Options that accept multiple arguments

Again, assuming we have a function read :: String -> F a for some type a, we can use manyF read :: Int -> String -> Maybe ErrorMsg -> Optlicative (List a).

In this case, Int represents the number of arguments expected (none of which may start with a hyphen character).

Running the parser

optlicate :: Constraints => Record optrow -> Preferences a -> Effect {cmd :: Maybe String, value :: Value a}

Preferences is a record:

{ errorOnUnrecognizedOpts :: Boolean
, usage :: Maybe String
, globalOpts :: Optlicative a
}

A defaultPreferences :: Preferences Void is available.

The errorOnUnrecognizedOpts field indicates whether an error should be generated if a user passes in an option that isn't recognized by the parser.

The usage field will print a given message in case of any error.

globalOpts is for options which don't match a given command; for more on commands see the next section.

The value field has type Value a, which is a type synonym for V (List OptError) a. This means you'll need to use unV from Data.Validation.Semigroup, handling any possible errors, in order to have access to the value of type a.

Dealing with Commands

Let's take a closer look at the "Constraints" part of the optlicate type signature. The actual signature starts like this:

optlicate :: forall optrow a e. Commando optrow => Record optrow -> Preferences a -> ...

The important part is the Commando typeclass constraint. It applies only to a certain class of rows -- similar to homogenous rows, but a bit more generalized than what usually comes to mind. Let's look at an example:

type MyConfig =
  ( command :: Opt Config
    ( more :: Opt Config ()
    )
  , second :: Opt Config ()
  )

Note that this type has not only breadth but also depth. The Opt type is a datatype around Optlicative but with extra type information in the second argument: this is what allows us to nest commands, treating every possible command (and associated options) as a tree-like structure (a record), where each node (field) represents a pair of a command entered, and the options for that command.

For example, if the user had run p command --help, the parser would then recognize this, and match the Optlicative Config associated with the command command and run it against the --help flag.

Any command, if it exists, will be placed into the cmd field of the result -- if the program is used like p command more, then cmd = Just "more".

Let's look at the first argument to optlicate. In our example case, we'd need a value of type Record MyConfig. If we can construct a value for just one field, we can construct them all. And those values are built using Opt, as suggested by the definition of MyConfig:

data Opt (a :: Type) (row :: # Type) = Opt (Optlicative a) (Record row)

Opts are pairs of Optlicatives and a record, which allows us to continue chaining new Optlicatives. With this in mind, we can construct what we want:

myConfig :: Record MyConfig
myConfig =
  { command: Opt commandOptlicative
    { more: Opt moreOptlicative {}
    }
  , second: Opt secondOptlicative {}
  }

We can also use endOpt to get rid of those empty records if we wish: more: endOpt moreOptlicative.

More examples

See the test/ folder.

Unsupported/future features

  • "Unsupported command" errors: when a command is given but does not match anything
  • passthrough options (as in program --program-opt -- --passthrough-opt)
  • use of single characters for options instead of just flags
  • other things I haven't thought of

Installation

  • Using bower:
> bower install purescript-optlicative
  • Or,

Add it to a package set!

purescript-optlicative's People

Contributors

thimoteus avatar garyb avatar ixmatus avatar cryogenian avatar

Stargazers

Kamil Adam avatar Tim Kersey avatar Georgi Bojinov avatar Pete Murphy avatar Evan Relf avatar Adam Recvlohe avatar Thomas Honeyman avatar Yue Zhuo avatar Tim de Putter avatar bouzuya avatar Simon Hafner avatar Conrad Steenberg avatar Felix Schlitter avatar Adrian Sieber avatar harapan avatar Paul Young avatar Denis Stoyanov avatar  avatar Woodson Delhia avatar John Mendonça avatar Andrejs Agejevs avatar Vasiliy Yorkin avatar Claudia Doppioslash avatar Marcin Biernat avatar  avatar andretshurotshka avatar Alex Gryzlov avatar mkay avatar paluh avatar Arthur Xavier avatar Christoph Hegemann avatar

Watchers

James Cloos avatar  avatar Christoph Hegemann avatar Kamil Adam avatar  avatar

purescript-optlicative's Issues

Provide function for parsing options without a command

Hi,

Thanks for great library!

I'm not sure if I missing something but it seems that current API forces users to define commands record to run oplicate.
I think that it would be useful to provide a function which facilitates use of Optlicative parser directly without additional record/command definition.

P.S.
I can try to provide a PR of course.

new release?

last release results in warning:

in module Node.Optlicative.Internal

  Name takeDrop was shadowed.

in value declaration takeDropWhile

which is fixed in master, but there is no new public release :(

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.