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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +1653 -1
  4. data/lib/tty/option.rb +63 -4
  5. data/lib/tty/option/aggregate_errors.rb +95 -0
  6. data/lib/tty/option/conversions.rb +126 -0
  7. data/lib/tty/option/converter.rb +63 -0
  8. data/lib/tty/option/deep_dup.rb +48 -0
  9. data/lib/tty/option/dsl.rb +105 -0
  10. data/lib/tty/option/dsl/arity.rb +49 -0
  11. data/lib/tty/option/dsl/conversion.rb +17 -0
  12. data/lib/tty/option/error_aggregator.rb +35 -0
  13. data/lib/tty/option/errors.rb +144 -0
  14. data/lib/tty/option/formatter.rb +389 -0
  15. data/lib/tty/option/inflection.rb +50 -0
  16. data/lib/tty/option/param_conversion.rb +34 -0
  17. data/lib/tty/option/param_permitted.rb +30 -0
  18. data/lib/tty/option/param_validation.rb +48 -0
  19. data/lib/tty/option/parameter.rb +310 -0
  20. data/lib/tty/option/parameter/argument.rb +18 -0
  21. data/lib/tty/option/parameter/environment.rb +20 -0
  22. data/lib/tty/option/parameter/keyword.rb +15 -0
  23. data/lib/tty/option/parameter/option.rb +99 -0
  24. data/lib/tty/option/parameters.rb +157 -0
  25. data/lib/tty/option/params.rb +122 -0
  26. data/lib/tty/option/parser.rb +57 -3
  27. data/lib/tty/option/parser/arguments.rb +166 -0
  28. data/lib/tty/option/parser/arity_check.rb +34 -0
  29. data/lib/tty/option/parser/environments.rb +169 -0
  30. data/lib/tty/option/parser/keywords.rb +158 -0
  31. data/lib/tty/option/parser/options.rb +273 -0
  32. data/lib/tty/option/parser/param_types.rb +51 -0
  33. data/lib/tty/option/parser/required_check.rb +36 -0
  34. data/lib/tty/option/pipeline.rb +38 -0
  35. data/lib/tty/option/result.rb +46 -0
  36. data/lib/tty/option/section.rb +26 -0
  37. data/lib/tty/option/sections.rb +56 -0
  38. data/lib/tty/option/usage.rb +166 -0
  39. data/lib/tty/option/usage_wrapper.rb +58 -0
  40. data/lib/tty/option/version.rb +3 -3
  41. metadata +37 -3
data/lib/tty/option.rb CHANGED
@@ -1,9 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty/option/version"
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
- class Error < StandardError; end
8
- end
9
- end
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