cliapp 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/MIT-LICENSE +21 -0
- data/README.md +131 -0
- data/Rakefile.rb +9 -0
- data/cliapp.gemspec +29 -0
- data/lib/cliapp.rb +430 -0
- data/task/common-task.rb +67 -0
- data/task/release-task.rb +77 -0
- data/test/action_test.rb +54 -0
- data/test/application_test.rb +547 -0
- data/test/init.rb +11 -0
- data/test/module_test.rb +92 -0
- data/test/util_test.rb +80 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b398e8d768022a0d3ecaf12d9e63f1d5ada3a18e14f7fa0dcb737f2ec4b55aaf
|
4
|
+
data.tar.gz: d1ec659812a404e055a0b11b873d118012e4090cd51bffa0f3a9639c3f0478b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 98e18468f041be66b58e3ee92523a35b999dc9396fa4d60d6125a9007830dc410bc7cc1d2d3bae69992cc05de0ff7c2a24512ac3d52a642ef1abbacaf04b6a3e
|
7
|
+
data.tar.gz: 220c43cf0dbf9a2db65bdd5d48a0fd02d6f44c9a3379b05673c670d9116bba780f05ac73e06ef9ac1f883a598f417b2ad6f8450159070e627c7b1c4a5ef9f351
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 kwatch@gmail.com
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
CLIApp
|
2
|
+
======
|
3
|
+
|
4
|
+
($Version: 0.1.0 $)
|
5
|
+
|
6
|
+
CLIApp is a small framework for command-line application.
|
7
|
+
If you need to create a CLI app such as Git or Docker, CLIApp is one of the solutions.
|
8
|
+
|
9
|
+
* GitHub: https://github.com/kwatch/cliapp-ruby/
|
10
|
+
|
11
|
+
|
12
|
+
Quick Start
|
13
|
+
-----------
|
14
|
+
|
15
|
+
```console
|
16
|
+
$ gem install cliapp
|
17
|
+
$ ruby -r cliapp -e 'puts CLIApp.skeleton' > sample
|
18
|
+
$ chmod a+x sample
|
19
|
+
$ ./sample --help | less
|
20
|
+
$ ./sample hello
|
21
|
+
Hello, world!
|
22
|
+
$ ./sample hello Alice --lang=fr
|
23
|
+
Bonjour, Alice!
|
24
|
+
```
|
25
|
+
|
26
|
+
|
27
|
+
Sample Code
|
28
|
+
-----------
|
29
|
+
|
30
|
+
File: sample
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
#!/usr/bin/env ruby
|
34
|
+
# coding: utf-8
|
35
|
+
# frozen_string_literal: true
|
36
|
+
|
37
|
+
require 'cliapp'
|
38
|
+
|
39
|
+
## create an application object
|
40
|
+
app = CLIApp.new("Sample", "Sample Application",
|
41
|
+
#command: "sample", # default: File.basename($0)
|
42
|
+
version: "1.0.0")
|
43
|
+
app.global_options({
|
44
|
+
:help => ["-h", "--help" , "print help message"],
|
45
|
+
:version => [ "--version" , "print version number"],
|
46
|
+
:list => ["-l", "--list" , "list action names"],
|
47
|
+
})
|
48
|
+
APP = app
|
49
|
+
|
50
|
+
## 'hello' action
|
51
|
+
app.action("hello", "greeting message", {
|
52
|
+
:lang => ["-l", "--lang=<en|fr|it>", "language", ["en", "it", "fr"]],
|
53
|
+
}) do |name="world", lang: "en"|
|
54
|
+
case lang
|
55
|
+
when "en" ; puts "Hello, #{name}!"
|
56
|
+
when "fr" ; puts "Bonjour, #{name}!"
|
57
|
+
when "it" ; puts "Chao, #{name}!"
|
58
|
+
else raise "** internal error: lang=#{lang.inspect}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
## 'clean' action
|
63
|
+
app.action("clean", "delete garbage files (& product files too if '-a')", {
|
64
|
+
:all => ["-a", "--all", "delete product files, too"],
|
65
|
+
}) do |all: false|
|
66
|
+
require 'fileutils' unless defined?(FileUtils)
|
67
|
+
FileUtils.rm_r(Dir.glob(GARBAGE_FILES), verbose: true)
|
68
|
+
FileUtils.rm_r(Dir.glob(PRODUCT_FILES), verbose: true) if all
|
69
|
+
end
|
70
|
+
GARBAGE_FILES = []
|
71
|
+
PRODUCT_FILES = []
|
72
|
+
|
73
|
+
## main
|
74
|
+
def main(argv=ARGV)
|
75
|
+
APP.run(*argv)
|
76
|
+
return 0
|
77
|
+
rescue OptionParser::ParseError, CLIApp::ActionError => exc
|
78
|
+
$stderr.puts "[ERROR] #{exc.message}"
|
79
|
+
return 1
|
80
|
+
end
|
81
|
+
|
82
|
+
if __FILE__ == $0
|
83
|
+
status_code = main(ARGV)
|
84
|
+
exit status_code
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
Output example:
|
89
|
+
|
90
|
+
```console
|
91
|
+
$ chmod a+x sample
|
92
|
+
|
93
|
+
$ ./sample -l
|
94
|
+
clean : delete garbage files (& product files too if '-a')
|
95
|
+
hello : greeting message
|
96
|
+
|
97
|
+
$ ./sample hello --help
|
98
|
+
sample hello --- greeting message
|
99
|
+
|
100
|
+
Usage:
|
101
|
+
$ sample hello [<options>] [<name>]
|
102
|
+
|
103
|
+
Options:
|
104
|
+
-l, --lang=<en|fr|it> language
|
105
|
+
|
106
|
+
$ ./sample hello
|
107
|
+
Hello, world!
|
108
|
+
|
109
|
+
$ ./sample hello Alice --lang=fr
|
110
|
+
Bonjour, Alice!
|
111
|
+
|
112
|
+
$ ./sample hello Alice Bob
|
113
|
+
[ERROR] Too many arguments.
|
114
|
+
|
115
|
+
$ ./sample hello --lang
|
116
|
+
[ERROR] missing argument: --lang
|
117
|
+
|
118
|
+
$ ./sample hello --lang=ja
|
119
|
+
[ERROR] invalid argument: --lang=ja
|
120
|
+
|
121
|
+
$ ./sample hello --language=en
|
122
|
+
[ERROR] invalid option: --language=en
|
123
|
+
```
|
124
|
+
|
125
|
+
|
126
|
+
License and Copyright
|
127
|
+
---------------------
|
128
|
+
|
129
|
+
$License: MIT License $
|
130
|
+
|
131
|
+
$Copyright: copyright(c) 2024 kwatch@gmail.com $
|
data/Rakefile.rb
ADDED
data/cliapp.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'cliapp'
|
5
|
+
spec.version = '$Version: 0.1.0 $'.split()[1]
|
6
|
+
spec.author = 'kwatch'
|
7
|
+
spec.email = 'kwatch@gmail.com'
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
9
|
+
spec.homepage = 'https://github.com/kwatch/cliapp-ruby'
|
10
|
+
spec.summary = "CLI Application Framework"
|
11
|
+
spec.description = <<-'END'
|
12
|
+
A small framework for CLI Applications such as Git, Docker, NPM, etc.
|
13
|
+
END
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.files = Dir[*%w[
|
16
|
+
README.md MIT-LICENSE Rakefile.rb cliapp.gemspec
|
17
|
+
lib/**/*.rb
|
18
|
+
test/**/*.rb
|
19
|
+
task/**/*.rb
|
20
|
+
]]
|
21
|
+
spec.executables = []
|
22
|
+
spec.bindir = 'bin'
|
23
|
+
spec.require_path = 'lib'
|
24
|
+
spec.test_files = Dir['test/**/*_test.rb']
|
25
|
+
#spec.extra_rdoc_files = ['README.md', 'CHANGES.md']
|
26
|
+
|
27
|
+
spec.required_ruby_version = ">= 2.4"
|
28
|
+
spec.add_development_dependency 'oktest', '~> 1.4'
|
29
|
+
end
|
data/lib/cliapp.rb
ADDED
@@ -0,0 +1,430 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
##
|
5
|
+
## Command-Line Application Framework
|
6
|
+
##
|
7
|
+
## $Version: 0.1.0 $
|
8
|
+
## $Copyright: copyright (c)2014 kwatch@gmail.com $
|
9
|
+
## $License: MIT License $
|
10
|
+
##
|
11
|
+
|
12
|
+
require 'optparse'
|
13
|
+
|
14
|
+
|
15
|
+
module CLIApp
|
16
|
+
|
17
|
+
|
18
|
+
class ActionError < StandardError; end
|
19
|
+
class ActionNotFoundError < ActionError; end
|
20
|
+
class ActionTooFewArgsError < ActionError; end
|
21
|
+
class ActionTooManyArgsError < ActionError; end
|
22
|
+
|
23
|
+
|
24
|
+
class Action
|
25
|
+
|
26
|
+
def initialize(name, desc=nil, option_schema={}, &block)
|
27
|
+
@name = name
|
28
|
+
@desc = desc
|
29
|
+
@option_schema = option_schema
|
30
|
+
@block = block
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :name, :desc, :option_schema, :block
|
34
|
+
|
35
|
+
def call(*args, **opts)
|
36
|
+
#; [!pc2hw] raises error when fewer arguments.
|
37
|
+
n_min, n_max = Util.arity_of_proc(@block)
|
38
|
+
args.length >= n_min or
|
39
|
+
raise ActionTooFewArgsError, "Too few arguments."
|
40
|
+
#; [!6vdhh] raises error when too many arguments.
|
41
|
+
n_max == nil || args.length <= n_max or
|
42
|
+
raise ActionTooManyArgsError, "Too many arguments."
|
43
|
+
#; [!7n4hs] invokes action block with args and kwargs.
|
44
|
+
if opts.empty? # for Ruby < 2.7
|
45
|
+
@block.call(*args) # for Ruby < 2.7
|
46
|
+
else
|
47
|
+
@block.call(*args, **opts)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
class Config
|
55
|
+
|
56
|
+
def initialize(name: nil, desc: nil, version: nil, command: nil,
|
57
|
+
help_indent: nil, help_option_width: nil, help_action_width: nil,
|
58
|
+
actionlist_width: nil, actionlist_format: nil)
|
59
|
+
command ||= File.basename($0)
|
60
|
+
@name = name || command # ex: "FooBar"
|
61
|
+
@desc = desc # ex: "Foo Bar application"
|
62
|
+
@command = command # ex: "foobar"
|
63
|
+
@version = version # ex: "1.0.0"
|
64
|
+
@help_indent = help_indent || " "
|
65
|
+
@help_option_width = help_option_width || 22
|
66
|
+
@help_action_width = help_action_width || 22
|
67
|
+
@actionlist_width = actionlist_width || 16
|
68
|
+
@actionlist_format = actionlist_format || nil # ex: "%-#{@actionlist_width}s : %s"
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_accessor :name, :desc, :command, :version
|
72
|
+
attr_accessor :help_indent, :help_option_width, :help_action_width
|
73
|
+
attr_accessor :actionlist_width, :actionlist_format
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def self.new(name, desc, version: nil, command: nil, **kws, &gopts_handler)
|
79
|
+
#; [!gpvqe] creates new Config object internally.
|
80
|
+
config = Config.new(name: name, desc: desc, version: version, command: command, **kws)
|
81
|
+
#; [!qyunk] creates new Application object with config object created.
|
82
|
+
return Application.new(config, &gopts_handler)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
class Application
|
87
|
+
|
88
|
+
def initialize(config, &gopts_handler)
|
89
|
+
@config = config
|
90
|
+
@gopts_handler = gopts_handler
|
91
|
+
@gopts_schema = {} # ex: {:help => ["-h", "--hel", "help message"}
|
92
|
+
@actions = {} # ex: {"clean" => Action.new(:clean, "delete files", {:all=>["-a", "all"]})}
|
93
|
+
end
|
94
|
+
|
95
|
+
attr_reader :config
|
96
|
+
|
97
|
+
def global_options(option_schema={})
|
98
|
+
#; [!2kq26] accepts global option schema.
|
99
|
+
@gopts_schema = option_schema
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def action(name, desc, schema_dict={}, &block)
|
104
|
+
#; [!i1jjg] converts action name into string.
|
105
|
+
name = name.to_s
|
106
|
+
#; [!kculn] registers an action.
|
107
|
+
action = Action.new(name, desc, schema_dict, &block)
|
108
|
+
@actions[name] = action
|
109
|
+
#; [!82n8q] returns an action object.
|
110
|
+
return action
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_action(name)
|
114
|
+
#; [!hop4z] returns action object if found, nil if else.
|
115
|
+
return @actions[name.to_s]
|
116
|
+
end
|
117
|
+
|
118
|
+
def each_action(sort: false, &b)
|
119
|
+
#; [!u46wo] returns Enumerator object if block not given.
|
120
|
+
return to_enum(:each_action, sort: sort) unless block_given?()
|
121
|
+
#; [!yorp6] if `sort: true` passed, sort actions by name.
|
122
|
+
names = @actions.keys
|
123
|
+
names = names.sort if sort
|
124
|
+
#; [!ealgm] yields each action object.
|
125
|
+
names.each do |name|
|
126
|
+
yield @actions[name]
|
127
|
+
end
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
def run(*args)
|
132
|
+
#; [!qv5fz] parses global options (not parses action options).
|
133
|
+
global_opts = parse_global_options(args)
|
134
|
+
#; [!kveua] handles global options such as '--help'.
|
135
|
+
done = handle_global_options(global_opts)
|
136
|
+
return if done
|
137
|
+
#; [!j029i] prints help message if no action name specified.
|
138
|
+
if args.empty?
|
139
|
+
done = do_when_action_not_specified(global_opts)
|
140
|
+
return if done
|
141
|
+
end
|
142
|
+
#; [!43u4y] raises error if action name is unknown.
|
143
|
+
action_name = args.shift()
|
144
|
+
action = get_action(action_name) or
|
145
|
+
raise ActionNotFoundError, "#{action_name}: Action not found."
|
146
|
+
#; [!lm0ir] parses all action options even after action args.
|
147
|
+
action_opts = parse_action_options(action, args)
|
148
|
+
#; [!ehshp] prints action help if action option contains help option.
|
149
|
+
if action_opts[:help]
|
150
|
+
print action_help_message(action)
|
151
|
+
return
|
152
|
+
end
|
153
|
+
#; [!0nwwe] invokes an action with action args and options.
|
154
|
+
action.call(*args, **action_opts)
|
155
|
+
end
|
156
|
+
|
157
|
+
def handle_global_options(global_opts)
|
158
|
+
#; [!6n0w0] when '-h' or '--help' specified, prints help message and returns true.
|
159
|
+
if global_opts[:help]
|
160
|
+
print application_help_message()
|
161
|
+
return true
|
162
|
+
end
|
163
|
+
#; [!zii8c] when '-V' or '--version' specified, prints version number and returns true.
|
164
|
+
if global_opts[:version]
|
165
|
+
puts @config.version
|
166
|
+
return true
|
167
|
+
end
|
168
|
+
#; [!csw5l] when '-l' or '--list' specified, prints action list and returns true.
|
169
|
+
if global_opts[:list]
|
170
|
+
print list_actions()
|
171
|
+
return true
|
172
|
+
end
|
173
|
+
#; [!5y8ph] if global option handler block specified, call it.
|
174
|
+
if (handler = @gopts_handler)
|
175
|
+
return !! handler.call(global_opts)
|
176
|
+
end
|
177
|
+
#; [!s816x] returns nil if global options are not handled.
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def application_help_message(width: nil, indent: nil)
|
182
|
+
#; [!p02s2] builds application help message.
|
183
|
+
#; [!41l2g] includes version number if it is specified.
|
184
|
+
#; [!2eycw] includes 'Options:' section if any global options exist.
|
185
|
+
#; [!x3dim] includes 'Actions:' section if any actions defined.
|
186
|
+
#; [!vxcin] help message will be affcted by config.
|
187
|
+
c = @config
|
188
|
+
indent ||= c.help_indent
|
189
|
+
options_str = option_help_message(@gopts_schema, width: width, indent: indent)
|
190
|
+
format = "#{indent}%-#{width || c.help_action_width}s %s\n"
|
191
|
+
actions_str = each_action(sort: true).collect {|action|
|
192
|
+
format % [action.name, action.desc]
|
193
|
+
}.join()
|
194
|
+
optstr = options_str.empty? ? "" : " [<options>]"
|
195
|
+
actstr = actions_str.empty? ? "" : " <action> [<arguments>...]"
|
196
|
+
ver = c.version ? " (#{c.version})" : nil
|
197
|
+
sb = []
|
198
|
+
sb << <<"END"
|
199
|
+
#{c.name}#{ver} --- #{c.desc}
|
200
|
+
|
201
|
+
Usage:
|
202
|
+
#{indent}$ #{c.command}#{optstr}#{actstr}
|
203
|
+
END
|
204
|
+
sb << (options_str.empty? ? "" : <<"END")
|
205
|
+
|
206
|
+
Options:
|
207
|
+
#{options_str.chomp()}
|
208
|
+
END
|
209
|
+
sb << (actions_str.empty? ? "" : <<"END")
|
210
|
+
|
211
|
+
Actions:
|
212
|
+
#{actions_str.chomp()}
|
213
|
+
END
|
214
|
+
return sb.join()
|
215
|
+
end
|
216
|
+
|
217
|
+
def action_help_message(action, width: nil, indent: nil)
|
218
|
+
#; [!ny72g] build action help message.
|
219
|
+
#; [!pr2vy] includes 'Options:' section if any options exist.
|
220
|
+
#; [!1xggx] help message will be affcted by config.
|
221
|
+
options_str = option_help_message(action.option_schema, width: width, indent: indent)
|
222
|
+
optstr = options_str.empty? ? "" : " [<options>]"
|
223
|
+
argstr = Util.argstr_of_proc(action.block)
|
224
|
+
c = @config
|
225
|
+
sb = []
|
226
|
+
sb << <<"END"
|
227
|
+
#{c.command} #{action.name} --- #{action.desc}
|
228
|
+
|
229
|
+
Usage:
|
230
|
+
#{c.help_indent}$ #{c.command} #{action.name}#{optstr}#{argstr}
|
231
|
+
END
|
232
|
+
sb << (options_str.empty? ? "" : <<"END")
|
233
|
+
|
234
|
+
Options:
|
235
|
+
#{options_str.chomp()}
|
236
|
+
END
|
237
|
+
return sb.join()
|
238
|
+
end
|
239
|
+
|
240
|
+
def parse_global_options(args)
|
241
|
+
#; [!o83ty] parses global options and returns it.
|
242
|
+
parser = new_parser()
|
243
|
+
global_opts = prepare_parser(parser, @gopts_schema)
|
244
|
+
parser.order!(args) # not parse options after arguments
|
245
|
+
return global_opts
|
246
|
+
end
|
247
|
+
|
248
|
+
def parse_action_options(action, args)
|
249
|
+
#; [!5m767] parses action options and returns it.
|
250
|
+
parser = new_parser()
|
251
|
+
action_opts = prepare_parser(parser, action.option_schema)
|
252
|
+
#; [!k2cto] adds '-h, --help' option automatically.
|
253
|
+
parser.on("-h", "--help", "print help message") {|v| action_opts[:help] = v }
|
254
|
+
parser.permute!(args) # parse all options even after arguments
|
255
|
+
return action_opts
|
256
|
+
end
|
257
|
+
|
258
|
+
protected
|
259
|
+
|
260
|
+
def prepare_parser(parser, schema_dict, opts={})
|
261
|
+
#; [!vcgq0] adds all option schema into parser.
|
262
|
+
## ex: schema_dict == {:help => ["-h", "--help", "help msg"]}
|
263
|
+
schema_dict.each do |key, arr|
|
264
|
+
parser.on(*arr) {|v| opts[key] = v }
|
265
|
+
end
|
266
|
+
#; [!lcpvw] returns hash object which stores options.
|
267
|
+
return opts
|
268
|
+
end
|
269
|
+
|
270
|
+
def new_parser(*args)
|
271
|
+
#; [!lnbpm] creates new parser object.
|
272
|
+
parser = OptionParser.new(*args)
|
273
|
+
#parser.require_exact = true
|
274
|
+
return parser
|
275
|
+
end
|
276
|
+
|
277
|
+
def option_help_message(option_schema, width: nil, indent: nil)
|
278
|
+
#; [!lfnlq] builds help message of options.
|
279
|
+
c = @config
|
280
|
+
width ||= c.help_option_width
|
281
|
+
indent ||= c.help_indent
|
282
|
+
parser = new_parser(nil, width, indent)
|
283
|
+
prepare_parser(parser, option_schema)
|
284
|
+
return parser.summarize().join()
|
285
|
+
end
|
286
|
+
|
287
|
+
def list_actions()
|
288
|
+
#; [!1xggx] output will be affcted by config.
|
289
|
+
c = @config
|
290
|
+
format = c.actionlist_format || "%-#{c.actionlist_width}s : %s"
|
291
|
+
format += "\n" unless format.end_with?("\n")
|
292
|
+
#; [!g99qx] returns list of action names and descriptions as a string.
|
293
|
+
#; [!rl5hs] sorts actions by name.
|
294
|
+
return each_action(sort: true).collect {|action|
|
295
|
+
#; [!rlak5] print only the first line of multiline description.
|
296
|
+
desc = (action.desc || "").each_line.take(1).first.chomp()
|
297
|
+
format % [action.name, desc]
|
298
|
+
}.join()
|
299
|
+
end
|
300
|
+
|
301
|
+
def do_when_action_not_specified(global_opts)
|
302
|
+
#; [!w5lq9] prints application help message.
|
303
|
+
print application_help_message()
|
304
|
+
#; [!txqnr] returns true which means 'done'.
|
305
|
+
return true
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
module Util
|
312
|
+
module_function
|
313
|
+
|
314
|
+
def arity_of_proc(proc_)
|
315
|
+
#; [!get6i] returns min and max arity of proc object.
|
316
|
+
#; [!ghrxo] returns nil as max arity if proc has variable param.
|
317
|
+
n_max = 0
|
318
|
+
n_min = proc_.arity
|
319
|
+
n_min = - (n_min + 1) if n_min < 0
|
320
|
+
has_rest = false
|
321
|
+
proc_.parameters.each do |ptype, _|
|
322
|
+
case ptype
|
323
|
+
when :req, :opt ; n_max += 1
|
324
|
+
when :rest ; has_rest = true
|
325
|
+
end
|
326
|
+
end
|
327
|
+
return n_min, (has_rest ? nil : n_max)
|
328
|
+
end
|
329
|
+
|
330
|
+
def argstr_of_proc(proc_)
|
331
|
+
#; [!gbk7b] generates argument string of proc object.
|
332
|
+
n = proc_.arity
|
333
|
+
n = - (n + 1) if n < 0
|
334
|
+
sb = []; cnt = 0
|
335
|
+
proc_.parameters.each do |(ptype, pname)|
|
336
|
+
aname = param2argname(pname)
|
337
|
+
case ptype
|
338
|
+
when :req, :opt
|
339
|
+
#; [!b6gzp] required param should be '<param>'.
|
340
|
+
#; [!q1030] optional param should be '[<param>]'.
|
341
|
+
n -= 1
|
342
|
+
if n >= 0 ; sb << " <#{aname}>"
|
343
|
+
else ; sb << " [<#{aname}>" ; cnt += 1
|
344
|
+
end
|
345
|
+
when :rest
|
346
|
+
#; [!osxwq] variable param should be '[<param>...]'.
|
347
|
+
sb << " [<#{aname}>...]"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
sb << ("]" * cnt)
|
351
|
+
return sb.join()
|
352
|
+
end
|
353
|
+
|
354
|
+
def param2argname(name)
|
355
|
+
#; [!52dzl] converts 'yes_or_no' to 'yes|no'.
|
356
|
+
#; [!6qkk6] converts 'file__html' to 'file.html'.
|
357
|
+
#; [!2kbhe] converts 'aa_bb_cc' to 'aa-bb-cc'.
|
358
|
+
name = name.to_s
|
359
|
+
name = name.gsub('_or_', '|') # ex: 'yes_or_no' -> 'yes|no'
|
360
|
+
name = name.gsub('__', '.') # ex: 'file__html' -> 'file.html'
|
361
|
+
name = name.gsub('_', '-') # ex: 'src_dir' -> 'src-dir'
|
362
|
+
return name
|
363
|
+
end
|
364
|
+
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
def self.skeleton()
|
369
|
+
#; [!zls9g] returns example code.
|
370
|
+
return File.read(__FILE__).split(/^__END__\n/, 2)[1]
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
__END__
|
378
|
+
#!/usr/bin/env ruby
|
379
|
+
# coding: utf-8
|
380
|
+
# frozen_string_literal: true
|
381
|
+
|
382
|
+
require 'cliapp'
|
383
|
+
|
384
|
+
## create an application object
|
385
|
+
app = CLIApp.new("Sample", "Sample Application",
|
386
|
+
#command: "sample", # default: File.basename($0)
|
387
|
+
version: "1.0.0")
|
388
|
+
app.global_options({
|
389
|
+
:help => ["-h", "--help" , "print help message"],
|
390
|
+
:version => [ "--version" , "print version number"],
|
391
|
+
:list => ["-l", "--list" , "list action names"],
|
392
|
+
})
|
393
|
+
APP = app
|
394
|
+
|
395
|
+
## 'hello' action
|
396
|
+
app.action("hello", "greeting message", {
|
397
|
+
:lang => ["-l", "--lang=<en|fr|it>", "language", ["en", "it", "fr"]],
|
398
|
+
}) do |name="world", lang: "en"|
|
399
|
+
case lang
|
400
|
+
when "en" ; puts "Hello, #{name}!"
|
401
|
+
when "fr" ; puts "Bonjour, #{name}!"
|
402
|
+
when "it" ; puts "Chao, #{name}!"
|
403
|
+
else raise "** internal error: lang=#{lang.inspect}"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
## 'clean' action
|
408
|
+
app.action("clean", "delete garbage files (& product files too if '-a')", {
|
409
|
+
:all => ["-a", "--all", "delete product files, too"],
|
410
|
+
}) do |all: false|
|
411
|
+
require 'fileutils' unless defined?(FileUtils)
|
412
|
+
FileUtils.rm_r(Dir.glob(GARBAGE_FILES), verbose: true)
|
413
|
+
FileUtils.rm_r(Dir.glob(PRODUCT_FILES), verbose: true) if all
|
414
|
+
end
|
415
|
+
GARBAGE_FILES = []
|
416
|
+
PRODUCT_FILES = []
|
417
|
+
|
418
|
+
## main
|
419
|
+
def main(argv=ARGV)
|
420
|
+
APP.run(*argv)
|
421
|
+
return 0
|
422
|
+
rescue OptionParser::ParseError, CLIApp::ActionError => exc
|
423
|
+
$stderr.puts "[ERROR] #{exc.message}"
|
424
|
+
return 1
|
425
|
+
end
|
426
|
+
|
427
|
+
if __FILE__ == $0
|
428
|
+
status_code = main(ARGV)
|
429
|
+
exit status_code
|
430
|
+
end
|
data/task/common-task.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
|
4
|
+
task :default => :help # or :test if you like
|
5
|
+
|
6
|
+
|
7
|
+
desc "list task names"
|
8
|
+
task :help do
|
9
|
+
system "rake -T"
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
desc "show how to release"
|
14
|
+
task :howto, [:version] do |t, args|
|
15
|
+
ver = args[:version] || ENV['version'] || "0.0.0"
|
16
|
+
zero_p = ver.end_with?('.0')
|
17
|
+
opt_b = zero_p ? " -b" : ""
|
18
|
+
puts <<"END"
|
19
|
+
How to release:
|
20
|
+
|
21
|
+
$ git diff # confirm that there is no changes
|
22
|
+
$ rake test
|
23
|
+
$ rake test:all # test on Ruby 2.x ~ 3.x
|
24
|
+
$ git checkout#{opt_b} rel-#{ver[0..-3]} # create or switch to release branch
|
25
|
+
$ vi CHANGES.md # if necessary
|
26
|
+
$ git add CHANGES.md # if necessary
|
27
|
+
$ git commit -m "Update 'CHANGES.md'" # if necessary
|
28
|
+
$ git log -1 # if necessary
|
29
|
+
$ cid=$(git log -1 | awk 'NR==1{print $2}') # if necessary
|
30
|
+
$ rake prepare[#{ver}] # update release number
|
31
|
+
$ git add -u . # add changes
|
32
|
+
$ git status -sb . # list files in staging area
|
33
|
+
$ git commit -m "Preparation for release #{ver}"
|
34
|
+
$ rake package # create a gem package
|
35
|
+
$ rake release[#{ver}] # upload to rubygems.org
|
36
|
+
$ git push -u origin
|
37
|
+
$ git tag | fgrep #{ver} # confirm release tag
|
38
|
+
$ git push --tags
|
39
|
+
$ git checkout - # back to main branch
|
40
|
+
$ git log -1 $cid # if necessary
|
41
|
+
$ git cherry-pick $cid # if necessary
|
42
|
+
|
43
|
+
END
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
desc "run test scripts"
|
48
|
+
task :test do
|
49
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), "lib")
|
50
|
+
sh "oktest test -sp"
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
desc "run test scripts on Ruby 2.x and 3.x"
|
55
|
+
task :'test:all' do
|
56
|
+
vs_home = ENV['VS_HOME'] or raise "$VS_HOME should be set."
|
57
|
+
defined?(RUBY_VERSIONS) or raise "RUBY_VERSIONS should be defined."
|
58
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), "lib")
|
59
|
+
RUBY_VERSIONS.each do |ver|
|
60
|
+
path_pat = "#{vs_home}/ruby/#{ver}.*/bin/ruby"
|
61
|
+
ruby_path = Dir.glob(path_pat).sort.last() or
|
62
|
+
raise "#{path_pat}: Not exist."
|
63
|
+
puts "\e[33m======== Ruby #{ver} ========\e[0m"
|
64
|
+
sh "#{ruby_path} -r oktest -e 'Oktest.main' -- test -sp" do end
|
65
|
+
puts ""
|
66
|
+
end
|
67
|
+
end
|