Class: FlexArgs

Inherits:
Object
  • Object
show all
Defined in:
lib/flex_args.rb,
lib/flex_args/parser.rb,
lib/flex_args/version.rb,
lib/flex_args/transform.rb,
lib/flex_args/constraint.rb

Overview

## Abstract

This is FlexArgs, and I am Robert Dober

It will allow for a versatile parsing of command line arguments.

By default the behavior is as follows

### Positional arguments

Are all arguments that do not:

- start with a `-`
- contain any of the following characters: `:,`
- contain the string `".."`

Therefore the following arguments will be parsed as

“‘spec # Only default positional arguments

parse(%w[hello world.glob 13 a-b]).positionals => %w[hello world.glob 13 a-b]

“‘

Now flags start with a - getting us many single letter flags or a -- as in Posix

“‘spec # Flags and positionals

args = parse(%w[hello --verbose -world 42])
expect(args.positionals).to eq(%w[hello 42])
expect(args.flags).to eq(Set.new(%i[verbose w o r l d]))

“‘ ### Value arguments

Instead of posix syntax like ‘–key=value` or `–key value` FlexArgs uses the key:value or `key,alpha,beta` syntax.

“‘spec # Just some values

parse(%w[alpha:42 hello:world]).values => {alpha: 42, hello: "world"}

“‘

N.B. that all digits values are converted to integers, as we can see in the following example signs are also taken into account.

#### Ranges

“‘spec # Ranges and signs

input = %w[range:-2..3 offset:+3 -V a/b.rb]
parsed = parse(input)
expect(parsed.positionals).to eq(["a/b.rb"])
expect(parsed.values).to eq(range: -2..3, offset: 3)
expect(parsed.flags).to eq(Set.new([:V]))

“‘

#### Lists

“‘spec # Just a list

parse(%w[a_list:1,2,a,3..4]).values => {a_list: [1, 2, "a", 3..4]}

“‘

But what if we want ‘,` inside a value? You need to double it

“‘spec # Escaping `,`

parse(%w[escaped:1,a,,b,c]).values => {escaped: [1, "a,b", "c"]}

“‘

#### Multiple Values

This is an alternative way to get a list of values

“‘spec # Multiple Values => Lists

parse(%w[list:1 list:2 hello list:3]).values => {list: [1, 2, 3]}

“‘

### Advanced Features

More advanced features are set with methods on the the instance before invoking parse.

These features are

- Aliased flags
- Defaults
- Constraints
   - allowed
   - required
   - domain checks
   - format checks
- Transformations
- Semantic Checks - between multiple values (v0.2)

and they are documented in the rdocs of the corresponding methods

Defined Under Namespace

Modules: Version Classes: Constraint, Parser, Transform

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#alias_definitionsObject (readonly)

Returns the value of attribute alias_definitions.



102
103
104
# File 'lib/flex_args.rb', line 102

def alias_definitions
  @alias_definitions
end

#default_valuesObject (readonly)

Returns the value of attribute default_values.



102
103
104
# File 'lib/flex_args.rb', line 102

def default_values
  @default_values
end

#resultObject (readonly)

Returns the value of attribute result.



102
103
104
# File 'lib/flex_args.rb', line 102

def result
  @result
end

Instance Method Details

#aliases(alias_definitions) ⇒ Object

## Aliases are defined by passing a hash

The key of the hash is the alias and it will be defined as an alias to the value of the hash for each grapheme of the key, here is how that works

“‘spec # Aliases

parser = FlexArgs
  .new
  .aliases(hu: :help, v: :version)

expect(parser.parse(%w[-uv]).flags).to eq(Set.new(%i[help version]))
expect(parser.parse(%w[-h]).flags).to eq(Set.new(%i[help]))

“‘



132
133
134
135
136
137
138
# File 'lib/flex_args.rb', line 132

def aliases(alias_definitions)
  alias_definitions
    .each do |key, value|
      key.to_s.grapheme_clusters.each { define_alias it, value }
    end
  self
end

#allow(*values) ⇒ Object

## Allowed values and flags

As soon as this method is called the parser will check that all values in args are allowed

