benry-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +428 -0
  3. data/Rakefile +108 -0
  4. data/benry-cli.gemspec +30 -0
  5. data/lib/benry/cli.rb +595 -0
  6. data/test/cli_test.rb +1025 -0
  7. metadata +79 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d4b05886752db49150fab5e3d6236f9e0ecdaf69
4
+ data.tar.gz: c91a2f5d46702753c3da2eb67e195e0310713d2a
5
+ SHA512:
6
+ metadata.gz: 71182c543f6ece58bfba085a6fe7afcf3a0ccc3f8302641143cd62d2b61dc71f8a0b6825e0a6998baf9f075e05e2ac1a1ac093a380c3ef1599453b56db52bd33
7
+ data.tar.gz: 91cfa77e7706d50d414bf1be9024a6a66d5548dc075d1a61bb29e247cbaf5a12f38ffca2cc221a1da46dfe56697fcd0b8144ccf443ef23383544fb85353e7aa4
@@ -0,0 +1,428 @@
1
+ Benry::CLI README
2
+ =================
3
+
4
+ ($Release: 0.1.0 $)
5
+
6
+ Benry::CLI is a MVC-like framework for command-line application.
7
+ It is suitable for command-line application such as `git`, `gem` or `rails`.
8
+
9
+ Compared with Rake, Benry::CLI have a distinct advantage:
10
+
11
+ * Benry::CLI can define sub-command which can take arguments and options.
12
+ * Rake can't define such sub-command.
13
+
14
+ If you use Rake as command-line application framework instead of build tool,
15
+ take account of Benry::CLI as an alternative of Rake.
16
+
17
+ (Benry::CLI requires Ruby >= 2.0)
18
+
19
+
20
+ Basic Example
21
+ -------------
22
+
23
+ ex1.rb:
24
+ ```ruby
25
+ require 'benry/cli'
26
+
27
+ ##
28
+ ## Define subclass of Benry::CLI::Action (= controller class).
29
+ ##
30
+ class HelloAction < Benry::CLI::Action
31
+
32
+ ##
33
+ ## Define action (= sub-command) with @action.()
34
+ ##
35
+ @action.(:hello, "print hello message")
36
+ def do_hello(name='World')
37
+ puts "Hello, #{name}!"
38
+ end
39
+
40
+ ##
41
+ ## When action name is nil then method name is used as action name instead.
42
+ ##
43
+ @action.(nil, "print goodbye message")
44
+ def goodbye(name='World')
45
+ puts "Goodbye, #{name}!"
46
+ end
47
+
48
+ end
49
+
50
+ VERSION = "1.0"
51
+
52
+ def main()
53
+ ##
54
+ ## Create application object with desription, version and action classes.
55
+ ## When 'action_classes' is nil, then all subclasses of Benry::CLI::Action
56
+ ## are used instead.
57
+ ##
58
+ classes = [
59
+ HelloAction,
60
+ ]
61
+ app = Benry::CLI::Application.new("example script",
62
+ version: VERSION,
63
+ action_classes: classes)
64
+ app.main()
65
+ end
66
+
67
+
68
+ if __FILE__ == $0
69
+ main()
70
+ end
71
+ ```
72
+
73
+ Example:
74
+ ```console
75
+ $ ruby ex1.rb hello
76
+ Hello, World!
77
+
78
+ $ ruby ex1.rb hello ruby
79
+ Hello, ruby!
80
+
81
+ $ ruby ex1.rb goodbye
82
+ Goodbye, World!
83
+
84
+ $ ruby ex1.rb goodbye sekai
85
+ Goodbye, sekai!
86
+ ```
87
+
88
+ Help mesage:
89
+ ```console
90
+ $ ruby ex1.rb --help # or: ruby ex1.rb help
91
+ example script
92
+
93
+ Usage:
94
+ ex1.rb [<options>] <action> [<args>...]
95
+
96
+ Options:
97
+ -h, --help : print help message
98
+ --version : print version
99
+
100
+ Actions:
101
+ goodbye : print goodbye message
102
+ hello : print hello message
103
+
104
+ (Run `ex1.rb help <action>' to show help message of each action.)
105
+ ```
106
+
107
+
108
+ Command-line Options
109
+ --------------------
110
+
111
+ ex2.rb:
112
+ ```ruby
113
+ require 'benry/cli'
114
+
115
+ class OptionTestAction < Benry::CLI::Action
116
+
117
+ ##
118
+ ## Define command-line options with @option.()
119
+ ##
120
+ @action.(:hello1, "say hello")
121
+ @option.('-q, --quiet' , "quiet mode") # no argument
122
+ @option.('-f, --format=TYPE' , "'text' or 'html'") # required arg
123
+ @option.('-d, --debug[=LEVEL]', "debug level") # optional arg
124
+ def do_hello1(name='World', quiet: nil, format: nil, debug: nil)
125
+ puts "name=%p, quiet=%p, format=%p, debug=%p" % \
126
+ [name, quiet, format, debug]
127
+ end
128
+
129
+ ##
130
+ ## Long-only version
131
+ ##
132
+ @action.(:hello2, "say hello")
133
+ @option.('--quiet' , "quiet mode") # no argument
134
+ @option.('--format=TYPE' , "'text' or 'html'") # required arg
135
+ @option.('--debug[=LEVEL]', "debug level") # optional arg
136
+ def do_hello2(name='World', quiet: nil, format: nil, debug: nil)
137
+ puts "name=%p, quiet=%p, format=%p, debug=%p" % \
138
+ [name, quiet, format, debug]
139
+ end
140
+
141
+ ##
142
+ ## Short-only version
143
+ ##
144
+ @action.(:hello3, "say hello")
145
+ @option.('-q' , "quiet mode") # no argument
146
+ @option.('-f TYPE' , "'text' or 'html'") # required arg
147
+ @option.('-d[=LEVEL]', "debug level") # optional arg
148
+ def do_hello3(name='World', q: nil, f: nil, d: nil)
149
+ puts "name=%p, q=%p, f=%p, d=%p" % \
150
+ [name, q, f, d]
151
+ end
152
+
153
+ ##
154
+ ## Change keyword arg name without '--long' option
155
+ ##
156
+ @action.(:hello4, "say hello")
157
+ @option.(:quiet, '-q ' , "quiet mode") # no argument
158
+ @option.(:format, '-f TYPE ' , "'text' or 'html'") # required arg
159
+ @option.(:debug, '-d[=LEVEL]' , "debug level") # optional arg
160
+ def do_hello4(name='World', quiet: nil, format: nil, debug: nil)
161
+ puts "name=%p, quit=%p, format=%p, debug=%p" % \
162
+ [name, quiet, format, debug]
163
+ end
164
+
165
+ end
166
+
167
+ def main()
168
+ app = Benry::CLI::Application.new(version: '1.0')
169
+ app.main()
170
+ end
171
+
172
+
173
+ if __FILE__ == $0
174
+ main()
175
+ end
176
+ ```
177
+
178
+ Example:
179
+ ```console
180
+ ## no options
181
+ $ ruby ex2.rb hello world
182
+ name="world", quiet=nil, format=nil, debug=nil
183
+
184
+ ## with some options
185
+ $ ruby ex2.rb hello -d -f foo.txt -d2 world
186
+ name="world", quiet=nil, format="foo.txt", debug="2"
187
+
188
+ ## notice that argument of '-d' is optional.
189
+ $ ruby ex2.rb hello -d
190
+ name="World", quiet=nil, format=nil, debug=true
191
+ $ ruby ex2.rb hello -d2
192
+ name="World", quiet=nil, format=nil, debug="2"
193
+ ```
194
+
195
+ Help message:
196
+ ```console
197
+ $ ruby ex2.rb hello -h
198
+ Usage:
199
+ ex2.rb [<options>] <action> [<args>...]
200
+
201
+ Options:
202
+ -h, --help : print help message
203
+ --version : print version
204
+
205
+ Actions:
206
+ hello1 : say hello
207
+ hello2 : say hello
208
+ hello3 : say hello
209
+ hello4 : say hello
210
+
211
+ (Run `ex2.rb help <action>' to show help message of each action.)
212
+ ```
213
+
214
+
215
+ Validation and Type Conversion
216
+ ------------------------------
217
+
218
+ ex3.rb:
219
+ ```ruby
220
+ require 'benry/cli'
221
+
222
+ class ValidationTestAction < Benry::CLI::Action
223
+
224
+ ##
225
+ ## @option.() can take a block argument which does both validation
226
+ ## and data conversion.
227
+ ##
228
+ @action.(:hello, "say hello")
229
+ @option.('-L, --log-level=N', "log level (1~5)") {|val|
230
+ val =~ /\A\d+\z/ or raise "positive integer expected."
231
+ val.to_i # convert string into integer
232
+ }
233
+ def do_hello(log_level: 1)
234
+ puts "log_level=#{log_level.inspect}"
235
+ end
236
+
237
+ end
238
+
239
+
240
+ if __FILE__ == $0
241
+ Benry::CLI::Application.new(version: '1.0').main()
242
+ end
243
+ ```
244
+
245
+ Example:
246
+ ```console
247
+ $ ruby ex3.rb hello -L abc
248
+ ERROR: -L abc: positive integer expected.
249
+ ```
250
+
251
+
252
+ Other Topics
253
+ ------------
254
+
255
+
256
+ ### Name Conversion Rule in Help Message
257
+
258
+ * Action name 'foo_bar' is printed as 'foo-bar' in help message.
259
+ * Long option '--foo-bar' corresponds to 'foo_bar' keyword argument.
260
+ * Parameter 'foo_bar' is printed as '<foo-bar>' in help message.
261
+ * Parameter 'foo_or_bar' is printed as '<foo|bar>' in help message (!!).
262
+
263
+ ex-argname.rb:
264
+ ```ruby
265
+ require 'benry/cli'
266
+
267
+ class FooAction < Benry::CLI::Action
268
+
269
+ ##
270
+ ## parameter name -> in help message
271
+ ## ----------------------------------------
272
+ ## file_name -> <file-name>
273
+ ## file_or_dir -> <file|dir>
274
+ ## file_name=nil -> [<file-name>]
275
+ ## *fil_namee -> [<file-name>...]
276
+ ##
277
+ @action.(:bla_bla_bla, "test")
278
+ @option.('-v, --verbose-mode', "enable verbose mode")
279
+ def do_something(file_name, file_or_dir=nil, verbose_mode: nil)
280
+ # ...
281
+ end
282
+
283
+ end
284
+
285
+ if __FILE__ == $0
286
+ Benry::CLI::Application.new().main()
287
+ end
288
+ ```
289
+
290
+ Example:
291
+ ```console
292
+ $ ruby ex-argname.rb help bla-bla-bla
293
+ test
294
+
295
+ Usage:
296
+ argname.rb bla-bla-bla [<options>] <file-name> [<file|dir>]
297
+
298
+ Options:
299
+ -h, --help : print help message
300
+ -v, --verbose-mode : enable verbose mode
301
+ ```
302
+
303
+
304
+ ### Prefix of Action Name
305
+
306
+ `self.prefix = "..."` sets prefix name of action.
307
+
308
+ mygit.rb:
309
+ ```ruby
310
+ require 'benry/cli'
311
+
312
+ class GitAction < Benry::CLI::Action
313
+
314
+ ##
315
+ ## add prefix 'git:' to each actions
316
+ ##
317
+ self.prefix = 'git'
318
+
319
+ @action.(:stage, "same as 'git add -p'")
320
+ def do_stage(*filenames)
321
+ system 'git', 'add', '-p', *filenames
322
+ end
323
+
324
+ @action.(:staged, "same as 'git diff --cached'")
325
+ def do_staged(*filenames)
326
+ system 'git', 'diff', '--cached', *filenames
327
+ end
328
+
329
+ @action.(:unstaged, "same as 'git reset HEAD'")
330
+ def do_unstage(*filenames)
331
+ system 'git', 'reset', 'HEAD', *filenames
332
+ end
333
+
334
+ end
335
+
336
+ if __FILE__ == $0
337
+ Benry::CLI::Application.new("git wrapper", version: '1.0').main()
338
+ end
339
+ ```
340
+
341
+ Example (prefix 'git:' is added to actions):
342
+ ```console
343
+ $ ruby mygit.rb --help
344
+ git wrapper
345
+
346
+ Usage:
347
+ mygit.rb [<options>] <action> [<args>...]
348
+
349
+ Options:
350
+ -h, --help : print help message
351
+ --version : print version
352
+
353
+ Actions:
354
+ git:stage : same as 'git add -p'
355
+ git:staged : same as 'git diff --cached'
356
+ git:unstaged : same as 'git reset HEAD'
357
+
358
+ (Run `mygit.rb help <action>' to show help message of each action.)
359
+ ```
360
+
361
+ Of course, it is possible to specify prefix name with `@action.(:'git:stage')`.
362
+
363
+
364
+ #### Multiple-Value Option
365
+
366
+ Validator callback can take option values as second argument.
367
+ Using it, you can define options which can be specified more than once
368
+ (such as `-I` option of gcc).
369
+
370
+ ex-multival.rb:
371
+ ```ruby
372
+ require 'benry/cli'
373
+
374
+ class TestAction < Benry::CLI::Action
375
+
376
+ @action.(nil, "example of multi-value option")
377
+ @option.("-I, --include=path", "include path") {|val, opt|
378
+ opt['include'] ||= []
379
+ opt['include'] << val
380
+ opt['include']
381
+ }
382
+ def multival(include: nil)
383
+ p include
384
+ end
385
+
386
+ end
387
+
388
+ if __FILE__ == $0
389
+ Benry::CLI::Application.new("multi-value example", version: '1.0').main()
390
+ end
391
+ ```
392
+
393
+ Example:
394
+ ```console
395
+ $ ruby ex-multival.rb multival -I /path1 -I /path2 -I /path3
396
+ ["/path1", "/path2", "/path3"]
397
+ ```
398
+
399
+
400
+ #### Parse Command Options without Application
401
+
402
+ If you need just command-line option parser instead of application,
403
+ use `Benry::CLI::OptionParser` class.
404
+
405
+ ex-parser.rb:
406
+ ```ruby
407
+ require 'benry/cli'
408
+
409
+ parser = Benry::CLI::OptionParser.new
410
+ parser.option("-h, --help", "print help")
411
+ parser.option("-f, --file=FILE", "load filename")
412
+ parser.option("-i, --indent=N", "indent") {|val|
413
+ val =~ /\A\d+\z/ or raise "integer expected." # validation
414
+ val.to_i # convertion (string -> integer)
415
+ }
416
+ args = ['-hftest.txt', '-i', '2', "x", "y"]
417
+ options = parser.parse(args)
418
+ puts options #=> {"help"=>true, "file"=>"test.txt", "indent"=>2}
419
+ puts args.inspect #=> ["x", "y"]
420
+ ```
421
+
422
+
423
+ License and Copyright
424
+ ---------------------
425
+
426
+ $License: MIT License $
427
+
428
+ $Copyright: copyright(c) 2016 kuwata-lab.com all rights reserved $
@@ -0,0 +1,108 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ project = "benry-cli"
5
+ release = ENV['RELEASE'] || "0.0.0"
6
+ copyright = "copyright(c) 2016 kuwata-lab.com all rights reserved"
7
+ license = "MIT License"
8
+
9
+ target_files = Dir[*%W[
10
+ README.md MIT-LICENSE.txt Rakefile
11
+ lib/**/*.rb
12
+ test/**/*_test.rb
13
+ #{project}.gemspec
14
+ ]]
15
+
16
+
17
+ task :default => :help
18
+
19
+
20
+ desc "show help"
21
+ task :help do
22
+ puts "rake help # help"
23
+ puts "rake test # run test"
24
+ puts "rake package RELEASE=X.X.X # create gem file"
25
+ puts "rake publish RELEASE=X.X.X # upload gem file"
26
+ puts "rake clean # remove files"
27
+ end
28
+
29
+
30
+ desc "do test"
31
+ task :test do
32
+ sh "ruby", *Dir.glob("test/*.rb")
33
+ end
34
+
35
+
36
+ desc "create package"
37
+ task :package do
38
+ release != "0.0.0" or
39
+ raise "specify $RELEASE"
40
+ ## copy
41
+ dir = "build"
42
+ rm_rf dir if File.exist?(dir)
43
+ mkdir dir
44
+ target_files.each do |file|
45
+ dest = File.join(dir, File.dirname(file))
46
+ mkdir_p dest, :verbose=>false unless File.exist?(dest)
47
+ cp file, "#{dir}/#{file}"
48
+ end
49
+ ## edit
50
+ Dir.glob("#{dir}/**/*").each do |file|
51
+ next unless File.file?(file)
52
+ File.open(file, 'rb+') do |f|
53
+ s = f.read()
54
+ s = s.gsub(/\$Release\:.*?\$/, "$"+"Release: #{release} $")
55
+ s = s.gsub(/\$Copyright\:.*?\$/, "$"+"Copyright: #{copyright} $")
56
+ s = s.gsub(/\$License\:.*?\$/, "$"+"License: #{license} $")
57
+ #
58
+ f.rewind()
59
+ f.truncate(0)
60
+ f.write(s)
61
+ end
62
+ end
63
+ ## build
64
+ chdir dir do
65
+ sh "gem build #{project}.gemspec"
66
+ end
67
+ mv "#{dir}/#{project}-#{release}.gem", "."
68
+ end
69
+
70
+
71
+ desc "upload gem file to rubygems.org"
72
+ task :publish do
73
+ release != "0.0.0" or
74
+ raise "specify $RELEASE"
75
+ #
76
+ gemfile = "#{project}-#{release}.gem"
77
+ print "** Are you sure to publish #{gemfile}? [y/N]: "
78
+ ans = $stdin.gets().strip()
79
+ if ans.downcase.start_with?("y")
80
+ sh "gem push #{gemfile}"
81
+ sh "git tag ruby-#{project}-#{release}"
82
+ sh "git push --tags"
83
+ end
84
+ end
85
+
86
+
87
+ desc "remove build files"
88
+ task :clean do
89
+ rm_rf "build"
90
+ #
91
+ filenames = []
92
+ content = File.read('README.md')
93
+ content.scan(/^(\w[-\w]+.rb):\n```ruby\n(.*?)```/m) do
94
+ filename, code = $1, $2
95
+ filenames << filename
96
+ end
97
+ rm_f filenames
98
+ end
99
+
100
+
101
+ desc "retrieve sample codes from README file"
102
+ task :'readme:examples' do
103
+ content = File.read('README.md')
104
+ content.scan(/^(\w[-\w]+.rb):\n```ruby\n(.*?)```/m) do
105
+ filename, code = $1, $2
106
+ File.open(filename, 'w') {|f| f.write(code) }
107
+ end
108
+ end