0.0
No release in over 3 years
Low commit activity in last 3 years
Parse CL args according to regexen
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

~> 0.1.1
 Project Readme

lab42_rgxargs

parse args according to regexen

Issue Count CI Coverage Status Gem Version Gem Downloads

Yet Another Command Line Argument Parser?

Short answer, Yes, long answer, Yes because I need one that does what I want.

How does it work?

Let us speculate about that:

Context Setup for speculations

Given the default parser

      let(:parser) {Lab42::Rgxargs.new}

      private
      def os(**kwds); L42::Map.new(**kwds) end

What Do I want?

Simple usage with minimum boilerplate

Context No Config Out Of The Box

Then I can parse ruby syntax based arguments

      kwds, positionals, _errors = parser.parse(%w{a: 42 hello :b})
      expect(kwds).to eq(os(a: "42", b: true))
      expect(positionals).to eq(%w{hello})

And the only error one can get with this null configuration is a missing value for trailing keyword arg

      kwds, _, errors = parser.parse(%w{a: b: a:})
      expect(kwds).to eq(os(a: "b:"))
      expect(errors).to eq([[:missing_required_value, :a]])

And for those who prefer to use pattern matching, like YHS

      parser.parse(%w{a: b: a:}) => {a:}, [], errors
      expect(a).to eq("b:")
      expect(errors).to eq([[:missing_required_value, :a]])

And unprovided arguments equal nil

    expect(parser.parse([]).first.verbose).to be_nil

Context Hash instead of L42::Map?

