hanami-cli 0.0.0 → 0.1.0.beta1
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 +4 -4
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +9 -2
- data/README.md +412 -6
- data/Rakefile +19 -2
- data/hanami-cli.gemspec +6 -1
- data/lib/hanami/cli.rb +120 -4
- data/lib/hanami/cli/banner.rb +121 -0
- data/lib/hanami/cli/command.rb +365 -0
- data/lib/hanami/cli/command_registry.rb +184 -0
- data/lib/hanami/cli/option.rb +125 -0
- data/lib/hanami/cli/parser.rb +123 -0
- data/lib/hanami/cli/program_name.rb +19 -0
- data/lib/hanami/cli/registry.rb +122 -0
- data/lib/hanami/cli/usage.rb +88 -0
- data/lib/hanami/cli/version.rb +3 -2
- metadata +59 -5
@@ -0,0 +1,184 @@
|
|
1
|
+
require "concurrent/hash"
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class CLI
|
5
|
+
# Command registry
|
6
|
+
#
|
7
|
+
# @since 0.1.0
|
8
|
+
# @api private
|
9
|
+
class CommandRegistry
|
10
|
+
# @since 0.1.0
|
11
|
+
# @api private
|
12
|
+
def initialize
|
13
|
+
@root = Node.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# @since 0.1.0
|
17
|
+
# @api private
|
18
|
+
def set(name, command, aliases, **options)
|
19
|
+
node = @root
|
20
|
+
command = command_for(name, command, **options)
|
21
|
+
name.split(/[[:space:]]/).each do |token|
|
22
|
+
node = node.put(node, token)
|
23
|
+
end
|
24
|
+
|
25
|
+
node.aliases!(aliases)
|
26
|
+
node.leaf!(command) unless command.nil?
|
27
|
+
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# @since 0.1.0
|
32
|
+
# @api private
|
33
|
+
def get(arguments)
|
34
|
+
node = @root
|
35
|
+
args = []
|
36
|
+
names = []
|
37
|
+
result = LookupResult.new(node, args, names, node.leaf?)
|
38
|
+
|
39
|
+
arguments.each_with_index do |token, i|
|
40
|
+
tmp = node.lookup(token)
|
41
|
+
|
42
|
+
if tmp.nil?
|
43
|
+
result = LookupResult.new(node, args, names, false)
|
44
|
+
break
|
45
|
+
elsif tmp.leaf?
|
46
|
+
args = arguments[i + 1..-1]
|
47
|
+
names = arguments[0..i]
|
48
|
+
node = tmp
|
49
|
+
result = LookupResult.new(node, args, names, true)
|
50
|
+
break
|
51
|
+
else
|
52
|
+
names = arguments[0..i]
|
53
|
+
node = tmp
|
54
|
+
result = LookupResult.new(node, args, names, node.leaf?)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# @since 0.1.0
|
64
|
+
# @api private
|
65
|
+
def command_for(name, command, **options)
|
66
|
+
if command.nil?
|
67
|
+
command
|
68
|
+
else
|
69
|
+
command.new(command_name: name, **options)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Node of the registry
|
74
|
+
#
|
75
|
+
# @since 0.1.0
|
76
|
+
# @api private
|
77
|
+
class Node
|
78
|
+
# @since 0.1.0
|
79
|
+
# @api private
|
80
|
+
attr_reader :parent
|
81
|
+
|
82
|
+
# @since 0.1.0
|
83
|
+
# @api private
|
84
|
+
attr_reader :children
|
85
|
+
|
86
|
+
# @since 0.1.0
|
87
|
+
# @api private
|
88
|
+
attr_reader :aliases
|
89
|
+
|
90
|
+
# @since 0.1.0
|
91
|
+
# @api private
|
92
|
+
attr_reader :command
|
93
|
+
|
94
|
+
# @since 0.1.0
|
95
|
+
# @api private
|
96
|
+
def initialize(parent = nil)
|
97
|
+
@parent = parent
|
98
|
+
@children = Concurrent::Hash.new
|
99
|
+
@aliases = Concurrent::Hash.new
|
100
|
+
@command = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# @since 0.1.0
|
104
|
+
# @api private
|
105
|
+
def put(parent, key)
|
106
|
+
children[key] ||= self.class.new(parent)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @since 0.1.0
|
110
|
+
# @api private
|
111
|
+
def lookup(token)
|
112
|
+
children[token] || aliases[token]
|
113
|
+
end
|
114
|
+
|
115
|
+
# @since 0.1.0
|
116
|
+
# @api private
|
117
|
+
def leaf!(command)
|
118
|
+
@command = command
|
119
|
+
end
|
120
|
+
|
121
|
+
# @since 0.1.0
|
122
|
+
# @api private
|
123
|
+
def alias!(key, child)
|
124
|
+
@aliases[key] = child
|
125
|
+
end
|
126
|
+
|
127
|
+
# @since 0.1.0
|
128
|
+
# @api private
|
129
|
+
def aliases!(aliases)
|
130
|
+
aliases.each do |a|
|
131
|
+
parent.alias!(a, self)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# @since 0.1.0
|
136
|
+
# @api private
|
137
|
+
def leaf?
|
138
|
+
!command.nil?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Result of a registry lookup
|
143
|
+
#
|
144
|
+
# @since 0.1.0
|
145
|
+
# @api private
|
146
|
+
class LookupResult
|
147
|
+
# @since 0.1.0
|
148
|
+
# @api private
|
149
|
+
attr_reader :names
|
150
|
+
|
151
|
+
# @since 0.1.0
|
152
|
+
# @api private
|
153
|
+
attr_reader :arguments
|
154
|
+
|
155
|
+
# @since 0.1.0
|
156
|
+
# @api private
|
157
|
+
def initialize(node, arguments, names, found)
|
158
|
+
@node = node
|
159
|
+
@arguments = arguments
|
160
|
+
@names = names
|
161
|
+
@found = found
|
162
|
+
end
|
163
|
+
|
164
|
+
# @since 0.1.0
|
165
|
+
# @api private
|
166
|
+
def found?
|
167
|
+
@found
|
168
|
+
end
|
169
|
+
|
170
|
+
# @since 0.1.0
|
171
|
+
# @api private
|
172
|
+
def children
|
173
|
+
@node.children
|
174
|
+
end
|
175
|
+
|
176
|
+
# @since 0.1.0
|
177
|
+
# @api private
|
178
|
+
def command
|
179
|
+
@node.command
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "hanami/utils/string"
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class CLI
|
5
|
+
# Command line option
|
6
|
+
#
|
7
|
+
# @since 0.1.0
|
8
|
+
# @api private
|
9
|
+
class Option
|
10
|
+
# @since 0.1.0
|
11
|
+
# @api private
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @since 0.1.0
|
15
|
+
# @api private
|
16
|
+
attr_reader :options
|
17
|
+
|
18
|
+
# @since 0.1.0
|
19
|
+
# @api private
|
20
|
+
def initialize(name, options = {})
|
21
|
+
@name = name
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
# @since 0.1.0
|
26
|
+
# @api private
|
27
|
+
def aliases
|
28
|
+
options[:aliases] || []
|
29
|
+
end
|
30
|
+
|
31
|
+
# @since 0.1.0
|
32
|
+
# @api private
|
33
|
+
def desc
|
34
|
+
desc = options[:desc]
|
35
|
+
values ? "#{desc}: (#{values.join('/')})" : desc
|
36
|
+
end
|
37
|
+
|
38
|
+
# @since 0.1.0
|
39
|
+
# @api private
|
40
|
+
def required?
|
41
|
+
options[:required]
|
42
|
+
end
|
43
|
+
|
44
|
+
# @since 0.1.0
|
45
|
+
# @api private
|
46
|
+
def type
|
47
|
+
options[:type]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @since 0.1.0
|
51
|
+
# @api private
|
52
|
+
def values
|
53
|
+
options[:values]
|
54
|
+
end
|
55
|
+
|
56
|
+
# @since 0.1.0
|
57
|
+
# @api private
|
58
|
+
def boolean?
|
59
|
+
type == :boolean
|
60
|
+
end
|
61
|
+
|
62
|
+
# @since 0.1.0
|
63
|
+
# @api private
|
64
|
+
def default
|
65
|
+
options[:default]
|
66
|
+
end
|
67
|
+
|
68
|
+
# @since 0.1.0
|
69
|
+
# @api private
|
70
|
+
def description_name
|
71
|
+
options[:label] || name.upcase
|
72
|
+
end
|
73
|
+
|
74
|
+
# @since 0.1.0
|
75
|
+
# @api private
|
76
|
+
def argument?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# @since 0.1.0
|
81
|
+
# @api private
|
82
|
+
#
|
83
|
+
# rubocop:disable Metrics/AbcSize
|
84
|
+
# rubocop:disable Metrics/MethodLength
|
85
|
+
def parser_options
|
86
|
+
dasherized_name = Hanami::Utils::String.dasherize(name)
|
87
|
+
parser_options = []
|
88
|
+
|
89
|
+
if type == :boolean
|
90
|
+
parser_options << "--[no-]#{dasherized_name}"
|
91
|
+
else
|
92
|
+
parser_options << "--#{dasherized_name}=#{name}"
|
93
|
+
parser_options << "--#{dasherized_name} #{name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
parser_options << values if values
|
97
|
+
parser_options.unshift(alias_name) unless alias_name.nil?
|
98
|
+
parser_options << desc if desc
|
99
|
+
parser_options
|
100
|
+
end
|
101
|
+
# rubocop:enable Metrics/MethodLength
|
102
|
+
# rubocop:enable Metrics/AbcSize
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# @since 0.1.0
|
107
|
+
# @api private
|
108
|
+
def alias_name
|
109
|
+
aliases.join(" ") if aliases.any?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Command line argument
|
114
|
+
#
|
115
|
+
# @since 0.1.0
|
116
|
+
# @api private
|
117
|
+
class Argument < Option
|
118
|
+
# @since 0.1.0
|
119
|
+
# @api private
|
120
|
+
def argument?
|
121
|
+
true
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require "hanami/cli/program_name"
|
3
|
+
|
4
|
+
module Hanami
|
5
|
+
class CLI
|
6
|
+
# Parse command line arguments and options
|
7
|
+
#
|
8
|
+
# @since 0.1.0
|
9
|
+
# @api private
|
10
|
+
module Parser
|
11
|
+
# @since 0.1.0
|
12
|
+
# @api private
|
13
|
+
#
|
14
|
+
# rubocop:disable Metrics/AbcSize
|
15
|
+
# rubocop:disable Metrics/MethodLength
|
16
|
+
def self.call(command, arguments, names)
|
17
|
+
parsed_options = {}
|
18
|
+
|
19
|
+
OptionParser.new do |opts|
|
20
|
+
command.options.each do |option|
|
21
|
+
opts.on(*option.parser_options) do |value|
|
22
|
+
parsed_options[option.name.to_sym] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on_tail("-h", "--help") do
|
27
|
+
return Result.help
|
28
|
+
end
|
29
|
+
end.parse!(arguments.dup)
|
30
|
+
|
31
|
+
parsed_options = command.default_params.merge(parsed_options)
|
32
|
+
parse_required_params(command, arguments, names, parsed_options)
|
33
|
+
rescue ::OptionParser::ParseError
|
34
|
+
return Result.failure
|
35
|
+
end
|
36
|
+
# rubocop:enable Metrics/MethodLength
|
37
|
+
# rubocop:enable Metrics/AbcSize
|
38
|
+
|
39
|
+
# @since 0.1.0
|
40
|
+
# @api private
|
41
|
+
def self.full_command_name(names)
|
42
|
+
ProgramName.call(names)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @since 0.1.0
|
46
|
+
# @api private
|
47
|
+
#
|
48
|
+
# rubocop:disable Metrics/AbcSize
|
49
|
+
# rubocop:disable Metrics/MethodLength
|
50
|
+
def self.parse_required_params(command, arguments, names, parsed_options)
|
51
|
+
parse_params = Hash[command.arguments.map(&:name).zip(arguments)]
|
52
|
+
parse_required_params = Hash[command.required_arguments.map(&:name).zip(arguments)]
|
53
|
+
all_required_params_satisfied = command.required_arguments.all? { |param| !parse_required_params[param.name].nil? }
|
54
|
+
|
55
|
+
unless all_required_params_satisfied
|
56
|
+
parse_required_params_values = parse_required_params.values.compact
|
57
|
+
|
58
|
+
usage = "\nUsage: \"#{full_command_name(names)} #{command.required_arguments.map(&:description_name).join(' ')}\""
|
59
|
+
|
60
|
+
if parse_required_params_values.empty? # rubocop:disable Style/GuardClause
|
61
|
+
return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with no arguments#{usage}")
|
62
|
+
else
|
63
|
+
return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with arguments #{parse_required_params_values}#{usage}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
Result.success(parse_params.merge(parsed_options))
|
68
|
+
end
|
69
|
+
# rubocop:enable Metrics/MethodLength
|
70
|
+
# rubocop:enable Metrics/AbcSize
|
71
|
+
|
72
|
+
# @since 0.1.0
|
73
|
+
# @api private
|
74
|
+
class Result
|
75
|
+
# @since 0.1.0
|
76
|
+
# @api private
|
77
|
+
def self.help
|
78
|
+
new(help: true)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @since 0.1.0
|
82
|
+
# @api private
|
83
|
+
def self.success(arguments = {})
|
84
|
+
new(arguments: arguments)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @since 0.1.0
|
88
|
+
# @api private
|
89
|
+
def self.failure(error = "Error: Invalid param provided")
|
90
|
+
new(error: error)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @since 0.1.0
|
94
|
+
# @api private
|
95
|
+
attr_reader :arguments
|
96
|
+
|
97
|
+
# @since 0.1.0
|
98
|
+
# @api private
|
99
|
+
attr_reader :error
|
100
|
+
|
101
|
+
# @since 0.1.0
|
102
|
+
# @api private
|
103
|
+
def initialize(arguments: {}, error: nil, help: false)
|
104
|
+
@arguments = arguments
|
105
|
+
@error = error
|
106
|
+
@help = help
|
107
|
+
end
|
108
|
+
|
109
|
+
# @since 0.1.0
|
110
|
+
# @api private
|
111
|
+
def error?
|
112
|
+
!error.nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
# @since 0.1.0
|
116
|
+
# @api private
|
117
|
+
def help?
|
118
|
+
@help
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Hanami
|
2
|
+
class CLI
|
3
|
+
# Program name
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
# @api private
|
7
|
+
module ProgramName
|
8
|
+
# @since 0.1.0
|
9
|
+
# @api private
|
10
|
+
SEPARATOR = " ".freeze
|
11
|
+
|
12
|
+
# @since 0.1.0
|
13
|
+
# @api private
|
14
|
+
def self.call(names = [], program_name: $PROGRAM_NAME)
|
15
|
+
[File.basename(program_name), names].flatten.join(SEPARATOR)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|