tty-option 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|