tty-option 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +1653 -1
- data/lib/tty/option.rb +63 -4
- data/lib/tty/option/aggregate_errors.rb +95 -0
- data/lib/tty/option/conversions.rb +126 -0
- data/lib/tty/option/converter.rb +63 -0
- data/lib/tty/option/deep_dup.rb +48 -0
- data/lib/tty/option/dsl.rb +105 -0
- data/lib/tty/option/dsl/arity.rb +49 -0
- data/lib/tty/option/dsl/conversion.rb +17 -0
- data/lib/tty/option/error_aggregator.rb +35 -0
- data/lib/tty/option/errors.rb +144 -0
- data/lib/tty/option/formatter.rb +389 -0
- data/lib/tty/option/inflection.rb +50 -0
- data/lib/tty/option/param_conversion.rb +34 -0
- data/lib/tty/option/param_permitted.rb +30 -0
- data/lib/tty/option/param_validation.rb +48 -0
- data/lib/tty/option/parameter.rb +310 -0
- data/lib/tty/option/parameter/argument.rb +18 -0
- data/lib/tty/option/parameter/environment.rb +20 -0
- data/lib/tty/option/parameter/keyword.rb +15 -0
- data/lib/tty/option/parameter/option.rb +99 -0
- data/lib/tty/option/parameters.rb +157 -0
- data/lib/tty/option/params.rb +122 -0
- data/lib/tty/option/parser.rb +57 -3
- data/lib/tty/option/parser/arguments.rb +166 -0
- data/lib/tty/option/parser/arity_check.rb +34 -0
- data/lib/tty/option/parser/environments.rb +169 -0
- data/lib/tty/option/parser/keywords.rb +158 -0
- data/lib/tty/option/parser/options.rb +273 -0
- data/lib/tty/option/parser/param_types.rb +51 -0
- data/lib/tty/option/parser/required_check.rb +36 -0
- data/lib/tty/option/pipeline.rb +38 -0
- data/lib/tty/option/result.rb +46 -0
- data/lib/tty/option/section.rb +26 -0
- data/lib/tty/option/sections.rb +56 -0
- data/lib/tty/option/usage.rb +166 -0
- data/lib/tty/option/usage_wrapper.rb +58 -0
- data/lib/tty/option/version.rb +3 -3
- metadata +37 -3
data/lib/tty/option.rb
CHANGED
@@ -1,9 +1,68 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "option/conversions"
|
4
|
+
require_relative "option/dsl"
|
5
|
+
require_relative "option/errors"
|
6
|
+
require_relative "option/parser"
|
7
|
+
require_relative "option/formatter"
|
8
|
+
require_relative "option/version"
|
4
9
|
|
5
10
|
module TTY
|
6
11
|
module Option
|
7
|
-
|
8
|
-
|
9
|
-
|
12
|
+
# Enhance object with command line option parsing
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def self.included(base)
|
16
|
+
base.module_eval do
|
17
|
+
include Interface
|
18
|
+
extend DSL
|
19
|
+
extend Inheritance
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Inheritance
|
24
|
+
# When class is inherited copy over parameter definitions
|
25
|
+
# This allows for definition of global parameters without
|
26
|
+
# affecting child class parameters and vice versa.
|
27
|
+
def inherited(subclass)
|
28
|
+
subclass.instance_variable_set(:@parameters, @parameters.dup)
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Interface
|
34
|
+
# The parsed parameters
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def params
|
38
|
+
@__params ||= Params.create
|
39
|
+
end
|
40
|
+
|
41
|
+
# Parse command line arguments
|
42
|
+
#
|
43
|
+
# @param [Array<String>] argv
|
44
|
+
# the command line arguments
|
45
|
+
# @param [Hash] env
|
46
|
+
# the hash of environment variables
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def parse(argv = ARGV, env = ENV, check_invalid_params: true,
|
50
|
+
raise_on_parse_error: false)
|
51
|
+
parser = Parser.new(self.class.parameters,
|
52
|
+
check_invalid_params: check_invalid_params,
|
53
|
+
raise_on_parse_error: raise_on_parse_error)
|
54
|
+
@__params = Params.create(*parser.parse(argv, env))
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Provide a formatted help usage for the configured parameters
|
59
|
+
#
|
60
|
+
# @return [String]
|
61
|
+
#
|
62
|
+
# @api public
|
63
|
+
def help(**config, &block)
|
64
|
+
Formatter.help(self.class.parameters, self.class.usage, **config, &block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end # Option
|
68
|
+
end # TTY
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
require_relative "usage_wrapper"
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
module Option
|
9
|
+
class AggregateErrors
|
10
|
+
include Enumerable
|
11
|
+
include UsageWrapper
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
def_delegators :@errors, :size, :empty?, :any?, :clear
|
15
|
+
|
16
|
+
# Create an intance from the passed error objects
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(errors = [])
|
20
|
+
@errors = errors
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add error
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def add(error)
|
27
|
+
@errors << error
|
28
|
+
error
|
29
|
+
end
|
30
|
+
|
31
|
+
# Enumerate each error
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# errors = AggregateErrors.new
|
35
|
+
# errors.each do |error|
|
36
|
+
# # instance of TTY::Option::Error
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def each(&block)
|
41
|
+
@errors.each(&block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# All error messages
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# errors = AggregateErrors.new
|
48
|
+
# errors.add TTY::OptionInvalidArgument.new("invalid argument")
|
49
|
+
# errors.messages
|
50
|
+
# # => ["invalid argument"]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def messages
|
54
|
+
map(&:message)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Format errors for display in terminal
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# errors = AggregateErrors.new
|
61
|
+
# errors.add TTY::OptionInvalidArgument.new("invalid argument")
|
62
|
+
# errors.summary
|
63
|
+
# # =>
|
64
|
+
# # Error: invalid argument
|
65
|
+
#
|
66
|
+
# @param [Integer] :width
|
67
|
+
# @param [Integer] :indent
|
68
|
+
#
|
69
|
+
# @return [String]
|
70
|
+
#
|
71
|
+
# @api public
|
72
|
+
def summary(width: 80, indent: 0)
|
73
|
+
return "" if count.zero?
|
74
|
+
|
75
|
+
output = []
|
76
|
+
space_indent = " " * indent
|
77
|
+
if messages.count == 1
|
78
|
+
message = messages.first
|
79
|
+
label = "Error: "
|
80
|
+
output << "#{space_indent}#{label}" \
|
81
|
+
"#{wrap(message, indent: indent + label.length, width: width)}"
|
82
|
+
else
|
83
|
+
output << space_indent + "Errors:"
|
84
|
+
messages.each_with_index do |message, num|
|
85
|
+
entry = " #{num + 1}) "
|
86
|
+
output << "#{space_indent}#{entry}" \
|
87
|
+
"#{wrap(message.capitalize, indent: indent + entry.length,
|
88
|
+
width: width)}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
output.join("\n")
|
92
|
+
end
|
93
|
+
end # AggregateErrors
|
94
|
+
end # Option
|
95
|
+
end # TTY
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "converter"
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
module Option
|
7
|
+
module Conversions
|
8
|
+
extend Converter
|
9
|
+
|
10
|
+
TRUE_VALUES = /^(true|y(es)?|t|1)$/i.freeze
|
11
|
+
FALSE_VALUES = /^(false|n(o)?|f|0)$/i.freeze
|
12
|
+
|
13
|
+
# @api public
|
14
|
+
def self.raise_invalid_argument(conv_name, val)
|
15
|
+
raise ConversionError,
|
16
|
+
format("invalid value of %<value>s for %<conv>s conversion",
|
17
|
+
value: val.inspect, conv: conv_name.inspect)
|
18
|
+
end
|
19
|
+
|
20
|
+
convert :bool, :boolean do |val|
|
21
|
+
case val.to_s
|
22
|
+
when TRUE_VALUES
|
23
|
+
true
|
24
|
+
when FALSE_VALUES
|
25
|
+
false
|
26
|
+
else
|
27
|
+
raise_invalid_argument(:bool, val)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
convert :date do |val|
|
32
|
+
begin
|
33
|
+
require "date" unless defined?(::Date)
|
34
|
+
::Date.parse(val)
|
35
|
+
rescue ArgumentError, TypeError
|
36
|
+
raise_invalid_argument(:date, val)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
convert :float do |val|
|
41
|
+
begin
|
42
|
+
Float(val)
|
43
|
+
rescue ArgumentError, TypeError
|
44
|
+
raise_invalid_argument(:float, val)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
convert :int, :integer do |val|
|
49
|
+
begin
|
50
|
+
Float(val).to_i
|
51
|
+
rescue ArgumentError, TypeError
|
52
|
+
raise_invalid_argument(:integer, val)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
convert :pathname, :path do |val|
|
57
|
+
require "pathname"
|
58
|
+
::Pathname.new(val.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
convert :regexp do |val|
|
62
|
+
begin
|
63
|
+
Regexp.new(val.to_s)
|
64
|
+
rescue TypeError, RegexpError
|
65
|
+
raise_invalid_argument(:regexp, val)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
convert :sym, :symbol do |val|
|
70
|
+
begin
|
71
|
+
String(val).to_sym
|
72
|
+
rescue ArgumentError
|
73
|
+
raise_invalid_argument(:symbol, val)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
convert :uri do |val|
|
78
|
+
begin
|
79
|
+
require "uri"
|
80
|
+
::URI.parse(val)
|
81
|
+
rescue ::URI::InvalidURIError
|
82
|
+
raise_invalid_argument(:uri, val)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
convert :list, :array do |val|
|
87
|
+
(val.respond_to?(:to_a) ? val : val.split(/(?<!\\),/))
|
88
|
+
.map { |v| v.strip.gsub(/\\,/, ",") }
|
89
|
+
.reject(&:empty?)
|
90
|
+
end
|
91
|
+
|
92
|
+
convert :map, :hash do |val|
|
93
|
+
values = val.respond_to?(:to_a) ? val : val.split(/[& ]/)
|
94
|
+
values.each_with_object({}) do |pair, pairs|
|
95
|
+
key, value = pair.split(/[=:]/, 2)
|
96
|
+
if (current = pairs[key.to_sym])
|
97
|
+
pairs[key.to_sym] = Array(current) << value
|
98
|
+
else
|
99
|
+
pairs[key.to_sym] = value
|
100
|
+
end
|
101
|
+
pairs
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
conversions.keys.each do |type|
|
106
|
+
next if type =~ /list|array|map|hash/
|
107
|
+
|
108
|
+
[:"#{type}_list", :"#{type}_array", :"#{type}s"].each do |new_type|
|
109
|
+
convert new_type do |val|
|
110
|
+
conversions[:list].(val).map do |obj|
|
111
|
+
conversions[type].(obj)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
[:"#{type}_map", :"#{type}_hash"].each do |new_type|
|
117
|
+
convert new_type do |val|
|
118
|
+
conversions[:map].(val).each_with_object({}) do |(k, v), h|
|
119
|
+
h[k] = conversions[type].(v)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end # Conversions
|
125
|
+
end # Option
|
126
|
+
end # TTY
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
module Option
|
5
|
+
module Converter
|
6
|
+
# Store conversions
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def conversions
|
10
|
+
@conversions ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Check if conversion is available
|
14
|
+
#
|
15
|
+
# @param [String] name
|
16
|
+
#
|
17
|
+
# @return [Boolean]
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def contain?(name)
|
21
|
+
conv_name = name.to_s.downcase.to_sym
|
22
|
+
conversions.key?(conv_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register a new conversion type
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# convert(:int) { |val| Float(val).to_i }
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def convert(*names, &block)
|
32
|
+
names.each do |name|
|
33
|
+
if contain?(name)
|
34
|
+
raise ConversionAlreadyDefined,
|
35
|
+
"conversion #{name.inspect} is already defined"
|
36
|
+
end
|
37
|
+
conversions[name] = block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Retrieve a conversion type
|
42
|
+
#
|
43
|
+
# @param [String] name
|
44
|
+
#
|
45
|
+
# @return [Proc]
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def [](name)
|
49
|
+
conv_name = name.to_s.downcase.to_sym
|
50
|
+
conversions.fetch(conv_name) { raise_unsupported_error(conv_name) }
|
51
|
+
end
|
52
|
+
alias fetch []
|
53
|
+
|
54
|
+
# Raise an error for unknown conversion type
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def raise_unsupported_error(conv_name)
|
58
|
+
raise UnsupportedConversion,
|
59
|
+
"unsupported conversion type #{conv_name.inspect}"
|
60
|
+
end
|
61
|
+
end # Converter
|
62
|
+
end # Option
|
63
|
+
end # TTY
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
module Option
|
5
|
+
module DeepDup
|
6
|
+
NONDUPLICATABLE = [
|
7
|
+
Symbol, TrueClass, FalseClass, NilClass, Numeric, Method
|
8
|
+
].freeze
|
9
|
+
|
10
|
+
# Duplicate an object making a deep copy
|
11
|
+
#
|
12
|
+
# @param [Object] object
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def self.deep_dup(object)
|
16
|
+
case object
|
17
|
+
when String then object.dup
|
18
|
+
when *NONDUPLICATABLE then object
|
19
|
+
when Hash then deep_dup_hash(object)
|
20
|
+
when Array then deep_dup_array(object)
|
21
|
+
else object.dup
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# A deep copy of hash
|
26
|
+
#
|
27
|
+
# @param [Hash] object
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
def self.deep_dup_hash(object)
|
31
|
+
object.each_with_object({}) do |(key, val), new_hash|
|
32
|
+
new_hash[deep_dup(key)] = deep_dup(val)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# A deep copy of array
|
37
|
+
#
|
38
|
+
# @param [Array] object
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
def self.deep_dup_array(object)
|
42
|
+
object.each_with_object([]) do |val, new_array|
|
43
|
+
new_array << deep_dup(val)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # DeepDup
|
47
|
+
end # Option
|
48
|
+
end # TTY
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
require_relative "dsl/arity"
|
6
|
+
require_relative "dsl/conversion"
|
7
|
+
require_relative "inflection"
|
8
|
+
require_relative "parameter/argument"
|
9
|
+
require_relative "parameter/environment"
|
10
|
+
require_relative "parameter/keyword"
|
11
|
+
require_relative "parameter/option"
|
12
|
+
require_relative "parameters"
|
13
|
+
require_relative "usage"
|
14
|
+
|
15
|
+
module TTY
|
16
|
+
module Option
|
17
|
+
module DSL
|
18
|
+
include Arity
|
19
|
+
include Conversion
|
20
|
+
include Inflection
|
21
|
+
extend Forwardable
|
22
|
+
|
23
|
+
def_delegators :usage, :command, :banner, :desc, :program,
|
24
|
+
:header, :footer, :example, :no_command
|
25
|
+
|
26
|
+
# Holds the usage information
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def usage(**properties, &block)
|
30
|
+
@usage ||= Usage.create(**properties, &block).tap do |usage|
|
31
|
+
if usage.command.empty?
|
32
|
+
usage.command(dasherize(demodulize(self.name)))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Specify an argument
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def argument(name, **settings, &block)
|
41
|
+
parameters << Parameter::Argument.create(name.to_sym, **settings, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Specify environment variable
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# EDITOR=vim
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
def environment(name, **settings, &block)
|
51
|
+
parameters << Parameter::Environment.create(name.to_sym, **settings, &block)
|
52
|
+
end
|
53
|
+
alias env environment
|
54
|
+
|
55
|
+
# Specify a keyword
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# foo=bar
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def keyword(name, **settings, &block)
|
62
|
+
parameters << Parameter::Keyword.create(name.to_sym, **settings, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
# A shortcut to specify flag option
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# --foo
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def flag(name, **settings, &block)
|
72
|
+
defaults = { default: false }
|
73
|
+
option(name, **defaults.merge(settings), &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Specify an option
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# -f
|
80
|
+
# --foo
|
81
|
+
# --foo bar
|
82
|
+
#
|
83
|
+
# @api public
|
84
|
+
def option(name, **settings, &block)
|
85
|
+
parameters << Parameter::Option.create(name.to_sym, **settings, &block)
|
86
|
+
end
|
87
|
+
alias opt option
|
88
|
+
|
89
|
+
# Remove parameter from the parameters definitions list
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
def ignore(*names)
|
93
|
+
parameters.delete(*names)
|
94
|
+
end
|
95
|
+
alias skip ignore
|
96
|
+
|
97
|
+
# Holds all parameters
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def parameters
|
101
|
+
@parameters ||= Parameters.new
|
102
|
+
end
|
103
|
+
end # DSL
|
104
|
+
end # Option
|
105
|
+
end # TTY
|