Although it can be very convenient to return an OpenStruct instance for the parsed options a Hash instance might be a better choice, especially for pattern matching as OpenStruct does not implement that protocol :(

Given a parser configured to return options as a Hash

      let(:parser) { Lab42::Rgxargs.new(l42_map: false) }
      let(:posix) { Lab42::Rgxargs.new(l42_map: false, posix: true) }

Then we just get a good ol' Hash ;)

    parser.parse(%w[a: 1 b: 2]) => {a: alpha, b: beta}, _, _
    expect(alpha.to_i + beta.to_i).to eq(3)

    posix.parse(%w[-n --a=1]) => {a: alpha, n: true}, _, _
    expect(alpha).to eq("1")

Context And What About Posix?

Given a posix enabled parser

    let(:parser) { Lab42::Rgxargs.new(posix: true) }

Then I can parse posix style options

      kwds, positionals, _errors = parser.parse(%w{-xy --a=42 --hello=b hello})
      expect(kwds).to eq(os(x:true, y:true, a: "42", hello: "b"))
      expect(positionals).to eq(%w{hello})

And we can use -- to get positionals with leading -s and we also accept long flags (therefore the = is needed for values)

      kwds, positionals, _errors = parser.parse(%w{-xy --a -- --hello=b})
      expect(kwds).to eq(os(x:true, y:true, a: true))
      expect(positionals).to eq(%w{--hello=b})

Something A Little Bit More Elaborate?

like

Context: Conversion Of Keyword Parameters

Given this additional configuration, with a guard

    before { parser.add_conversion(:lower, %r{\A([-+]?\d+)}, &:to_i) }

Then the parsed value for the lower argument will be an Integer, while the other parsed values remain Strings

    expect(parser.parse(%w[lower: 42 upper: 43]).first)
      .to eq(os(lower: 42, upper: "43"))
Context: Withe predefined matchers

And such common converters are predefined of course, and thusly

      parser.add_conversion(:alpha, :int)
      expect(parser.parse(%w[alpha: 42]).first)
        .to eq(os(alpha: 42))

And you can see all predefined matchers as follows

    predefined_matchers =
      %w[ existing_dirs int int_list int_range list range ]
      .join("\n\t")
    expect(parser.predefined_matchers).to eq(predefined_matchers)

And We can also just pass in the converter without a guard

    parser.add_conversion(:maybe_int, &:to_i)
      expect(parser.parse(%w[maybe_int: fourtytwo]).first)
        .to eq(os(maybe_int: 0))

But converters with guards do return meaningful error messages

    _, _, errors = parser.parse(%w{lower: hello})
    expect(errors).to eq([[:syntax_error, :lower, "hello does not match (?-mix:\\A([-+]?\\d+))"]])

Context: General Syntax

Sometimes we want to define syntax for positional parameters too.

This can be done with the add_syntax method.

And therefore

      parser.add_syntax(%r{(\d+)\.\.(\d+)}, ->(*captures){ Range.new(*captures.map(&:to_i)) })
      _, my_range, _ = parser.parse(%w{1..3})
      expect(my_range.first).to eq(1..3)

And we have some predefined syntaxes, of course

      parser.add_syntax(:range)
      _, my_range, _ = parser.parse(%w{1..3})
      expect(my_range.first).to eq(1..3)

And they are of course applied to all arguments, e.g.

      parser.add_syntax(:range)
      parser.add_syntax(:list)
      _, pos , _ = parser.parse(%w{ 1,2 1..3 42})
      list, range, answer = pos
      expect(list).to eq(%w{1 2}) # N.B. Strings
      expect(range).to eq(1..3)
      expect(answer).to eq(%w{42})  # N.B. Strings

And there is a special int_list converter available

      parser.add_syntax(:int_list)
      _, list, _ = parser.parse(%w{1,2,4})
      expect(list.first).to eq([1,2,4])

And Of course a add_syntax (for positionals) and add_conversion (for keywords) can be mixed using the same converters under the hood

      parser.add_conversion([:lower, :upper], :int)
      parser.add_syntax([:int, :range])

      kwds, pos, _ = parser.parse(%w[42 lower: 1 upper: 2 1..3])
      expect(kwds).to eq(os(lower: 1, upper: 2))
      expect(pos).to eq([42, 1..3])

Context Giving Names to Syntaxes

Often times you might want to distinguish arguments by their syntax and not by their position.

Imagine a tool that compares a file's access time with a timestamp, then it might make sense to name the positionals as follows:

And therefore we have

      parser.add_syntax(%r{\A(\d+:\d+:\d+)\z}, ->(ts){ ts }, as: :timestamp)
      kwds, positionals, _ = parser.parse(%w[foo 20:10:10])
      expect(kwds.timestamp).to eq("20:10:10")
      expect(positionals).to eq(%w{foo})

And for more complex possibilities of timestamps one can use a little DSL

      parser.define_arg(:timestamp) do
        syntax(%r{\A(\d+:\d+)\z}, &:itself)
        syntax(%r{\A(\d{6,})\z}) { |capture| capture.to_i }
      end

      kwds, _, _ = parser.parse(%w[123456])
      expect(kwds.timestamp).to eq(123456)

Context Constraints

Context: Allowing Keyword Params

And Allowing keywords means, all others are forbidden

    parser.allow_kwds(:version)

    _, _, errors = parser.parse(%w[vision: 41])
    expect(errors) == [[:unauthorized_kwd, :vision]]

But the allowed work as expected

    parser.allow_kwds(:version)

    kwds, _, errors = parser.parse(%w[version: 42])
    expect(errors).to be_empty
    expect(kwds.version).to eq("42")

Context: Require Keyword Params

And if required keywords are absent...

    parser.require_kwds(:from)
    parser.add_conversion(:to, :int, :required)

    _, _, errors = parser.parse(%w[version: 42])
    expect(errors).to eq([
     [:required_kwd_missing, :from],
     [:required_kwd_missing, :to]
    ])

But if they are present...

    parser.require_kwds(:from)
    parser.add_conversion(:to, :int, :required)

    kwds, _, errors = parser.parse(%w[to: 2 from: 1])
    expect(errors).to be_empty
    expect(kwds).to eq(os(from: "1", to: 2))

Context Syntactic Sugar

Now all these API calls might not be your cup of tea, so let us add Syntactic Sugar to your Cup of Tea:

Given a simple definition for converting required parameters

    let :parser do
      Lab42::Rgxargs.new do
        needs  :n, &:to_i
        allows :m, &:to_i
      end
    end

Then the conversion works of course as expected

  kwds, _, _ = parser.parse(%w[n: alpha, m: 42])
  expect(kwds).to eq(os(n: 0, m: 42))
Context: Using predefined matches in the DSL

Given the directories dir1 and dir2 in the fixtures directory

    let :parser do
      Lab42::Rgxargs.new do
        allows :dirs, :existing_dirs
      end
    end

Then we can parse the keyword arguments with existing dirs w/o an error

    glob = 'spec/fixtures/dir*'
    kwds, _, _ = parser.parse(["dirs:", glob])
    expect(kwds.dirs.sort).to eq(%w[spec/fixtures/dir1 spec/fixtures/dir2])

LICENSE

Copyright 202[0,1,2] Robert Dober robert.dober@gmail.com,

Apache-2.0 c.f LICENSE