mini-cli 0.1.1 → 0.5.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 +4 -4
- data/gems.rb +1 -0
- data/lib/mini-cli.rb +185 -104
- data/lib/mini-cli/run.rb +34 -20
- data/lib/mini-cli/version.rb +1 -1
- data/lib/mini_cli.rb +1 -0
- data/mini-cli.gemspec +19 -19
- data/rakefile.rb +3 -5
- data/samples/custom_args.rb +15 -15
- data/samples/simple.rb +1 -3
- data/test/apps/noop.rb +6 -0
- data/test/apps/sequence.rb +26 -0
- data/test/helper.rb +8 -7
- data/test/mini-cli/exec_test.rb +94 -0
- data/test/mini-cli/main_test.rb +48 -33
- data/test/mini-cli/ruby_run_test.rb +43 -0
- data/test/mini-cli/run_test.rb +31 -26
- metadata +18 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b366ee44d4b95a00a977cbfdcf3e7cc7aff2cc1d3f72c03e0df2c2858aaf22e8
|
4
|
+
data.tar.gz: 8a622678016ca76e554a6b80d40dbea7dd7d6db5a494b5d7dd83944e68d66921
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73a9bee03f689d2f1e7334b6b927ba8cd45efb8422729c1abe93bf9062e26de47886ff0e70dc518ed01eeab7330e8777cd25a2c6fa270d6f16d32c0979de8d8d
|
7
|
+
data.tar.gz: cf6679d1245593b36ab18481ea4c83a14da4953cf41e6e849b92951a5dc5301c3603ef42476fe1cab0995d008b5356233f50dc2135f9f3d2530c5a61683560cd
|
data/gems.rb
CHANGED
data/lib/mini-cli.rb
CHANGED
@@ -1,162 +1,243 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module MiniCli
|
4
|
-
def self.included(
|
5
|
-
|
6
|
-
|
4
|
+
def self.included(mod)
|
5
|
+
mod.const_set(
|
6
|
+
:MiniCli__,
|
7
|
+
Instance.new(caller_locations(1, 1).first.absolute_path)
|
8
|
+
)
|
9
|
+
mod.private_constant(:MiniCli__)
|
7
10
|
end
|
8
11
|
|
9
12
|
def name(name = nil)
|
10
|
-
|
11
|
-
@__name = name || File.basename(MiniCli::SRC, '.*')
|
13
|
+
__minicli__.name(name)
|
12
14
|
end
|
13
15
|
|
14
16
|
def help(helptext, *args)
|
15
|
-
|
17
|
+
__minicli__.help(helptext, args)
|
16
18
|
end
|
17
19
|
|
18
20
|
def show_help
|
19
|
-
|
20
|
-
true
|
21
|
+
__minicli__.show_help
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
-
|
25
|
-
exit(code)
|
24
|
+
def show_errors?
|
25
|
+
__minicli__.show_errors
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
28
|
+
def show_errors=(value)
|
29
|
+
__minicli__.show_errors = value ? true : false
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_code
|
33
|
+
__minicli__.error_code
|
34
|
+
end
|
35
|
+
|
36
|
+
def error(code, message = nil)
|
37
|
+
$stderr.puts("#{name}: #{message}") if message && show_errors?
|
38
|
+
exit(__minicli__.error_code = code)
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_argv(argv = nil, &argv_converter)
|
42
|
+
return __minicli__.converter = argv_converter if argv_converter
|
43
|
+
argv = Array.new(argv || ARGV)
|
31
44
|
exit(show_help) if argv.index('--help') || argv.index('-h')
|
32
|
-
|
33
|
-
defined?(@__argv_converter) ? @__argv_converter.call(args) || args : args
|
45
|
+
__minicli__.main(argv, method(:error).to_proc)
|
34
46
|
end
|
35
47
|
|
36
|
-
def main
|
37
|
-
|
38
|
-
yield(
|
48
|
+
def main
|
49
|
+
__minicli__.run do
|
50
|
+
yield(parse_argv)
|
39
51
|
rescue Interrupt
|
40
52
|
error(130, 'aborted')
|
41
53
|
end
|
42
54
|
end
|
43
55
|
|
56
|
+
def before(&callback)
|
57
|
+
callback and __minicli__.before << callback
|
58
|
+
end
|
59
|
+
|
60
|
+
def after(&callback)
|
61
|
+
callback and __minicli__.after << callback
|
62
|
+
end
|
63
|
+
|
44
64
|
private
|
45
65
|
|
46
|
-
def
|
47
|
-
|
66
|
+
def __minicli__
|
67
|
+
self.class.const_get(:MiniCli__)
|
48
68
|
end
|
49
69
|
|
50
|
-
class
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
70
|
+
class Instance
|
71
|
+
attr_reader :source
|
72
|
+
attr_writer :converter
|
73
|
+
attr_accessor :show_errors, :before, :after, :error_code
|
74
|
+
|
75
|
+
def initialize(source)
|
76
|
+
@source = source
|
77
|
+
@name = File.basename(source, '.*')
|
78
|
+
@parser = @converter = @main_proc = nil
|
79
|
+
@before, @after = [], []
|
80
|
+
@show_errors = true
|
81
|
+
@before_ok = false
|
82
|
+
@error_code = 0
|
83
|
+
init_main
|
55
84
|
end
|
56
85
|
|
57
|
-
def
|
58
|
-
|
59
|
-
print("Usage: #{name}")
|
60
|
-
print(' [OPTIONS]') unless @options.empty?
|
61
|
-
print(' ', @args.join(' ')) unless @args.empty?
|
62
|
-
puts
|
63
|
-
puts(nil, 'Valid Options:') unless @options.empty?
|
64
|
-
puts(@helptext) unless @helptext.empty?
|
86
|
+
def name(name = nil)
|
87
|
+
name ? @name = name.to_s : @name
|
65
88
|
end
|
66
89
|
|
67
|
-
def
|
68
|
-
@
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
86
|
-
process(arguments)
|
90
|
+
def help(helptext, args)
|
91
|
+
@parser = ArgvParser.new(helptext, args)
|
92
|
+
end
|
93
|
+
|
94
|
+
def show_help
|
95
|
+
parser.show_help(@name)
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def run(&main_proc)
|
100
|
+
@main_proc = main_proc
|
101
|
+
end
|
102
|
+
|
103
|
+
def main(argv, error)
|
104
|
+
@before.each(&:call)
|
105
|
+
@before_ok = true
|
106
|
+
args = parser.parse(argv, error)
|
107
|
+
@converter ? @converter.call(args) || args : args
|
87
108
|
end
|
88
109
|
|
89
110
|
private
|
90
111
|
|
91
|
-
def
|
92
|
-
|
112
|
+
def init_main
|
113
|
+
at_exit do
|
114
|
+
next if $! and not ($!.kind_of?(SystemExit) and $!.success?)
|
115
|
+
shutdown unless @after.empty?
|
116
|
+
@main_proc&.call
|
117
|
+
end
|
93
118
|
end
|
94
119
|
|
95
|
-
def
|
96
|
-
|
97
|
-
next if
|
98
|
-
|
120
|
+
def shutdown(pid = Process.pid)
|
121
|
+
at_exit do
|
122
|
+
next if Process.pid != pid
|
123
|
+
@after.reverse_each(&:call) if @before_ok
|
124
|
+
exit(@error_code)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def parser
|
129
|
+
@parser ||= ArgvParser.new(nil, [])
|
130
|
+
end
|
131
|
+
|
132
|
+
class ArgvParser
|
133
|
+
def initialize(helptext, args)
|
134
|
+
@helptext = helptext.to_s
|
135
|
+
@args = args.flatten.map!(&:to_s).uniq
|
136
|
+
@options = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
def show_help(name)
|
140
|
+
parse_help! unless @options
|
141
|
+
print("Usage: #{name}")
|
142
|
+
print(' [OPTIONS]') unless @options.empty?
|
143
|
+
print(' ', @args.join(' ')) unless @args.empty?
|
144
|
+
puts
|
145
|
+
puts(nil, 'Options:') unless @options.empty?
|
146
|
+
puts(@helptext) unless @helptext.empty?
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse(argv, error)
|
150
|
+
@error, @result = error, {}
|
151
|
+
parse_help! unless @options
|
152
|
+
process(parse_argv(argv))
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def parse_argv(argv)
|
158
|
+
arguments = []
|
159
|
+
while (arg = argv.shift)
|
160
|
+
case arg
|
161
|
+
when '--'
|
162
|
+
break arguments += argv
|
163
|
+
when /\A--([[[:alnum:]]-]+)\z/
|
164
|
+
handle_option(Regexp.last_match[1], argv)
|
165
|
+
when /\A-([[:alnum:]]+)\z/
|
166
|
+
parse_options(Regexp.last_match[1], argv)
|
167
|
+
else
|
168
|
+
arguments << arg
|
169
|
+
end
|
170
|
+
end
|
171
|
+
arguments
|
172
|
+
end
|
173
|
+
|
174
|
+
def error(msg)
|
175
|
+
@error[1, msg]
|
176
|
+
end
|
177
|
+
|
178
|
+
def process(arguments)
|
179
|
+
@args.each do |arg|
|
180
|
+
process_arg(arg, arguments.shift) unless arg.index('..')
|
181
|
+
end
|
182
|
+
arguments.unshift(@result['FILES']) if @result.key?('FILES')
|
183
|
+
@result['FILES'] = arguments
|
184
|
+
@result
|
185
|
+
end
|
186
|
+
|
187
|
+
def process_arg(arg, value)
|
99
188
|
if arg.start_with?('[')
|
100
189
|
@result[arg[1..-2]] = value if value
|
101
190
|
else
|
102
191
|
@result[arg] = value || error("parameter expected - #{arg}")
|
103
192
|
end
|
104
193
|
end
|
105
|
-
arguments.unshift(@result['FILES']) if @result.key?('FILES')
|
106
|
-
@result['FILES'] = arguments
|
107
|
-
@result
|
108
|
-
end
|
109
194
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
@result[key] = value = argv.shift
|
114
|
-
return unless value.nil? || value.start_with?('-')
|
115
|
-
error("parameter #{key} expected - --#{option}")
|
116
|
-
end
|
117
|
-
|
118
|
-
def parse_options(options, argv)
|
119
|
-
options.each_char do |opt|
|
120
|
-
key = @options[opt] || error("unknown option - #{opt}")
|
121
|
-
next @result[key] = true if key == key.downcase
|
195
|
+
def handle_option(option, argv, test = ->(k) { option == k })
|
196
|
+
key = @options[option] || error("unknown option - #{option}")
|
197
|
+
@result[key] = test[key] and return
|
122
198
|
@result[key] = value = argv.shift
|
123
|
-
|
124
|
-
error("parameter #{key} expected -
|
199
|
+
return unless value.nil? || value.start_with?('-')
|
200
|
+
error("parameter #{key} expected - --#{option}")
|
125
201
|
end
|
126
|
-
end
|
127
202
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
203
|
+
def parse_options(options, argv)
|
204
|
+
test = ->(k) { k == k.downcase }
|
205
|
+
options.each_char { |opt| handle_option(opt, argv, test) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def parse_help!
|
209
|
+
@options = {}
|
210
|
+
@helptext.each_line do |line|
|
211
|
+
case line
|
212
|
+
when /-([[:alnum:]]), --([[[:alnum:]]-]+) ([[:upper:]]+)\s+\S+/
|
213
|
+
option_with_argument(Regexp.last_match)
|
214
|
+
when /--([[[:alnum:]]-]+) ([[:upper:]]+)\s+\S+/
|
215
|
+
short_option_with_argument(Regexp.last_match)
|
216
|
+
when /-([[:alnum:]]), --([[[:alnum:]]-]+)\s+\S+/
|
217
|
+
option(Regexp.last_match)
|
218
|
+
when /--([[[:alnum:]]-]+)\s+\S+/
|
219
|
+
short_option(Regexp.last_match)
|
220
|
+
end
|
140
221
|
end
|
141
222
|
end
|
142
|
-
end
|
143
223
|
|
144
|
-
|
145
|
-
|
146
|
-
|
224
|
+
def option_with_argument(match)
|
225
|
+
@options[match[1]] = @options[match[2]] = match[3]
|
226
|
+
end
|
147
227
|
|
148
|
-
|
149
|
-
|
150
|
-
|
228
|
+
def short_option_with_argument(match)
|
229
|
+
@options[match[1]] = match[2]
|
230
|
+
end
|
151
231
|
|
152
|
-
|
153
|
-
|
154
|
-
|
232
|
+
def option(match)
|
233
|
+
@options[match[1]] = @options[match[2]] = match[2]
|
234
|
+
end
|
155
235
|
|
156
|
-
|
157
|
-
|
236
|
+
def short_option(match)
|
237
|
+
@options[match[1]] = match[1]
|
238
|
+
end
|
158
239
|
end
|
159
240
|
end
|
160
241
|
|
161
|
-
private_constant(:
|
242
|
+
private_constant(:Instance)
|
162
243
|
end
|
data/lib/mini-cli/run.rb
CHANGED
@@ -1,32 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'open3'
|
4
|
-
|
5
3
|
module MiniCli
|
6
|
-
def run(*cmd)
|
7
|
-
|
8
|
-
opts
|
9
|
-
|
4
|
+
def run(*cmd, stdin_data: nil, status: false, chdir: nil)
|
5
|
+
in_read, in_write = IO.pipe
|
6
|
+
opts = { err: %i[child out], in: in_read }
|
7
|
+
opts[:chdir] = chdir if chdir
|
8
|
+
ret = IO.popen(*cmd, opts, &__run_proc(stdin_data, in_write))
|
9
|
+
status ? [Process.last_status, ret] : ret
|
10
|
+
rescue Errno::ENOENT
|
10
11
|
nil
|
12
|
+
ensure
|
13
|
+
in_read&.close
|
14
|
+
in_write&.close
|
11
15
|
end
|
12
16
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
result = Open3.capture3(*cmd)
|
17
|
-
result.shift unless opts.first == :stdout
|
18
|
-
result.pop unless opts.last == :status
|
19
|
-
result.size == 1 ? result.first : result
|
17
|
+
def run_ruby(*cmd, **run_options)
|
18
|
+
require 'shellwords'
|
19
|
+
run(Shellwords.join(RUBY_CMD + cmd), **run_options)
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
|
24
|
-
return result.last.success? if opts.empty?
|
25
|
-
return result if opts.size == 2
|
26
|
-
opts.first == :status ? result.last : result.first
|
22
|
+
def run_script(script, status: false, chdir: nil)
|
23
|
+
run_ruby(stdin_data: script, status: status, chdir: chdir)
|
27
24
|
end
|
28
25
|
|
29
|
-
|
30
|
-
|
26
|
+
private
|
27
|
+
|
28
|
+
RUBY_CMD =
|
29
|
+
%w[--disable gems --disable did_you_mean --disable rubyopt].unshift(
|
30
|
+
RbConfig.ruby
|
31
|
+
).freeze
|
32
|
+
|
33
|
+
def __run_proc(stdin_data, in_write)
|
34
|
+
return :read unless stdin_data
|
35
|
+
proc do |out|
|
36
|
+
in_write.sync = true
|
37
|
+
if stdin_data.respond_to?(:readpartial)
|
38
|
+
IO.copy_stream(stdin_data, in_write)
|
39
|
+
else
|
40
|
+
in_write.write(stdin_data)
|
41
|
+
end
|
42
|
+
in_write.close
|
43
|
+
out.read
|
44
|
+
end
|
31
45
|
end
|
32
46
|
end
|
data/lib/mini-cli/version.rb
CHANGED
data/lib/mini_cli.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'mini-cli'
|
data/mini-cli.gemspec
CHANGED
@@ -2,30 +2,30 @@
|
|
2
2
|
|
3
3
|
require_relative './lib/mini-cli/version'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'mini-cli'
|
7
|
+
spec.version = MiniCli::VERSION
|
8
|
+
spec.required_ruby_version = '>= 2.5.0'
|
9
|
+
|
10
|
+
spec.summary = 'The lean CLI framework for Ruby'
|
11
|
+
spec.description = <<~DESC
|
10
12
|
This gem is a lean, easy to use CLI framework with a very small footprint.
|
11
13
|
It provides an easy to use argument parsing, help displaying and
|
12
14
|
minimalistic error handling.
|
13
15
|
DESC
|
14
|
-
gem.author = 'Mike Blumtritt'
|
15
|
-
gem.homepage = 'https://github.com/mblumtritt/mini-cli'
|
16
|
-
gem.metadata = {
|
17
|
-
'source_code_uri' => 'https://github.com/mblumtritt/mini-cli',
|
18
|
-
'bug_tracker_uri' => 'https://github.com/mblumtritt/mini-cli/issues'
|
19
|
-
}
|
20
16
|
|
21
|
-
|
17
|
+
spec.author = 'Mike Blumtritt'
|
18
|
+
spec.homepage = 'https://github.com/mblumtritt/mini-cli'
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/mblumtritt/mini-cli'
|
20
|
+
spec.metadata['bug_tracker_uri'] =
|
21
|
+
'https://github.com/mblumtritt/mini-cli/issues'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
spec.add_development_dependency 'bundler'
|
24
|
+
spec.add_development_dependency 'minitest'
|
25
|
+
spec.add_development_dependency 'rake'
|
26
26
|
|
27
|
-
all_files =
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
|
28
|
+
spec.test_files = all_files.grep(%r{^(spec|test)/})
|
29
|
+
spec.files = all_files - spec.test_files
|
30
|
+
spec.extra_rdoc_files = %w[README.md]
|
31
31
|
end
|
data/rakefile.rb
CHANGED
@@ -3,13 +3,11 @@
|
|
3
3
|
require 'rake/testtask'
|
4
4
|
require 'bundler/gem_tasks'
|
5
5
|
|
6
|
-
|
6
|
+
$stdout.sync = $stderr.sync = true
|
7
7
|
|
8
|
-
|
8
|
+
task(:default) { exec('rake --tasks') }
|
9
9
|
|
10
|
-
|
11
|
-
exec "#{$PROGRAM_NAME} --tasks"
|
12
|
-
end
|
10
|
+
CLOBBER << 'prj'
|
13
11
|
|
14
12
|
Rake::TestTask.new(:test) do |t|
|
15
13
|
t.ruby_opts = %w[-w]
|
data/samples/custom_args.rb
CHANGED
@@ -11,21 +11,21 @@ help <<~HELP, %w[TARGET [SOURCE]]
|
|
11
11
|
--opt option without any argument
|
12
12
|
HELP
|
13
13
|
|
14
|
-
main
|
15
|
-
cfg.each_pair{ |key, value| puts("#{key}: #{value}") }
|
16
|
-
end
|
14
|
+
main { |cfg| cfg.each_pair { |key, value| puts("#{key}: #{value}") } }
|
17
15
|
|
18
16
|
parse_argv do |args|
|
19
|
-
Struct
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
17
|
+
Struct
|
18
|
+
.new(:target, :sources, :name, :url, :switch, :opt)
|
19
|
+
.new
|
20
|
+
.tap do |cfg|
|
21
|
+
cfg.target = args['TARGET']
|
22
|
+
cfg.sources = args['FILES'] # args['FILES'] is an array containing all surplus arguments
|
23
|
+
source = args['SOURCE'] || ENV['SOURCE']
|
24
|
+
cfg.sources.unshift(source) if source
|
25
|
+
cfg.sources << 'STDIN' if cfg.sources.empty?
|
26
|
+
cfg.name = args['NAME'] || 'default_name'
|
27
|
+
cfg.url = args['URL'] || 'www.url.test'
|
28
|
+
cfg.switch = args.key?('switch')
|
29
|
+
cfg.opt = args.key?('opt')
|
30
|
+
end
|
31
31
|
end
|
data/samples/simple.rb
CHANGED
data/test/apps/noop.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# demonstrates the call sequence
|
3
|
+
|
4
|
+
require_relative '../../lib/mini-cli'
|
5
|
+
include(MiniCli)
|
6
|
+
|
7
|
+
help <<~HELP
|
8
|
+
-x, --exit early exit
|
9
|
+
-e, --error exit with error
|
10
|
+
HELP
|
11
|
+
|
12
|
+
main do |*args|
|
13
|
+
puts('main' + args.inspect)
|
14
|
+
exit if args.first['exit']
|
15
|
+
error(42, '!error!') if args.first['error']
|
16
|
+
end
|
17
|
+
|
18
|
+
before { |*args| puts('before_1' + args.inspect) }
|
19
|
+
after { |*args| puts('after_1' + args.inspect) }
|
20
|
+
before { |*args| puts('before_2' + args.inspect) }
|
21
|
+
after { |*args| puts('after_2' + args.inspect) }
|
22
|
+
|
23
|
+
parse_argv do |*args|
|
24
|
+
puts('parse_argv' + args.inspect)
|
25
|
+
args.first
|
26
|
+
end
|
data/test/helper.rb
CHANGED
@@ -11,14 +11,15 @@ class Test < Minitest::Test
|
|
11
11
|
attr_reader :subject
|
12
12
|
|
13
13
|
def setup
|
14
|
-
@subject =
|
15
|
-
|
16
|
-
|
14
|
+
@subject =
|
15
|
+
Class.new do
|
16
|
+
include MiniCli
|
17
|
+
attr_reader :exit_args
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def exit(*args)
|
20
|
+
@exit_args = args
|
21
|
+
end
|
22
|
+
end.new
|
22
23
|
end
|
23
24
|
|
24
25
|
def assert_stop_with_error(code, text, &block)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'shellwords'
|
5
|
+
require_relative '../helper'
|
6
|
+
|
7
|
+
class ExecTest < Minitest::Test
|
8
|
+
def test_noop
|
9
|
+
assert_empty(assert_no_err('noop.rb'))
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_noop_help
|
13
|
+
assert_equal("Usage: noop\n", assert_no_err('noop.rb', '--help'))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_sequence
|
17
|
+
expected = [
|
18
|
+
'before_1[]',
|
19
|
+
'before_2[]',
|
20
|
+
"parse_argv[{\"FILES\"=>[]}]",
|
21
|
+
"main[{\"FILES\"=>[]}]",
|
22
|
+
'after_2[]',
|
23
|
+
'after_1[]'
|
24
|
+
]
|
25
|
+
assert_equal(expected, assert_no_err('sequence.rb').split("\n"))
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_sequence_help
|
29
|
+
expected = [
|
30
|
+
'Usage: sequence [OPTIONS]',
|
31
|
+
'',
|
32
|
+
'Options:',
|
33
|
+
'-x, --exit early exit',
|
34
|
+
'-e, --error exit with error'
|
35
|
+
]
|
36
|
+
assert_equal(expected, assert_no_err('sequence.rb', '--help').split("\n"))
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_sequence_early_exit
|
40
|
+
expected = [
|
41
|
+
'before_1[]',
|
42
|
+
'before_2[]',
|
43
|
+
"parse_argv[{\"exit\"=>true, \"FILES\"=>[]}]",
|
44
|
+
"main[{\"exit\"=>true, \"FILES\"=>[]}]",
|
45
|
+
'after_2[]',
|
46
|
+
'after_1[]'
|
47
|
+
]
|
48
|
+
assert_equal(expected, assert_no_err('sequence.rb', '-x').split("\n"))
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_sequence_error
|
52
|
+
expected = [
|
53
|
+
'before_1[]',
|
54
|
+
'before_2[]',
|
55
|
+
"parse_argv[{\"error\"=>true, \"FILES\"=>[]}]",
|
56
|
+
"main[{\"error\"=>true, \"FILES\"=>[]}]",
|
57
|
+
'after_2[]',
|
58
|
+
'after_1[]'
|
59
|
+
]
|
60
|
+
std, err, status = invoke('sequence.rb', '-e')
|
61
|
+
assert_same(42, status.exitstatus)
|
62
|
+
assert_equal("sequence: !error!\n", err)
|
63
|
+
assert_equal(expected, std.split("\n"))
|
64
|
+
end
|
65
|
+
|
66
|
+
def assert_no_err(...)
|
67
|
+
std, err = assert_success(...)
|
68
|
+
assert_empty(err)
|
69
|
+
std
|
70
|
+
end
|
71
|
+
|
72
|
+
def assert_success(...)
|
73
|
+
std, err, status = invoke(...)
|
74
|
+
assert(status.success?)
|
75
|
+
return std, err
|
76
|
+
end
|
77
|
+
|
78
|
+
def invoke(name, *args)
|
79
|
+
Open3.capture3(
|
80
|
+
Shellwords.join(
|
81
|
+
[
|
82
|
+
RbConfig.ruby,
|
83
|
+
'--disable',
|
84
|
+
'gems',
|
85
|
+
'--disable',
|
86
|
+
'did_you_mean',
|
87
|
+
'--disable',
|
88
|
+
'rubyopt',
|
89
|
+
File.expand_path("../apps/#{name}", __dir__)
|
90
|
+
] + args
|
91
|
+
)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
data/test/mini-cli/main_test.rb
CHANGED
@@ -1,24 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../helper'
|
2
4
|
|
3
5
|
class MainTest < Test
|
4
6
|
def test_defaults
|
5
7
|
assert_equal('helper', subject.name)
|
6
|
-
|
8
|
+
assert(subject.show_errors?)
|
9
|
+
assert_output("Usage: helper\n") { subject.show_help }
|
7
10
|
end
|
8
11
|
|
9
12
|
def test_methods
|
10
|
-
subject = Class.new{ include MiniCli }.new
|
13
|
+
subject = Class.new { include MiniCli }.new
|
11
14
|
|
12
15
|
expected_methods = %i[
|
16
|
+
after
|
17
|
+
before
|
13
18
|
error
|
19
|
+
error_code
|
14
20
|
help
|
15
21
|
main
|
16
22
|
name
|
17
23
|
parse_argv
|
18
24
|
run
|
25
|
+
run_ruby
|
26
|
+
run_script
|
27
|
+
show_errors=
|
28
|
+
show_errors?
|
19
29
|
show_help
|
20
|
-
]
|
21
|
-
methods = (subject.methods - Object.
|
30
|
+
]
|
31
|
+
methods = (subject.methods - Object.instance_methods).sort!
|
22
32
|
assert_equal(expected_methods, methods)
|
23
33
|
end
|
24
34
|
|
@@ -27,9 +37,14 @@ class MainTest < Test
|
|
27
37
|
subject.error(42, 'some error text')
|
28
38
|
end
|
29
39
|
|
30
|
-
assert_stop_with_error(21, 'error')
|
31
|
-
|
32
|
-
|
40
|
+
assert_stop_with_error(21, 'error') { subject.error(21, :error) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_no_error
|
44
|
+
subject.show_errors = false
|
45
|
+
|
46
|
+
assert_output(nil, nil) { subject.error(666, 'some error text') }
|
47
|
+
assert_equal([666], subject.exit_args)
|
33
48
|
end
|
34
49
|
|
35
50
|
def test_help_simple
|
@@ -39,7 +54,7 @@ class MainTest < Test
|
|
39
54
|
Some helptext
|
40
55
|
EXPECTED
|
41
56
|
|
42
|
-
assert_output(expected_text){ subject.show_help }
|
57
|
+
assert_output(expected_text) { subject.show_help }
|
43
58
|
end
|
44
59
|
|
45
60
|
def test_help_with_args
|
@@ -54,7 +69,7 @@ class MainTest < Test
|
|
54
69
|
here
|
55
70
|
EXPECTED
|
56
71
|
|
57
|
-
assert_output(expected_text){ subject.show_help }
|
72
|
+
assert_output(expected_text) { subject.show_help }
|
58
73
|
end
|
59
74
|
|
60
75
|
def test_argument_required
|
@@ -64,10 +79,10 @@ class MainTest < Test
|
|
64
79
|
subject.parse_argv(as_argv(''))
|
65
80
|
end
|
66
81
|
|
67
|
-
expected = {'ARG' => 'arg', 'FILES' => []}
|
82
|
+
expected = { 'ARG' => 'arg', 'FILES' => [] }
|
68
83
|
assert_equal(expected, subject.parse_argv(as_argv('arg')))
|
69
84
|
|
70
|
-
expected = {'ARG' => 'arg1', 'FILES' => %w[arg2 arg3]}
|
85
|
+
expected = { 'ARG' => 'arg1', 'FILES' => %w[arg2 arg3] }
|
71
86
|
assert_equal(expected, subject.parse_argv(as_argv('arg1 arg2 arg3')))
|
72
87
|
end
|
73
88
|
|
@@ -83,10 +98,10 @@ class MainTest < Test
|
|
83
98
|
subject.parse_argv(as_argv(''))
|
84
99
|
end
|
85
100
|
|
86
|
-
expected = {'ARG1' => 'arg1', 'ARG2' => 'arg2', 'FILES' => []}
|
101
|
+
expected = { 'ARG1' => 'arg1', 'ARG2' => 'arg2', 'FILES' => [] }
|
87
102
|
assert_equal(expected, subject.parse_argv(as_argv('arg1 arg2')))
|
88
103
|
|
89
|
-
expected = {'ARG1' => 'arg', 'FILES' => []}
|
104
|
+
expected = { 'ARG1' => 'arg', 'FILES' => [] }
|
90
105
|
assert_equal(expected, subject.parse_argv(as_argv('arg')))
|
91
106
|
end
|
92
107
|
|
@@ -100,21 +115,21 @@ class MainTest < Test
|
|
100
115
|
Some additional explaination can be here
|
101
116
|
HELP
|
102
117
|
|
103
|
-
expected = {'FILES' => []}
|
118
|
+
expected = { 'FILES' => [] }
|
104
119
|
assert_equal(expected, subject.parse_argv(as_argv('')))
|
105
120
|
|
106
|
-
expected = {'NAME' => 'name', 'FILES' => []}
|
121
|
+
expected = { 'NAME' => 'name', 'FILES' => [] }
|
107
122
|
assert_equal(expected, subject.parse_argv(as_argv('--named name')))
|
108
123
|
assert_equal(expected, subject.parse_argv(as_argv('-n name')))
|
109
124
|
|
110
|
-
expected = {'LNAME' => 'long', 'FILES' => []}
|
125
|
+
expected = { 'LNAME' => 'long', 'FILES' => [] }
|
111
126
|
assert_equal(expected, subject.parse_argv(as_argv('--named-long long')))
|
112
127
|
|
113
|
-
expected = {'unnamed' => true, 'FILES' => []}
|
128
|
+
expected = { 'unnamed' => true, 'FILES' => [] }
|
114
129
|
assert_equal(expected, subject.parse_argv(as_argv('--unnamed')))
|
115
130
|
assert_equal(expected, subject.parse_argv(as_argv('-u')))
|
116
131
|
|
117
|
-
expected = {'un-named' => true, 'FILES' => []}
|
132
|
+
expected = { 'un-named' => true, 'FILES' => [] }
|
118
133
|
assert_equal(expected, subject.parse_argv(as_argv('--un-named')))
|
119
134
|
|
120
135
|
expected = {
|
@@ -125,23 +140,14 @@ class MainTest < Test
|
|
125
140
|
'FILES' => %w[FILE1 FILE2]
|
126
141
|
}
|
127
142
|
|
128
|
-
result =
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
--un-named
|
133
|
-
FILE1
|
134
|
-
FILE2
|
135
|
-
])
|
143
|
+
result =
|
144
|
+
subject.parse_argv(
|
145
|
+
%w[--named name --named-long long --unnamed --un-named FILE1 FILE2]
|
146
|
+
)
|
136
147
|
assert_equal(expected, result)
|
137
148
|
|
138
|
-
result =
|
139
|
-
-nu name
|
140
|
-
--named-long long
|
141
|
-
--un-named
|
142
|
-
FILE1
|
143
|
-
FILE2
|
144
|
-
])
|
149
|
+
result =
|
150
|
+
subject.parse_argv(%w[-nu name --named-long long --un-named FILE1 FILE2])
|
145
151
|
assert_equal(expected, result)
|
146
152
|
end
|
147
153
|
|
@@ -175,4 +181,13 @@ class MainTest < Test
|
|
175
181
|
result = subject.parse_argv(as_argv('-nup name port in out opt file'))
|
176
182
|
assert_equal(expected, result)
|
177
183
|
end
|
184
|
+
|
185
|
+
def test_run
|
186
|
+
call_sequence = []
|
187
|
+
subject.main { call_sequence << :main }
|
188
|
+
subject.before { call_sequence << :start_1 }
|
189
|
+
subject.after { call_sequence << :end_1 }
|
190
|
+
subject.before { call_sequence << :start_2 }
|
191
|
+
subject.after { call_sequence << :end_2 }
|
192
|
+
end
|
178
193
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../helper'
|
4
|
+
|
5
|
+
class RubyRunTest < Test
|
6
|
+
def test_simple
|
7
|
+
result = subject.run_script('puts("Hello World")')
|
8
|
+
assert_equal("Hello World\n", result)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_error
|
12
|
+
result = subject.run_script('UNDEFINED')
|
13
|
+
assert_match(/NameError/, result)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_chdir
|
17
|
+
home = Dir.home
|
18
|
+
refute(home == Dir.pwd)
|
19
|
+
result = subject.run_script('print Dir.pwd', chdir: home)
|
20
|
+
assert_equal(home, result)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_status
|
24
|
+
status, result = subject.run_script('print :Ok', status: true)
|
25
|
+
assert_instance_of(Process::Status, status)
|
26
|
+
assert(status.success?)
|
27
|
+
assert_equal('Ok', result)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_status_error
|
31
|
+
status, result = subject.run_script('print :Err;exit 42', status: true)
|
32
|
+
assert_instance_of(Process::Status, status)
|
33
|
+
refute(status.success?)
|
34
|
+
assert_same(42, status.exitstatus)
|
35
|
+
assert_equal('Err', result)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_stream
|
39
|
+
stream = StringIO.new('puts "Hello World"')
|
40
|
+
result = subject.run_script(stream)
|
41
|
+
assert_equal("Hello World\n", result)
|
42
|
+
end
|
43
|
+
end
|
data/test/mini-cli/run_test.rb
CHANGED
@@ -1,44 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../helper'
|
2
4
|
|
3
5
|
class RunTest < Test
|
4
|
-
def
|
5
|
-
|
6
|
-
|
6
|
+
def test_simple
|
7
|
+
result = subject.run('pwd')
|
8
|
+
assert_equal("#{Dir.pwd}\n", result)
|
7
9
|
end
|
8
10
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
11
|
+
def test_simple_error
|
12
|
+
result = subject.run('ls /no-valid-dir')
|
13
|
+
assert_match(/No such file or directory/, result)
|
14
|
+
end
|
12
15
|
|
13
|
-
|
14
|
-
|
16
|
+
def test_chdir
|
17
|
+
home = Dir.home
|
18
|
+
refute(home == Dir.pwd)
|
19
|
+
result = subject.run('pwd', chdir: home)
|
20
|
+
assert_equal("#{home}\n", result)
|
15
21
|
end
|
16
22
|
|
17
23
|
def test_status
|
18
|
-
result = subject.run('pwd',
|
19
|
-
assert_instance_of(TrueClass, result)
|
20
|
-
|
21
|
-
status = subject.run('pwd', status: true)
|
22
|
-
assert_instance_of(Process::Status, status)
|
23
|
-
|
24
|
-
out, status = subject.run('pwd', stdout: true, status: true)
|
25
|
-
assert_equal(@pwd, out)
|
24
|
+
status, result = subject.run('pwd', status: true)
|
26
25
|
assert_instance_of(Process::Status, status)
|
27
26
|
assert(status.success?)
|
27
|
+
assert_equal("#{Dir.pwd}\n", result)
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
31
|
-
|
32
|
-
assert_match(/No such file or directory/, err)
|
33
|
-
|
34
|
-
out, err = subject.run('ls /no-valid-dir', stdout: true, stderr: true)
|
35
|
-
assert_empty(out)
|
36
|
-
assert_match(/No such file or directory/, err)
|
37
|
-
|
38
|
-
err, status = subject.run('ls /no-valid-dir', stderr: true, status: true)
|
39
|
-
assert_match(/No such file or directory/, err)
|
30
|
+
def test_status_error
|
31
|
+
status, result = subject.run('ls /no-valid-dir', status: true)
|
40
32
|
assert_instance_of(Process::Status, status)
|
41
33
|
refute(status.success?)
|
34
|
+
assert_match(/No such file or directory/, result)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_stdin
|
38
|
+
string = 'Hello World'
|
39
|
+
result = subject.run('cat', stdin_data: string)
|
40
|
+
assert_equal(string, result)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_stdin_stream
|
44
|
+
stream = StringIO.new('Hello World')
|
45
|
+
result = subject.run('cat', stdin_data: stream)
|
46
|
+
assert_equal(stream.string, result)
|
42
47
|
end
|
43
48
|
|
44
49
|
def test_failure
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mini-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -56,10 +56,11 @@ description: |
|
|
56
56
|
This gem is a lean, easy to use CLI framework with a very small footprint.
|
57
57
|
It provides an easy to use argument parsing, help displaying and
|
58
58
|
minimalistic error handling.
|
59
|
-
email:
|
59
|
+
email:
|
60
60
|
executables: []
|
61
61
|
extensions: []
|
62
|
-
extra_rdoc_files:
|
62
|
+
extra_rdoc_files:
|
63
|
+
- README.md
|
63
64
|
files:
|
64
65
|
- ".gitignore"
|
65
66
|
- README.md
|
@@ -67,20 +68,25 @@ files:
|
|
67
68
|
- lib/mini-cli.rb
|
68
69
|
- lib/mini-cli/run.rb
|
69
70
|
- lib/mini-cli/version.rb
|
71
|
+
- lib/mini_cli.rb
|
70
72
|
- mini-cli.gemspec
|
71
73
|
- rakefile.rb
|
72
74
|
- samples/custom_args.rb
|
73
75
|
- samples/demo.rb
|
74
76
|
- samples/simple.rb
|
77
|
+
- test/apps/noop.rb
|
78
|
+
- test/apps/sequence.rb
|
75
79
|
- test/helper.rb
|
80
|
+
- test/mini-cli/exec_test.rb
|
76
81
|
- test/mini-cli/main_test.rb
|
82
|
+
- test/mini-cli/ruby_run_test.rb
|
77
83
|
- test/mini-cli/run_test.rb
|
78
84
|
homepage: https://github.com/mblumtritt/mini-cli
|
79
85
|
licenses: []
|
80
86
|
metadata:
|
81
87
|
source_code_uri: https://github.com/mblumtritt/mini-cli
|
82
88
|
bug_tracker_uri: https://github.com/mblumtritt/mini-cli/issues
|
83
|
-
post_install_message:
|
89
|
+
post_install_message:
|
84
90
|
rdoc_options: []
|
85
91
|
require_paths:
|
86
92
|
- lib
|
@@ -95,11 +101,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
101
|
- !ruby/object:Gem::Version
|
96
102
|
version: '0'
|
97
103
|
requirements: []
|
98
|
-
rubygems_version: 3.
|
99
|
-
signing_key:
|
104
|
+
rubygems_version: 3.2.15
|
105
|
+
signing_key:
|
100
106
|
specification_version: 4
|
101
107
|
summary: The lean CLI framework for Ruby
|
102
108
|
test_files:
|
109
|
+
- test/apps/noop.rb
|
110
|
+
- test/apps/sequence.rb
|
103
111
|
- test/helper.rb
|
112
|
+
- test/mini-cli/exec_test.rb
|
104
113
|
- test/mini-cli/main_test.rb
|
114
|
+
- test/mini-cli/ruby_run_test.rb
|
105
115
|
- test/mini-cli/run_test.rb
|