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.
data/Rakefile CHANGED
@@ -1,2 +1,19 @@
1
- require "bundler/gem_tasks"
2
- task :default => :spec
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ namespace :spec do
7
+ RSpec::Core::RakeTask.new(:unit) do |task|
8
+ file_list = FileList['spec/**/*_spec.rb']
9
+
10
+ task.pattern = file_list
11
+ end
12
+
13
+ task :coverage do
14
+ ENV['COVERAGE'] = 'true'
15
+ Rake::Task['spec:unit'].invoke
16
+ end
17
+ end
18
+
19
+ task default: 'spec:unit'
@@ -5,9 +5,10 @@ require 'hanami/cli/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "hanami-cli"
8
- spec.version = Hanami::Cli::VERSION
8
+ spec.version = Hanami::CLI::VERSION
9
9
  spec.authors = ["Luca Guidi"]
10
10
  spec.email = ["me@lucaguidi.com"]
11
+ spec.licenses = ["MIT"]
11
12
 
12
13
  spec.summary = "Hanami CLI"
13
14
  spec.description = "Hanami framework to build command line interfaces with Ruby"
@@ -22,6 +23,10 @@ Gem::Specification.new do |spec|
22
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
24
  spec.require_paths = ["lib"]
24
25
 
26
+ spec.add_dependency "hanami-utils", "1.1.0.beta1"
27
+ spec.add_dependency "concurrent-ruby", "~> 1.0"
28
+
25
29
  spec.add_development_dependency "bundler", "~> 1.14"
26
30
  spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.5"
27
32
  end
@@ -1,7 +1,123 @@
1
- require "hanami/cli/version"
2
-
1
+ # Hanami
2
+ #
3
+ # @since 0.1.0
3
4
  module Hanami
4
- module Cli
5
- # Your code goes here...
5
+ # General purpose Command Line Interface (CLI) framework for Ruby
6
+ #
7
+ # @since 0.1.0
8
+ class CLI
9
+ require "hanami/cli/version"
10
+ require "hanami/cli/command"
11
+ require "hanami/cli/registry"
12
+ require "hanami/cli/parser"
13
+ require "hanami/cli/usage"
14
+ require "hanami/cli/banner"
15
+
16
+ # Check if command
17
+ #
18
+ # @param command [Object] the command to check
19
+ #
20
+ # @return [TrueClass,FalseClass] true if instance of `Hanami::CLI::Command`
21
+ #
22
+ # @since 0.1.0
23
+ # @api private
24
+ def self.command?(command)
25
+ case command
26
+ when Class
27
+ command.ancestors.include?(Command)
28
+ else
29
+ command.is_a?(Command)
30
+ end
31
+ end
32
+
33
+ # Create a new instance
34
+ #
35
+ # @param registry [Hanami::CLI::Registry] a registry
36
+ #
37
+ # @return [Hanami::CLI] the new instance
38
+ # @since 0.1.0
39
+ def initialize(registry)
40
+ @commands = registry
41
+ end
42
+
43
+ # Invoke the CLI
44
+ #
45
+ # @param arguments [Array<string>] the command line arguments (defaults to `ARGV`)
46
+ # @param out [IO] the standard output (defaults to `$stdout`)
47
+ #
48
+ # @since 0.1.0
49
+ def call(arguments: ARGV, out: $stdout)
50
+ result = commands.get(arguments)
51
+
52
+ if result.found?
53
+ command, args = parse(result, out)
54
+ command.call(args)
55
+ else
56
+ usage(result, out)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # @since 0.1.0
63
+ # @api private
64
+ attr_reader :commands
65
+
66
+ # Parse arguments for a command.
67
+ #
68
+ # It may exit in case of error, or in case of help.
69
+ #
70
+ # @param result [Hanami::CLI::CommandRegistry::LookupResult]
71
+ # @param out [IO] sta output
72
+ #
73
+ # @return [Array<Hanami:CLI::Command, Array>] returns an array where the
74
+ # first element is a command and the second one is the list of arguments
75
+ #
76
+ # @since 0.1.0
77
+ # @api private
78
+ def parse(result, out) # rubocop:disable Metrics/MethodLength
79
+ command = result.command
80
+ return [command, result.arguments] unless command?(command)
81
+
82
+ result = Parser.call(command, result.arguments, result.names)
83
+
84
+ if result.help?
85
+ Banner.call(command, out)
86
+ exit(0)
87
+ end
88
+
89
+ if result.error?
90
+ out.puts(result.error)
91
+ exit(1)
92
+ end
93
+
94
+ [command, result.arguments]
95
+ end
96
+
97
+ # Prints the command usage and exit.
98
+ #
99
+ # @param result [Hanami::CLI::CommandRegistry::LookupResult]
100
+ # @param out [IO] sta output
101
+ #
102
+ # @since 0.1.0
103
+ # @api private
104
+ def usage(result, out)
105
+ Usage.call(result, out)
106
+ exit(1)
107
+ end
108
+
109
+ # Check if command
110
+ #
111
+ # @param command [Object] the command to check
112
+ #
113
+ # @return [TrueClass,FalseClass] true if instance of `Hanami::CLI::Command`
114
+ #
115
+ # @since 0.1.0
116
+ # @api private
117
+ #
118
+ # @see .command?
119
+ def command?(command)
120
+ CLI.command?(command)
121
+ end
6
122
  end
