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
2025
2026
 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