benry-cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +428 -0
- data/Rakefile +108 -0
- data/benry-cli.gemspec +30 -0
- data/lib/benry/cli.rb +595 -0
- data/test/cli_test.rb +1025 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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 $
|
data/Rakefile
ADDED
@@ -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
|