Git Product home page Git Product logo

xopts's Introduction

XOpts

Build Status Coverage Status Hex.pm Hex.pm Hex.pm

XOpts a Command Line Argument Parser on Steroids.

Ridiculous Speed Starting Guide

Zero Configuration, (almost) Zero Value

iex(1)> XOpts.parse([])
{:ok, %{switches: %{}, keywords: %{}, positionals: [], errors: []}}

iex(2)> XOpts.parse(~W[alpha beta gamma])
{:ok, %{switches: %{}, keywords: %{}, positionals: ~W[alpha beta gamma], errors: []}}

iex(3)> XOpts.parse(~W[:verbose alpha level: 42 beta gamma])
{:ok, %{switches: %{verbose: true}, keywords: %{level: "42"}, positionals: ~W[alpha beta gamma], errors: []}}

Posix is widely used and although it is ugly (beauty lies you know in whose eyes), we can accept it by default

iex(4)> XOpts.parse(~W[--verbose alpha --level 42 beta gamma])
{:ok, %{switches: %{verbose: true}, keywords: %{level: "42"}, positionals: ~W[alpha beta gamma], errors: []}}

Configure for Great Incredible Ridiculous Value

iex(0)> configuration = %{
...(0)>   allowed_keywords: %{
...(0)>     alpha2: {:string, default: "fr"}
...(0)>   },
...(0)>   required_keywords: %{
...(0)>     value: {:int, min: 1} # could be written as: value: :positive_int
...(0)>   }

Incredible Speed Starting Guide

Great Speed Starting Guide

Posix Or Not Posix?

If we do not want to parse posix switches or keywords we can disable them

iex(5)> XOpts.parse(~W[--verbose alpha --level 42 beta gamma], posix: false)
{:ok, %{switches: %{}, keywords: %{}, positionals: ~W[--verbose alpha --level 42 beta gamma], errors: []}}

For fairness we can also disable keyword style arguments:

iex(6)> XOpts.parse(~W[:verbose alpha level: 42 beta gamma], keyword_style: false)
{:ok, %{switches: %{}, keywords: %{}, positionals: ~W[:verbose alpha level: 42 beta gamma], errors: []}}

Simple Configuration

Restricting and Typing Keyword Arguments

As we have seen in the examples configuration can be passed as keyword arguments, however for more complex configuration that might become tedious and we therefore will pass in a map.

iex(7)> configuration = %{
...(7)>   allowed_keywords: %{
...(7)>     count: :int,
...(7)>     message: :string }}
...(7)> XOpts.parse(~W[hello count: 42])
{:ok, %{switches: %{}, keywords: %{count: 42}, positionals: ~W[hello], errors: []}}

Did you notice the type conversion of the int parameter? Of course you did!

Now the user is alerted of misspelled or badly typed arguments:

iex(8)> configuration = %{
...(8)>   allowed_keywords: %{
...(8)>     count: :int,
...(8)>     message: :string }}
...(8)> XOpts.parse(~W[hello cont: 42])
{:error,
  %{switches: %{},
    keywords: %{}, 
    positionals: ~W[hello], 
    errors: [{:forbidden, keyword: "cont", value: 42}]}}

She will also be alerted of badly typed arguments

iex(9)> configuration = %{
...(9)>   allowed_keywords: %{
...(9)>     count: :int,
...(9)>     message: :string }}
...(9)> XOpts.parse(~W[hello cont: alpha])
{:error, 
  %{switches: %{},
    keywords: %{},
    positionals: ~W[hello],
    errors: [{:invalid_type, keyword: "cont", value: "alpha", requested: :int}]}}

Requiring and Typing Keyword Arguments

Sometimes keyword arguments need to be present

iex(10)> configuration = %{
...(10)>   required_keywords: %{
...(10)>     n: :non_negative_int }}
...(10)> XOpts.parse(~W[n: 2])
{:ok, %{switches: %{}, keywords: %{n: 2}, positionals: ~W[], errors: []}}

and when they are not

iex(11)> configuration = %{
...(11)>   required_keywords: %{
...(11)>     n: :non_negative_int }}
...(11)> XOpts.parse(~W[n: 2])
{:ok,
  %{switches: %{}, keywords: %{n: 2}, positionals: ~W[], errors: [{:missing, keyword: "n"}]}}

or violate a constraint

iex(12)> configuration = %{
...(12)>   required_keywords: %{
...(12)>     n: :non_negative_int }}
...(12)> XOpts.parse(~W[n: 2])
{:error,
  %{switches: %{},
    keywords: %{n: 2},
    positionals: [],
    errors: [{:constraint_violation, keyword: "n", value: -1, range: [0]}]}}

A wild combination of errors

iex(13)> configuration = %{
...(13)>    allowed_keywords: %{
...(13)>      lang: ~r(A [[:alnum:]]{2} z)x},
...(13)>    required_keywords: %{
...(13)>      base: :any},
...(13)>    allowed_switches: []}
...(13)> input = ~W[ --verbose lang: frr ]
...(13)> XOpts.parse(input, configuration)
{:error,
  %{
    switches: %{},
    keywords: %{},
    positionals: [],
    errors: [
      {:forbidden, switch: "verbose"},
      {:missing, keyword: "base"},
      {:constraint_violation, keyword: "lang", value: "frr", constraint: ~r(A [[:alnum:]]{2} z)x}]}}

Which was, of course, completely unnecessary

iex(14)> configuration = %{
...(14)>   allowed_keywords: %{
...(14)>     lang: ~r(A [[:alnum:]] {2} z)x},
...(14)>   required_keywords: %{base: :any},
...(14)>   allowed_switches: []}
...(14)> input = ~W[ lang: fr --base zero cinq ]
...(14)> XOpts.parse(input, configuration)
{:ok,
  %{
    switches: %{},
    keywords: %{base: "zero", lang: "fr"},
    positionals: ~W[cinq],
    errors: []}}

Defaults

Allowed keyword arguments as well as positionals can have default values

iex(15)> configuration = %{
...(15)>    allowed_keywords: %{
...(15)>      n: [:int, default: 42] }}
...(15)> XOpts.parse([], configuration)
{:ok,
  %{switches: %{},
    keywords: %{n: 42},
    poistionals: [],
    errors: []}}

which, of course can be overridden:

iex(16)> configuration = %{
...(16)>    allowed_keywords: %{
...(16)>      n: [:int, default: 42] }}
...(16)> XOpts.parse(~W[n: 11], configuration)
{:ok,
  %{switches: %{},
    keywords: %{n: 11},
    poistionals: [],
    errors: []}}

Strict Order

The first possibility do assure the Order Of Things (TM: Dominon) is just to assure that keyword arguments and switches come before poistional arguments.

This can be accomplished with the strict: true parameter

iex(17)> XOpts.parse(~W[hello :world], strict: true)
{:error,
  %{switches: %{}, keywords: %{}, positionals: ~W[hello :world], errors: [
    {:ordered_argument_error, world: "after positional"}
  ]}}

or simply by calling the parse_strictly/2 function

iex(18)> XOpts.parse_strictly(~W[hello :world])
{:error,
  %{switches: %{}, keywords: %{}, positionals: ~W[hello :world], errors: [
    {:ordered_argument_error, world: "after positional"}
  ]}}

Of course the End Of Keywords separator :: or -- avoid this

iex(19)> XOpts.parse_strictly(~W[hello :: :world])
{:ok,
  %{switches: %{}, keywords: %{}, positionals: ~W[hello :world], errors: []}}

Advanced Configuration

Constraints on Positionals

It might be necessary to request and or restrict the number of positional parameters

A first example requiring at least two

iex(20)> configuration = %{
...(20)>   nof_postionals: [2]}
...(20)> XOpts.parse(~W[a], configuration)
{:error,
  %{switches: %{}, keywords: %{}, poistionals: ~W[a], errors: [
  {:missing_positional, needed: 2, present: 1}
  ]}
} 

Of course Chuck Norris 5th lemma holds: Enough is enough

iex(21)> configuration = %{
...(21)>   nof_postionals: [2]}
...(21)> XOpts.parse(~W[a b], configuration)
{:ok,
  %{switches: %{}, keywords: %{}, poistionals: ~W[a b], errors: []}}

If we want to restrict the number of positionals it is done with the second number in this list:

iex(22)> configuration = %{
...(22)>   nof_postionals: [1, 2]}
...(22)> XOpts.parse(~W[a b c], configuration)
{:error,
  %{switches: %{}, keywords: %{}, poistionals: ~W[a b c], errors: [
  {:spurious_positional, allowed: 2, present: 3}
  ]}
} 

We can also constrain positional parameters

iex(23)> configuration = %{
...(23)>   positional_constraints: [:int, ~r{AA}]} # Stupid example but well
...(23)> XOpts.parse(~W[ab Bc], configuration)
{:error,
  %{switches: %{}, keywords: %{}, poistionals: ~W[ab Bc],
    errors: [
      {:constraint_violation, positional: 1, value: "ab", constraint: :int},
      {:constraint_violation, positional: 2, value: "Bc", constraint: ~r{AA}},
    ]}}

and use nil for unconstrained positionals between constrained ones:

iex(24)> configuration = %{
...(24)>   positional_constraints: [:int, nil, ~r{AA}]} # Stupid example but well
...(24)> XOpts.parse(~W[ab de Bc], configuration)
{:error,
  %{switches: %{}, keywords: %{}, poistionals: ~W[ab de Bc],
    errors: [
      {:constraint_violation, positional: 1, value: "ab", constraint: :int},
      {:constraint_violation, positional: 3, value: "Bc", constraint: ~r{AA}},
    ]}}

Also note that defining a constraint for a positional parameter does not make it required:

iex(25)> configuration = %{
...(25)>   positional_constraints: [:int, nil, ~r{AA}]} # Stupid example but well
...(25)> XOpts.parse(~W[42 de], configuration)
{:ok,
  %{switches: %{}, keywords: %{}, poistionals: [42, "de"], errors: []}}

Type Conversions

Constraints, like Regex or :string are just checked and if they succeed the value is assigned to the keyword or positional argument as is. :string always succeeds BTW.

However there are other builtin types that will, if the check succeeds, coherce the string into a different form.

:int type

Min, Max, Range

We have already seen an example for that, but there can be constraints added as follows

For simplicity we will use the imported form for doctests from now on, obviously parse/1 is imported from XOpts

iex(26)> parse(~W[42], positional_constraints: [[:int, max: 40]])
{:error,
  %{switches: %{}, keywords: %{}, positionals: [42], errors: [
  {:constraint_violation_error, positional: 1, value: 42, max: 40}
  ]}}

Of course min can also be used.

The parser will also not allow impossible constraints as shown in the next example

iex(27)> parse([], allowed_keywords: %{n: [:int, min: 10, max: 5]})
{:illegal_config, [{:empty_range, keyword: :n, min: 10, max: 5}]}

And defaults need to be in range too:

iex(28)> parse([], allowed_keywords: %{n: [:int, min: 10, default: 0]})
{:illegal_config, [{:illegal_default, keyword: :n, min: 10, default: 0}]}
Concise range specification form

If we use min and max we can just pass a range

iex(29)> parse(~W[n: 40], required_keywords: %{n: 41..50})
{:error,
  %{switches: %{}, keywords: %{n: 40}, positionals: [], errors: [
  {:constraint_violation_error, keyword: :n, value: 40, min: 41, max: 50}
  ]}}

Oh and let us prove that respecting the requirements yields the results we want, too:

iex(30)> parse(~W[n: 42], required_keywords: %{n: 41..50})
{:ok,
  %{switches: %{}, keywords: %{n: 42}, positionals: [], errors: []}}

Some ranges, even open ones, are predefined, as, e.g.

:non_negative_int
iex(31)> parse(~W[n: -1], allowed_keywords: %{n: :non_negative_int})
{:error,
  %{switches: %{}, keywords: %{n: -1}, positionals: [], errors: [
  {:constraint_violation_error, keyword: :n, value: -1, min: 0}
  ]}}

or

:positive_int
iex(32)> parse(~W[n: 0], allowed_keywords: %{n: :positive_int})
{:error,
  %{switches: %{}, keywords: %{n: 0}, positionals: [], errors: [
  {:constraint_violation_error, keyword: :n, value: 0, min: 1}
  ]}}

Installation

If available in Hex, the package can be installed by adding xopts to your list of dependencies in mix.exs:

def deps do
  [
    {:xopts, "~> 0.1.0"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/xopt.

LICENSE

xopts's People

Contributors

robertdober avatar

Stargazers

 avatar

Watchers

 avatar  avatar

xopts's Issues

New option group for exclusive options

Given the following declarations

   group :language, exclusive: true do
     option :elixir
     option :erlang
   end

At most one of the options must be selected.


One can force that exactly one option of the group must be selected via

    group :language, requires: 1 do
       option :elixir
       option :erlang
     end

Provide all options in one array and also allow a requirement range

    group :language, allowed: 2..4 do
       options [:en, :fr, :it, :oc, :pt]
     end

allowed: is just a convenience for
min: and max:

Therefore the following has the exact same semantic as the above:

    group :language, min: 2, max: 4 do
       options [:en, :fr, :it, :oc, :pt]
     end

Better syntax

Now:

     option :version, :integer, 42

Then

     option :version, :integer, default: 42

Now:

     option :version, :integer, nil, :required

Then

     option :version, :integer, required: true

Now

   option :elixir, :boolean, :group, :language
   option :erlang, :boolean, :group, :language
   group_option :all, for: :language

Then

  option_group :languages do
    option :elixir # boolean is implicit
    option :erlang
    # a set them all option named `--all-languages` will be created automatically
  end

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.