7
123
  end
@@ -0,0 +1,121 @@
1
+ require "hanami/cli/program_name"
2
+
3
+ module Hanami
4
+ class CLI
5
+ # Command banner
6
+ #
7
+ # @since 0.1.0
8
+ # @api private
9
+ module Banner
10
+ # Prints command banner
11
+ #
12
+ # @param command [Hanami::CLI::Command] the command
13
+ # @param out [IO] standard output
14
+ #
15
+ # @since 0.1.0
16
+ # @api private
17
+ def self.call(command, out)
18
+ output = [
19
+ command_name(command),
20
+ command_name_and_arguments(command),
21
+ command_description(command),
22
+ command_arguments(command),
23
+ command_options(command),
24
+ command_examples(command)
25
+ ].compact.join("\n")
26
+
27
+ out.puts output
28
+ end
29
+
30
+ # @since 0.1.0
31
+ # @api private
32
+ def self.command_name(command)
33
+ "Command:\n #{full_command_name(command)}"
34
+ end
35
+
36
+ # @since 0.1.0
37
+ # @api private
38
+ def self.command_name_and_arguments(command)
39
+ "\nUsage:\n #{full_command_name(command)}#{arguments(command)}"
40
+ end
41
+
42
+ # @since 0.1.0
43
+ # @api private
44
+ def self.command_examples(command)
45
+ return if command.examples.empty?
46
+
47
+ "\nExamples:\n#{command.examples.map { |example| " #{full_command_name(command)} #{example}" }.join("\n")}"
48
+ end
49
+
50
+ # @since 0.1.0
51
+ # @api private
52
+ def self.command_description(command)
53
+ return if command.description.nil?
54
+
55
+ "\nDescription:\n #{command.description}"
56
+ end
57
+
58
+ # @since 0.1.0
59
+ # @api private
60
+ def self.command_arguments(command)
61
+ return if command.arguments.empty?
62
+ "\nArguments:\n#{extended_command_arguments(command)}"
63
+ end
64
+
65
+ # @since 0.1.0
66
+ # @api private
67
+ def self.command_options(command)
68
+ "\nOptions:\n#{extended_command_options(command)}"
69
+ end
70
+
71
+ # @since 0.1.0
72
+ # @api private
73
+ def self.full_command_name(command)
74
+ ProgramName.call(command.command_name)
75
+ end
76
+
77
+ # @since 0.1.0
78
+ # @api private
79
+ def self.arguments(command)
80
+ required_arguments = command.required_arguments
81
+ optional_arguments = command.optional_arguments
82
+
83
+ required = required_arguments.map { |arg| arg.name.upcase }.join(' ') if required_arguments.any?
84
+ optional = optional_arguments.map { |arg| "[#{arg.name.upcase}]" }.join(' ') if optional_arguments.any?
85
+ result = [required, optional].compact
86
+
87
+ " #{result.join(' ')}" unless result.empty?
88
+ end
89
+
90
+ # @since 0.1.0
91
+ # @api private
92
+ def self.extended_command_arguments(command)
93
+ command.arguments.map do |argument|
94
+ " #{argument.name.to_s.upcase.ljust(20)}\t# #{'REQUIRED ' if argument.required?}#{argument.desc}"
95
+ end.join("\n")
96
+ end
97
+
98
+ # @since 0.1.0
99
+ # @api private
100
+ def self.extended_command_options(command)
101
+ result = command.options.map do |option|
102
+ name = Utils::String.dasherize(option.name)
103
+ name = if option.boolean?
104
+ "[no-]#{name}"
105
+ else
106
+ "#{name}=VALUE"
107
+ end
108
+
109
+ name = "#{name}, #{option.aliases.map { |a| a.start_with?('--') ? "#{a}=VALUE" : "#{a} VALUE" }.join(', ')}" unless option.aliases.empty?
110
+ name = " --#{name.ljust(30)}"
111
+ name = "#{name}\t# #{option.desc}"
112
+ name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
113
+ name
114
+ end
115
+
116
+ result << " --#{'help, -h'.ljust(30)}\t# Print this help"
117
+ result.join("\n")
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,365 @@
1
+ require "forwardable"
2
+ require "hanami/cli/option"
3
+ require "concurrent/array"
4
+
5
+ module Hanami
6
+ class CLI
7
+ # Base class for commands
8
+ #
9
+ # @since 0.1.0
10
+ class Command
11
+ # @since 0.1.0
12
+ # @api private
13
+ def self.inherited(base)
14
+ super
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ # @since 0.1.0
19
+ # @api private
20
+ module ClassMethods
21
+ # @since 0.1.0
22
+ # @api private
23
+ def self.extended(base)
24
+ super
25
+
26
+ base.class_eval do
27
+ @description = nil
28
+ @examples = Concurrent::Array.new
29
+ @arguments = Concurrent::Array.new
30
+ @options = Concurrent::Array.new
31
+ end
32
+ end
33
+
34
+ # @since 0.1.0
35
+ # @api private
36
+ attr_reader :description
37
+
38
+ # @since 0.1.0
39
+ # @api private
40
+ attr_reader :examples
41
+
42
+ # @since 0.1.0
43
+ # @api private
44
+ attr_reader :arguments
45
+
46
+ # @since 0.1.0
47
+ # @api private
48
+ attr_reader :options
49
+ end
50
+
51
+ # Set the description of the command
52
+ #
53
+ # @param description [String] the description
54
+ #
55
+ # @since 0.1.0
56
+ #
57
+ # @example
58
+ # require "hanami/cli"
59
+ #
60
+ # class Echo < Hanami::CLI::Command
61
+ # desc "Prints given input"
62
+ #
63
+ # def call(*)
64
+ # # ...
65
+ # end
66
+ # end
67
+ def self.desc(description)
68
+ @description = description
69
+ end
70
+
71
+ # Describe the usage of the command
72
+ #
73
+ # @param examples [Array<String>] one or more examples
74
+ #
75
+ # @since 0.1.0
76
+ #
77
+ # @example
78
+ # require "hanami/cli"
79
+ #
80
+ # class Server < Hanami::CLI::Command
81
+ # example [
82
+ # " # Basic usage (it uses the bundled server engine)",
83
+ # "--server=webrick # Force `webrick` server engine",
84
+ # "--host=0.0.0.0 # Bind to a host",
85
+ # "--port=2306 # Bind to a port",
86
+ # "--no-code-reloading # Disable code reloading"
87
+ # ]
88
+ #
89
+ # def call(*)
90
+ # # ...
91
+ # end
92
+ # end
93
+ #
94
+ # # $ foo server --help
95
+ # # # ...
96
+ # #
97
+ # # Examples:
98
+ # # foo server # Basic usage (it uses the bundled server engine)
99
+ # # foo server --server=webrick # Force `webrick` server engine
100
+ # # foo server --host=0.0.0.0 # Bind to a host
101
+ # # foo server --port=2306 # Bind to a port
102
+ # # foo server --no-code-reloading # Disable code reloading
103
+ def self.example(*examples)
104
+ @examples += examples.flatten
105
+ end
106
+
107
+ # Specify an argument
108
+ #
109
+ # @param name [Symbol] the argument name
110
+ # @param options [Hash] a set of options
111
+ #
112
+ # @since 0.1.0
113
+ #
114
+ # @example Optional argument
115
+ # require "hanami/cli"
116
+ #
117
+ # class Hello < Hanami::CLI::Command
118
+ # argument :name
119
+ #
120
+ # def call(name: nil, **)
121
+ # if name.nil?
122
+ # puts "Hello, stranger"
123
+ # else
124
+ # puts "Hello, #{name}"
125
+ # end
126
+ # end
127
+ # end
128
+ #
129
+ # # $ foo hello
130
+ # # Hello, stranger
131
+ #
132
+ # # $ foo hello Luca
133
+ # # Hello, Luca
134
+ #
135
+ # @example Required argument
136
+ # require "hanami/cli"
137
+ #
138
+ # class Hello < Hanami::CLI::Command
139
+ # argument :name, required: true
140
+ #
141
+ # def call(name:, **)
142
+ # puts "Hello, #{name}"
143
+ # end
144
+ # end
145
+ #
146
+ # # $ foo hello Luca
147
+ # # Hello, Luca
148
+ #
149
+ # # $ foo hello
150
+ # # ERROR: "foo hello" was called with no arguments
151
+ # # Usage: "foo hello NAME"
152
+ #
153
+ # @example Multiple arguments
154
+ # require "hanami/cli"
155
+ #
156
+ # module Generate
157
+ # class Action < Hanami::CLI::Command
158
+ # argument :app, required: true
159
+ # argument :action, required: true
160
+ #
161
+ # def call(app:, action:, **)
162
+ # puts "Generating action: #{action} for app: #{app}"
163
+ # end
164
+ # end
165
+ # end
166
+ #
167
+ # # $ foo generate action web home
168
+ # # Generating action: home for app: web
169
+ #
170
+ # # $ foo generate action
171
+ # # ERROR: "foo generate action" was called with no arguments
172
+ # # Usage: "foo generate action APP ACTION"
173
+ #
174
+ # @example Description
175
+ # require "hanami/cli"
176
+ #
177
+ # class Hello < Hanami::CLI::Command
178
+ # argument :name, desc: "The name of the person to greet"
179
+ #
180
+ # def call(name: nil, **)
181
+ # # ...
182
+ # end
183
+ # end
184
+ #
185
+ # # $ foo hello --help
186
+ # # Command:
187
+ # # foo hello
188
+ # #
189
+ # # Usage:
190
+ # # foo hello [NAME]
191
+ # #
192
+ # # Arguments:
193
+ # # NAME # The name of the person to greet
194
+ # #
195
+ # # Options:
196
+ # # --help, -h # Print this help
197
+ def self.argument(name, options = {})
198
+ @arguments << Argument.new(name, options)
199
+ end
200
+
201
+ # Command line option (aka optional argument)
202
+ #
203
+ # @param name [Symbol] the param name
204
+ # @param options [Hash] a set of options
205
+ #
206
+ # @since 0.1.0
207
+ #
208
+ # @example Basic usage
209
+ # require "hanami/cli"
210
+ #
211
+ # class Console < Hanami::CLI::Command
212
+ # param :engine
213
+ #
214
+ # def call(engine: nil, **)
215
+ # puts "starting console (engine: #{engine || :irb})"
216
+ # end
217
+ # end
218
+ #
219
+ # # $ foo console
220
+ # # starting console (engine: irb)
221
+ #
222
+ # # $ foo console --engine=pry
223
+ # # starting console (engine: pry)
224
+ #
225
+ # @example List values
226
+ # require "hanami/cli"
227
+ #
228
+ # class Console < Hanami::CLI::Command
229
+ # param :engine, values: %w(irb pry ripl)
230
+ #
231
+ # def call(engine: nil, **)
232
+ # puts "starting console (engine: #{engine || :irb})"
233
+ # end
234
+ # end
235
+ #
236
+ # # $ foo console
237
+ # # starting console (engine: irb)
238
+ #
239
+ # # $ foo console --engine=pry
240
+ # # starting console (engine: pry)
241
+ #
242
+ # # $ foo console --engine=foo
243
+ # # Error: Invalid param provided
244
+ #
245
+ # @example Description
246
+ # require "hanami/cli"
247
+ #
248
+ # class Console < Hanami::CLI::Command
249
+ # param :engine, desc: "Force a console engine"
250
+ #
251
+ # def call(engine: nil, **)
252
+ # # ...
253
+ # end
254
+ # end
255
+ #
256
+ # # $ foo console --help
257
+ # # # ...
258
+ # #
259
+ # # Options:
260
+ # # --engine=VALUE # Force a console engine: (irb/pry/ripl)
261
+ # # --help, -h # Print this help
262
+ #
263
+ # @example Boolean
264
+ # require "hanami/cli"
265
+ #
266
+ # class Server < Hanami::CLI::Command
267
+ # param :code_reloading, type: :boolean, default: true
268
+ #
269
+ # def call(code_reloading:, **)
270
+ # puts "staring server (code reloading: #{code_reloading})"
271
+ # end
272
+ # end
273
+ #
274
+ # # $ foo server
275
+ # # starting server (code reloading: true)
276
+ #
277
+ # # $ foo server --no-code-reloading
278
+ # # starting server (code reloading: false)
279
+ #
280
+ # # $ foo server --help
281
+ # # # ...
282
+ # #
283
+ # # Options:
284
+ # # --[no]-code-reloading
285
+ #
286
+ # @example Aliases
287
+ # require "hanami/cli"
288
+ #
289
+ # class Server < Hanami::CLI::Command
290
+ # param :port, aliases: ["-p"]
291
+ #
292
+ # def call(options)
293
+ # puts "staring server (port: #{options.fetch(:port, 2300)})"
294
+ # end
295
+ # end
296
+ #
297
+ # # $ foo server
298
+ # # starting server (port: 2300)
299
+ #
300
+ # # $ foo server --port=2306
301
+ # # starting server (port: 2306)
302
+ #
303
+ # # $ foo server -p 2306
304
+ # # starting server (port: 2306)
305
+ #
306
+ # # $ foo server --help
307
+ # # # ...
308
+ # #
309
+ # # Options:
310
+ # # --port=VALUE, -p VALUE
311
+ def self.option(name, options = {})
312
+ @options << Option.new(name, options)
313
+ end
314
+
315
+ # @since 0.1.0
316
+ # @api private
317
+ def self.params
318
+ (@arguments + @options).uniq
319
+ end
320
+
321
+ # @since 0.1.0
322
+ # @api private
323
+ def self.default_params
324
+ params.each_with_object({}) do |param, result|
325
+ result[param.name] = param.default unless param.default.nil?
326
+ end
327
+ end
328
+
329
+ # @since 0.1.0
330
+ # @api private
331
+ def self.required_arguments
332
+ arguments.select(&:required?)
333
+ end
334
+
335
+ # @since 0.1.0
336
+ # @api private
337
+ def self.optional_arguments
338
+ arguments.reject(&:required?)
339
+ end
340
+
341
+ extend Forwardable
342
+
343
+ delegate %i[
344
+ description
345
+ examples
346
+ arguments
347
+ options
348
+ params
349
+ default_params
350
+ required_arguments
351
+ optional_arguments
352
+ ] => "self.class"
353
+
354
+ # @since 0.1.0
355
+ # @api private
356
+ attr_reader :command_name
357
+
358
+ # @since 0.1.0
359
+ # @api private
360
+ def initialize(command_name:, **)
361
+ @command_name = command_name
362
+ end
363
+ end
364
+ end
365
+ end