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.
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