Values are either identified by their name (a Symbol) or by their name and default value (a pair of Symbol and any value

“‘spec

parser = FlexArgs
  .new
  .allow(:min, [:max, 3])

expect(parser.parse(%w[min:1]).values)
  .to eq(min: 1, max: 3)

“‘

Therefore the following args are illegal and an ArgumentError is raised

“‘spec # Argument Error for unallowed value

expect do
  FlexArgs
    .new
    .allow(:n)
    .parse(%w[a:2])
end
  .to raise_error(
    ArgumentError,
    "unallowed value arg a:"
  )

“‘



176
177
178
179
# File 'lib/flex_args.rb', line 176

def allow(*values)
  values.each { allow_value it }
  self
end

#allowed?(value) ⇒ Boolean

Returns:

  • (Boolean)


181
182
183
184
# File 'lib/flex_args.rb', line 181

def allowed?(value)
  return true unless @allowed_values
  @allowed_values.member?(value)
end

#constrain(value, *constraints, &block) ⇒ Object

## Constraints

Constraints are defined with the `constraint` method.

**N.B.** defined Constraints **after** allow unless you
want to explicitly add the value arg with `allow`

### Simple forms

#### Regexp constraint

“‘spec # regexp constraints

 parser = FlexArgs
            .new
            .constrain(:n, %r{\A \d+ \z}x)

expect(parser.parse(%w[n:42]).values).to eq(n: 42)

expect { parser.parse(%w[n:42a]) }
  .to raise_error(ArgumentError, 'regexp constraint n: (?x-mi:\A \d+ \z) violated by value "42a"')

“‘

#### Range constraints

“‘spec # range constraints

  parser = FlexArgs
             .new
             .constrain(:n, 2..3)

expect(parser.parse(%w[n:2]).values).to eq(n: 2)

expect { parser.parse(%w[n:42a]) }
  .to raise_error(ArgumentError, 'range constraint n: 2..3 violated by value "42a"')

expect { parser.parse(%w[n:42]) }
  .to raise_error(ArgumentError, 'range constraint n: 2..3 violated by value "42"')

“‘

#### Combining Constraints

More than one constraint can be imposed on a value

Firstly one can combine any constraint with a custom constraint.

N.B. That in this case the custom constraint is executed last, therefore

“‘spec # Order of constraints

parser = FlexArgs.new
  .constrain(:n, 1..10) { it == 5 ? [:ok, 100] : [:ok, 2*it] }

expect(parser.parse(%w[n:5]).values).to eq(n: 100)

“‘

However if we had defined the custom constraint (which actually is a transformer as it always returns an :ok value, the range constraint would fail

“‘spec # Bad order

parser = FlexArgs.new
  .constrain(:n) { it == 5 ? [:ok, 100] : [:ok, 2*it] }
  .constrain(:n, 1..10)

expect { parser.parse(%w[n:5]) }
.to raise_error(ArgumentError)

“‘

As mentioned before, custom constraints that always return :ok values are indeed transformers and can be written more with the transform [method below](/FlexArgs.html#method-i-transform-label-Transformers)

Please find the documentation about more constraints [here](/FlexArgs/Constraint.html)



264
265
266
267
268
269
# File 'lib/flex_args.rb', line 264

def constrain(value, *constraints, &block)
  @allowed_values << value.to_sym if @allowed_values
  @constraints[value.to_sym] << Constraint.new(value, *constraints) unless constraints.empty?
  @constraints[value.to_sym] << Constraint.new(value, &block) if block
  self
end

#constraints(key) ⇒ Object



186
# File 'lib/flex_args.rb', line 186

def constraints(key) = @constraints[key]

#parse(args) ⇒ Object



104
105
106
107
108
109
110
111
112
# File 'lib/flex_args.rb', line 104

def parse(args)
  result = Parser.new(self).parse(args)
  case result
  in [:ok, result1]
    result1
  in [:error, errors]
    raise ArgumentError, errors.join("\n")
  end
end

#transform(value, *transforms, &transformer) ⇒ Object

## Transformers

Transformers and Constraints are executed in the order they are defined

“‘spec # Transformer assures constraint passes

parser = FlexArgs.new
           .transform(:word) { it.downcase }
           .constrain(:word, "a".."z")

expect(parser.parse(%w[word:A]).values).to eq(word: "a")

“‘

And therefore

“‘spec # Transformer called too late to assure constraint passes

parser = FlexArgs.new
           .constrain(:word, "a".."z")
           .transform(:word) { it.downcase }

expect { parser.parse(%w[word:A]) }.to raise_error(ArgumentError)

“‘



295
296
297
298
299
300
# File 'lib/flex_args.rb', line 295

def transform(value, *transforms, &transformer)
  @allowed_values << value.to_sym if @allowed_values
  @constraints[value.to_sym] << Transform.new(*transforms) unless transforms.empty?
  @constraints[value.to_sym] << Transform.new(&transformer) if transformer
  self
end

#valuesObject

Just returns the parsed values, or their default values if a default was provided and the value was not present in the provided arguments.



307
308
309
# File 'lib/flex_args.rb', line 307

def values
  default_values.merge(values_from_args)
end