cliapp 0.1.0
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 +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
|