dry-cli 0.4.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 +7 -0
- data/.codeclimate.yml +12 -0
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +77 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +70 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.md +31 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docsite/source/index.html.md +588 -0
- data/dry-cli.gemspec +36 -0
- data/lib/dry/cli.rb +129 -0
- data/lib/dry/cli/banner.rb +127 -0
- data/lib/dry/cli/command.rb +367 -0
- data/lib/dry/cli/command_registry.rb +211 -0
- data/lib/dry/cli/errors.rb +39 -0
- data/lib/dry/cli/option.rb +132 -0
- data/lib/dry/cli/parser.rb +140 -0
- data/lib/dry/cli/program_name.rb +21 -0
- data/lib/dry/cli/registry.rb +328 -0
- data/lib/dry/cli/usage.rb +91 -0
- data/lib/dry/cli/utils/files.rb +443 -0
- data/lib/dry/cli/version.rb +8 -0
- data/script/ci +51 -0
- metadata +169 -0
@@ -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
|