dry-cli 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/hash'
4
+ require 'hanami/utils/callbacks'
5
+
6
+ module Dry
7
+ class CLI
8
+ # Command registry
9
+ #
10
+ # @since 0.1.0
11
+ # @api private
12
+ class CommandRegistry
13
+ # @since 0.1.0
14
+ # @api private
15
+ def initialize
16
+ @root = Node.new
17
+ end
18
+
19
+ # @since 0.1.0
20
+ # @api private
21
+ def set(name, command, aliases, **options)
22
+ node = @root
23
+ command = command_for(name, command, **options)
24
+ name.split(/[[:space:]]/).each do |token|
25
+ node = node.put(node, token)
26
+ end
27
+
28
+ node.aliases!(aliases)
29
+ node.leaf!(command) unless command.nil?
30
+
31
+ nil
32
+ end
33
+
34
+ # @since 0.1.0
35
+ # @api private
36
+ #
37
+ def get(arguments)
38
+ node = @root
39
+ args = []
40
+ names = []
41
+ result = LookupResult.new(node, args, names, node.leaf?)
42
+
43
+ arguments.each_with_index do |token, i|
44
+ tmp = node.lookup(token)
45
+
46
+ if tmp.nil?
47
+ result = LookupResult.new(node, args, names, false)
48
+ break
49
+ elsif tmp.leaf?
50
+ args = arguments[i + 1..-1]
51
+ names = arguments[0..i]
52
+ node = tmp
53
+ result = LookupResult.new(node, args, names, true)
54
+ break
55
+ else
56
+ names = arguments[0..i]
57
+ node = tmp
58
+ result = LookupResult.new(node, args, names, node.leaf?)
59
+ end
60
+ end
61
+
62
+ result
63
+ end
64
+
65
+ private
66
+
67
+ # @since 0.1.0
68
+ # @api private
69
+ def command_for(name, command, **options)
70
+ if command.nil?
71
+ command
72
+ else
73
+ command.new(command_name: name, **options)
74
+ end
75
+ end
76
+
77
+ # Node of the registry
78
+ #
79
+ # @since 0.1.0
80
+ # @api private
81
+ class Node
82
+ # @since 0.1.0
83
+ # @api private
84
+ attr_reader :parent
85
+
86
+ # @since 0.1.0
87
+ # @api private
88
+ attr_reader :children
89
+
90
+ # @since 0.1.0
91
+ # @api private
92
+ attr_reader :aliases
93
+
94
+ # @since 0.1.0
95
+ # @api private
96
+ attr_reader :command
97
+
98
+ # @since 0.1.0
99
+ # @api private
100
+ attr_reader :before_callbacks
101
+
102
+ # @since 0.1.0
103
+ # @api private
104
+ attr_reader :after_callbacks
105
+
106
+ # @since 0.1.0
107
+ # @api private
108
+ def initialize(parent = nil)
109
+ @parent = parent
110
+ @children = Concurrent::Hash.new
111
+ @aliases = Concurrent::Hash.new
112
+ @command = nil
113
+
114
+ @before_callbacks = Hanami::Utils::Callbacks::Chain.new
115
+ @after_callbacks = Hanami::Utils::Callbacks::Chain.new
116
+ end
117
+
118
+ # @since 0.1.0
119
+ # @api private
120
+ def put(parent, key)
121
+ children[key] ||= self.class.new(parent)
122
+ end
123
+
124
+ # @since 0.1.0
125
+ # @api private
126
+ def lookup(token)
127
+ children[token] || aliases[token]
128
+ end
129
+
130
+ # @since 0.1.0
131
+ # @api private
132
+ def leaf!(command)
133
+ @command = command
134
+ end
135
+
136
+ # @since 0.1.0
137
+ # @api private
138
+ def alias!(key, child)
139
+ @aliases[key] = child
140
+ end
141
+
142
+ # @since 0.1.0
143
+ # @api private
144
+ def aliases!(aliases)
145
+ aliases.each do |a|
146
+ parent.alias!(a, self)
147
+ end
148
+ end
149
+
150
+ # @since 0.1.0
151
+ # @api private
152
+ def leaf?
153
+ !command.nil?
154
+ end
155
+ end
156
+
157
+ # Result of a registry lookup
158
+ #
159
+ # @since 0.1.0
160
+ # @api private
161
+ class LookupResult
162
+ # @since 0.1.0
163
+ # @api private
164
+ attr_reader :names
165
+
166
+ # @since 0.1.0
167
+ # @api private
168
+ attr_reader :arguments
169
+
170
+ # @since 0.1.0
171
+ # @api private
172
+ def initialize(node, arguments, names, found)
173
+ @node = node
174
+ @arguments = arguments
175
+ @names = names
176
+ @found = found
177
+ end
178
+
179
+ # @since 0.1.0
180
+ # @api private
181
+ def found?
182
+ @found
183
+ end
184
+
185
+ # @since 0.1.0
186
+ # @api private
187
+ def children
188
+ @node.children
189
+ end
190
+
191
+ # @since 0.1.0
192
+ # @api private
193
+ def command
194
+ @node.command
195
+ end
196
+
197
+ # @since 0.2.0
198
+ # @api private
199
+ def before_callbacks
200
+ @node.before_callbacks
201
+ end
202
+
203
+ # @since 0.2.0
204
+ # @api private
205
+ def after_callbacks
206
+ @node.after_callbacks
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hanami/utils/deprecation'
4
+
5
+ module Dry
6
+ # General purpose Command Line Interface (CLI) framework for Ruby
7
+ #
8
+ # @since 0.1.0
9
+ class CLI
10
+ # @since 0.2.0
11
+ class Error < StandardError
12
+ end
13
+
14
+ # @since 0.2.1
15
+ class UnknownCommandError < Error
16
+ # @since 0.2.1
17
+ # @api private
18
+ def initialize(command_name)
19
+ super("unknown command: `#{command_name}'")
20
+ end
21
+ end
22
+
23
+ # @since 0.2.0
24
+ class InvalidCallbackError < Error
25
+ # @since 0.2.0
26
+ # @api private
27
+ def initialize(callback)
28
+ message = case callback
29
+ when Class
30
+ "expected `#{callback.inspect}' to respond to `#initialize' with arity 0"
31
+ else
32
+ "expected `#{callback.inspect}' to respond to `#call'"
33
+ end
34
+
35
+ super(message)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hanami/utils/string'
4
+
5
+ module Dry
6
+ class CLI
7
+ # Command line option
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
11
+ class Option
12
+ # @since 0.1.0
13
+ # @api private
14
+ attr_reader :name
15
+
16
+ # @since 0.1.0
17
+ # @api private
18
+ attr_reader :options
19
+
20
+ # @since 0.1.0
21
+ # @api private
22
+ def initialize(name, options = {})
23
+ @name = name
24
+ @options = options
25
+ end
26
+
27
+ # @since 0.1.0
28
+ # @api private
29
+ def aliases
30
+ options[:aliases] || []
31
+ end
32
+
33
+ # @since 0.1.0
34
+ # @api private
35
+ def desc
36
+ desc = options[:desc]
37
+ values ? "#{desc}: (#{values.join('/')})" : desc
38
+ end
39
+
40
+ # @since 0.1.0
41
+ # @api private
42
+ def required?
43
+ options[:required]
44
+ end
45
+
46
+ # @since 0.1.0
47
+ # @api private
48
+ def type
49
+ options[:type]
50
+ end
51
+
52
+ # @since 0.1.0
53
+ # @api private
54
+ def values
55
+ options[:values]
56
+ end
57
+
58
+ # @since 0.1.0
59
+ # @api private
60
+ def boolean?
61
+ type == :boolean
62
+ end
63
+
64
+ # @since 0.3.0
65
+ # @api private
66
+ def array?
67
+ type == :array
68
+ end
69
+
70
+ # @since 0.1.0
71
+ # @api private
72
+ def default
73
+ options[:default]
74
+ end
75
+
76
+ # @since 0.1.0
77
+ # @api private
78
+ def description_name
79
+ options[:label] || name.upcase
80
+ end
81
+
82
+ # @since 0.1.0
83
+ # @api private
84
+ def argument?
85
+ false
86
+ end
87
+
88
+ # @since 0.1.0
89
+ # @api private
90
+ #
91
+ # rubocop:disable Metrics/AbcSize
92
+ def parser_options
93
+ dasherized_name = Hanami::Utils::String.dasherize(name)
94
+ parser_options = []
95
+
96
+ if boolean?
97
+ parser_options << "--[no-]#{dasherized_name}"
98
+ else
99
+ parser_options << "--#{dasherized_name}=#{name}"
100
+ parser_options << "--#{dasherized_name} #{name}"
101
+ end
102
+
103
+ parser_options << Array if array?
104
+ parser_options << values if values
105
+ parser_options.unshift(alias_name) unless alias_name.nil?
106
+ parser_options << desc if desc
107
+ parser_options
108
+ end
109
+ # rubocop:enable Metrics/AbcSize
110
+
111
+ private
112
+
113
+ # @since 0.1.0
114
+ # @api private
115
+ def alias_name
116
+ aliases.join(' ') if aliases.any?
117
+ end
118
+ end
119
+
120
+ # Command line argument
121
+ #
122
+ # @since 0.1.0
123
+ # @api private
124
+ class Argument < Option
125
+ # @since 0.1.0
126
+ # @api private
127
+ def argument?
128
+ true
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'dry/cli/program_name'
5
+
6
+ module Dry
7
+ class CLI
8
+ # Parse command line arguments and options
9
+ #
10
+ # @since 0.1.0
11
+ # @api private
12
+ module Parser
13
+ # @since 0.1.0
14
+ # @api private
15
+ #
16
+ def self.call(command, arguments, names)
17
+ original_arguments = arguments.dup
18
+ parsed_options = {}
19
+
20
+ OptionParser.new do |opts|
21
+ command.options.each do |option|
22
+ opts.on(*option.parser_options) do |value|
23
+ parsed_options[option.name.to_sym] = value
24
+ end
25
+ end
26
+
27
+ opts.on_tail('-h', '--help') do
28
+ return Result.help
29
+ end
30
+ end.parse!(arguments)
31
+
32
+ parsed_options = command.default_params.merge(parsed_options)
33
+ parse_required_params(command, arguments, names, parsed_options)
34
+ rescue ::OptionParser::ParseError
35
+ Result.failure("Error: \"#{command.command_name}\" was called with arguments \"#{original_arguments.join(' ')}\"") # rubocop:disable Metrics/LineLength
36
+ end
37
+
38
+ # @since 0.1.0
39
+ # @api private
40
+ def self.full_command_name(names)
41
+ ProgramName.call(names)
42
+ end
43
+
44
+ # @since 0.1.0
45
+ # @api private
46
+ #
47
+ # rubocop:disable Metrics/AbcSize
48
+ def self.parse_required_params(command, arguments, names, parsed_options)
49
+ parsed_params = match_arguments(command.arguments, arguments)
50
+ parsed_required_params = match_arguments(command.required_arguments, arguments)
51
+ all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } # rubocop:disable Metrics/LineLength
52
+
53
+ unused_arguments = arguments.drop(command.required_arguments.length)
54
+
55
+ unless all_required_params_satisfied
56
+ parsed_required_params_values = parsed_required_params.values.compact
57
+
58
+ usage = "\nUsage: \"#{full_command_name(names)} #{command.required_arguments.map(&:description_name).join(' ')}\"" # rubocop:disable Metrics/LineLength
59
+
60
+ if parsed_required_params_values.empty? # rubocop:disable Style/GuardClause
61
+ return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with no arguments#{usage}") # rubocop:disable Metrics/LineLength
62
+ else
63
+ return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with arguments #{parsed_required_params_values}#{usage}") # rubocop:disable Metrics/LineLength
64
+ end
65
+ end
66
+
67
+ parsed_params.reject! { |_key, value| value.nil? }
68
+ parsed_options = parsed_options.merge(parsed_params)
69
+ parsed_options = parsed_options.merge(args: unused_arguments) if unused_arguments.any?
70
+ Result.success(parsed_options)
71
+ end
72
+ # rubocop:enable Metrics/AbcSize
73
+
74
+ def self.match_arguments(command_arguments, arguments)
75
+ result = {}
76
+
77
+ command_arguments.each_with_index do |cmd_arg, index|
78
+ if cmd_arg.array?
79
+ result[cmd_arg.name] = arguments[index..-1]
80
+ break
81
+ else
82
+ result[cmd_arg.name] = arguments.at(index)
83
+ end
84
+ end
85
+
86
+ result
87
+ end
88
+
89
+ # @since 0.1.0
90
+ # @api private
91
+ class Result
92
+ # @since 0.1.0
93
+ # @api private
94
+ def self.help
95
+ new(help: true)
96
+ end
97
+
98
+ # @since 0.1.0
99
+ # @api private
100
+ def self.success(arguments = {})
101
+ new(arguments: arguments)
102
+ end
103
+
104
+ # @since 0.1.0
105
+ # @api private
106
+ def self.failure(error = 'Error: Invalid param provided')
107
+ new(error: error)
108
+ end
109
+
110
+ # @since 0.1.0
111
+ # @api private
112
+ attr_reader :arguments
113
+
114
+ # @since 0.1.0
115
+ # @api private
116
+ attr_reader :error
117
+
118
+ # @since 0.1.0
119
+ # @api private
120
+ def initialize(arguments: {}, error: nil, help: false)
121
+ @arguments = arguments
122
+ @error = error
123
+ @help = help
124
+ end
125
+
126
+ # @since 0.1.0
127
+ # @api private
128
+ def error?
129
+ !error.nil?
130
+ end
131
+
132
+ # @since 0.1.0
133
+ # @api private
134
+ def help?
135
+ @help
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end