mini-cli 0.1.0 → 0.4.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/README.md +1 -1
- data/gems.rb +1 -0
- data/lib/mini-cli.rb +185 -104
- data/lib/mini-cli/run.rb +22 -22
- data/lib/mini-cli/version.rb +1 -1
- data/lib/mini_cli.rb +1 -0
- data/mini-cli.gemspec +21 -21
- data/rakefile.rb +3 -5
- data/samples/custom_args.rb +15 -15
- data/samples/simple.rb +1 -3
- data/test/apps/noop +8 -0
- data/test/apps/sequence +28 -0
- data/test/helper.rb +8 -7
- data/test/mini-cli/exec_test.rb +96 -0
- data/test/mini-cli/main_test.rb +46 -33
- data/test/mini-cli/run_test.rb +31 -26
- metadata +19 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 969a4421172e3cf6a3b6c6ecc294932e825a170de914ac5f19926fcc4e3da674
|
4
|
+
data.tar.gz: '039fc6a1d177834a3999468142a71a985ef417d179011cbba6b7b57777cb05c8'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8fc8a932a4e508327570bb3c4e062950d78b19cbaf87594456e2f5d340a22bcbe2db32bbffd55c76e7e57e2fdb1ca63118d2f20a67976bb60fb3795e9cf372a
|
7
|
+
data.tar.gz: 3a2d8fa7af6ec17ae701ab0a6505ce03084646540c3cab5a54facb76c5434ad5b29acdbcb9aea35d19e7daa60bdabdb79cf143a8f8a22a2d6af0b6c7d4aeb98e
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Mini Cli
|
2
2
|
|
3
|
-
This gem is a
|
3
|
+
This gem is a lean, easy to use CLI framework with a very small footprint. It provides an easy to use argument parsing, help displaying, minimalistic error handling and some tools like executing external programs and gather their output.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
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,32 @@
|
|
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
17
|
private
|
14
18
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def __run_opt(opts)
|
30
|
-
%i[stdout stderr status].keep_if{ |s| opts.delete(s) }
|
19
|
+
def __run_proc(stdin_data, in_write)
|
20
|
+
return :read unless stdin_data
|
21
|
+
proc do |out|
|
22
|
+
in_write.sync = true
|
23
|
+
if stdin_data.respond_to?(:readpartial)
|
24
|
+
IO.copy_stream(stdin_data, in_write)
|
25
|
+
else
|
26
|
+
in_write.write(stdin_data)
|
27
|
+
end
|
28
|
+
in_write.close
|
29
|
+
out.read
|
30
|
+
end
|
31
31
|
end
|
32
32
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
12
|
+
This gem is a lean, easy to use CLI framework with a very small footprint.
|
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
ADDED
data/test/apps/sequence
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# demonstrates the call sequence
|
5
|
+
|
6
|
+
require_relative '../../lib/mini-cli'
|
7
|
+
include(MiniCli)
|
8
|
+
|
9
|
+
help <<~HELP
|
10
|
+
-x, --exit early exit
|
11
|
+
-e, --error exit with error
|
12
|
+
HELP
|
13
|
+
|
14
|
+
main do |*args|
|
15
|
+
puts('main' + args.inspect)
|
16
|
+
exit if args.first['exit']
|
17
|
+
error(42, '!error!') if args.first['error']
|
18
|
+
end
|
19
|
+
|
20
|
+
before { |*args| puts('before_1' + args.inspect) }
|
21
|
+
after { |*args| puts('after_1' + args.inspect) }
|
22
|
+
before { |*args| puts('before_2' + args.inspect) }
|
23
|
+
after { |*args| puts('after_2' + args.inspect) }
|
24
|
+
|
25
|
+
parse_argv do |*args|
|
26
|
+
puts('parse_argv' + args.inspect)
|
27
|
+
args.first
|
28
|
+
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,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'shellwords'
|
5
|
+
require_relative '../helper'
|
6
|
+
|
7
|
+
class ExecTest < Minitest::Test
|
8
|
+
parallelize_me!
|
9
|
+
|
10
|
+
def test_noop
|
11
|
+
assert_empty(assert_no_err('noop'))
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_noop_help
|
15
|
+
assert_equal("Usage: noop\n", assert_no_err('noop', '--help'))
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_sequence
|
19
|
+
expected = [
|
20
|
+
'before_1[]',
|
21
|
+
'before_2[]',
|
22
|
+
"parse_argv[{\"FILES\"=>[]}]",
|
23
|
+
"main[{\"FILES\"=>[]}]",
|
24
|
+
'after_2[]',
|
25
|
+
'after_1[]'
|
26
|
+
]
|
27
|
+
assert_equal(expected, assert_no_err('sequence').split("\n"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_sequence_help
|
31
|
+
expected = [
|
32
|
+
'Usage: sequence [OPTIONS]',
|
33
|
+
'',
|
34
|
+
'Options:',
|
35
|
+
'-x, --exit early exit',
|
36
|
+
'-e, --error exit with error'
|
37
|
+
]
|
38
|
+
assert_equal(expected, assert_no_err('sequence', '--help').split("\n"))
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_sequence_early_exit
|
42
|
+
expected = [
|
43
|
+
'before_1[]',
|
44
|
+
'before_2[]',
|
45
|
+
"parse_argv[{\"exit\"=>true, \"FILES\"=>[]}]",
|
46
|
+
"main[{\"exit\"=>true, \"FILES\"=>[]}]",
|
47
|
+
'after_2[]',
|
48
|
+
'after_1[]'
|
49
|
+
]
|
50
|
+
assert_equal(expected, assert_no_err('sequence', '-x').split("\n"))
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_sequence_error
|
54
|
+
expected = [
|
55
|
+
'before_1[]',
|
56
|
+
'before_2[]',
|
57
|
+
"parse_argv[{\"error\"=>true, \"FILES\"=>[]}]",
|
58
|
+
"main[{\"error\"=>true, \"FILES\"=>[]}]",
|
59
|
+
'after_2[]',
|
60
|
+
'after_1[]'
|
61
|
+
]
|
62
|
+
std, err, status = invoke('sequence', '-e')
|
63
|
+
assert_same(42, status.exitstatus)
|
64
|
+
assert_equal("sequence: !error!\n", err)
|
65
|
+
assert_equal(expected, std.split("\n"))
|
66
|
+
end
|
67
|
+
|
68
|
+
def assert_no_err(...)
|
69
|
+
std, err = assert_success(...)
|
70
|
+
assert_empty(err)
|
71
|
+
std
|
72
|
+
end
|
73
|
+
|
74
|
+
def assert_success(...)
|
75
|
+
std, err, status = invoke(...)
|
76
|
+
assert(status.success?)
|
77
|
+
return std, err
|
78
|
+
end
|
79
|
+
|
80
|
+
def invoke(name, *args)
|
81
|
+
Open3.capture3(
|
82
|
+
Shellwords.join(
|
83
|
+
[
|
84
|
+
RbConfig.ruby,
|
85
|
+
'--disable',
|
86
|
+
'gems',
|
87
|
+
'--disable',
|
88
|
+
'did_you_mean',
|
89
|
+
'--disable',
|
90
|
+
'rubyopt',
|
91
|
+
File.expand_path("../apps/#{name}", __dir__)
|
92
|
+
] + args
|
93
|
+
)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
data/test/mini-cli/main_test.rb
CHANGED
@@ -1,24 +1,32 @@
|
|
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
|
+
show_errors=
|
26
|
+
show_errors?
|
19
27
|
show_help
|
20
|
-
]
|
21
|
-
methods = (subject.methods - Object.
|
28
|
+
]
|
29
|
+
methods = (subject.methods - Object.instance_methods).sort!
|
22
30
|
assert_equal(expected_methods, methods)
|
23
31
|
end
|
24
32
|
|
@@ -27,9 +35,14 @@ class MainTest < Test
|
|
27
35
|
subject.error(42, 'some error text')
|
28
36
|
end
|
29
37
|
|
30
|
-
assert_stop_with_error(21, 'error')
|
31
|
-
|
32
|
-
|
38
|
+
assert_stop_with_error(21, 'error') { subject.error(21, :error) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_no_error
|
42
|
+
subject.show_errors = false
|
43
|
+
|
44
|
+
assert_output(nil, nil) { subject.error(666, 'some error text') }
|
45
|
+
assert_equal([666], subject.exit_args)
|
33
46
|
end
|
34
47
|
|
35
48
|
def test_help_simple
|
@@ -39,7 +52,7 @@ class MainTest < Test
|
|
39
52
|
Some helptext
|
40
53
|
EXPECTED
|
41
54
|
|
42
|
-
assert_output(expected_text){ subject.show_help }
|
55
|
+
assert_output(expected_text) { subject.show_help }
|
43
56
|
end
|
44
57
|
|
45
58
|
def test_help_with_args
|
@@ -54,7 +67,7 @@ class MainTest < Test
|
|
54
67
|
here
|
55
68
|
EXPECTED
|
56
69
|
|
57
|
-
assert_output(expected_text){ subject.show_help }
|
70
|
+
assert_output(expected_text) { subject.show_help }
|
58
71
|
end
|
59
72
|
|
60
73
|
def test_argument_required
|
@@ -64,10 +77,10 @@ class MainTest < Test
|
|
64
77
|
subject.parse_argv(as_argv(''))
|
65
78
|
end
|
66
79
|
|
67
|
-
expected = {'ARG' => 'arg', 'FILES' => []}
|
80
|
+
expected = { 'ARG' => 'arg', 'FILES' => [] }
|
68
81
|
assert_equal(expected, subject.parse_argv(as_argv('arg')))
|
69
82
|
|
70
|
-
expected = {'ARG' => 'arg1', 'FILES' => %w[arg2 arg3]}
|
83
|
+
expected = { 'ARG' => 'arg1', 'FILES' => %w[arg2 arg3] }
|
71
84
|
assert_equal(expected, subject.parse_argv(as_argv('arg1 arg2 arg3')))
|
72
85
|
end
|
73
86
|
|
@@ -83,10 +96,10 @@ class MainTest < Test
|
|
83
96
|
subject.parse_argv(as_argv(''))
|
84
97
|
end
|
85
98
|
|
86
|
-
expected = {'ARG1' => 'arg1', 'ARG2' => 'arg2', 'FILES' => []}
|
99
|
+
expected = { 'ARG1' => 'arg1', 'ARG2' => 'arg2', 'FILES' => [] }
|
87
100
|
assert_equal(expected, subject.parse_argv(as_argv('arg1 arg2')))
|
88
101
|
|
89
|
-
expected = {'ARG1' => 'arg', 'FILES' => []}
|
102
|
+
expected = { 'ARG1' => 'arg', 'FILES' => [] }
|
90
103
|
assert_equal(expected, subject.parse_argv(as_argv('arg')))
|
91
104
|
end
|
92
105
|
|
@@ -100,21 +113,21 @@ class MainTest < Test
|
|
100
113
|
Some additional explaination can be here
|
101
114
|
HELP
|
102
115
|
|
103
|
-
expected = {'FILES' => []}
|
116
|
+
expected = { 'FILES' => [] }
|
104
117
|
assert_equal(expected, subject.parse_argv(as_argv('')))
|
105
118
|
|
106
|
-
expected = {'NAME' => 'name', 'FILES' => []}
|
119
|
+
expected = { 'NAME' => 'name', 'FILES' => [] }
|
107
120
|
assert_equal(expected, subject.parse_argv(as_argv('--named name')))
|
108
121
|
assert_equal(expected, subject.parse_argv(as_argv('-n name')))
|
109
122
|
|
110
|
-
expected = {'LNAME' => 'long', 'FILES' => []}
|
123
|
+
expected = { 'LNAME' => 'long', 'FILES' => [] }
|
111
124
|
assert_equal(expected, subject.parse_argv(as_argv('--named-long long')))
|
112
125
|
|
113
|
-
expected = {'unnamed' => true, 'FILES' => []}
|
126
|
+
expected = { 'unnamed' => true, 'FILES' => [] }
|
114
127
|
assert_equal(expected, subject.parse_argv(as_argv('--unnamed')))
|
115
128
|
assert_equal(expected, subject.parse_argv(as_argv('-u')))
|
116
129
|
|
117
|
-
expected = {'un-named' => true, 'FILES' => []}
|
130
|
+
expected = { 'un-named' => true, 'FILES' => [] }
|
118
131
|
assert_equal(expected, subject.parse_argv(as_argv('--un-named')))
|
119
132
|
|
120
133
|
expected = {
|
@@ -125,23 +138,14 @@ class MainTest < Test
|
|
125
138
|
'FILES' => %w[FILE1 FILE2]
|
126
139
|
}
|
127
140
|
|
128
|
-
result =
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
--un-named
|
133
|
-
FILE1
|
134
|
-
FILE2
|
135
|
-
])
|
141
|
+
result =
|
142
|
+
subject.parse_argv(
|
143
|
+
%w[--named name --named-long long --unnamed --un-named FILE1 FILE2]
|
144
|
+
)
|
136
145
|
assert_equal(expected, result)
|
137
146
|
|
138
|
-
result =
|
139
|
-
-nu name
|
140
|
-
--named-long long
|
141
|
-
--un-named
|
142
|
-
FILE1
|
143
|
-
FILE2
|
144
|
-
])
|
147
|
+
result =
|
148
|
+
subject.parse_argv(%w[-nu name --named-long long --un-named FILE1 FILE2])
|
145
149
|
assert_equal(expected, result)
|
146
150
|
end
|
147
151
|
|
@@ -175,4 +179,13 @@ class MainTest < Test
|
|
175
179
|
result = subject.parse_argv(as_argv('-nup name port in out opt file'))
|
176
180
|
assert_equal(expected, result)
|
177
181
|
end
|
182
|
+
|
183
|
+
def test_run
|
184
|
+
call_sequence = []
|
185
|
+
subject.main { call_sequence << :main }
|
186
|
+
subject.before { call_sequence << :start_1 }
|
187
|
+
subject.after { call_sequence << :end_1 }
|
188
|
+
subject.before { call_sequence << :start_2 }
|
189
|
+
subject.after { call_sequence << :end_2 }
|
190
|
+
end
|
178
191
|
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.4.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-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -53,13 +53,14 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
description: |
|
56
|
-
This gem is a
|
57
|
-
|
56
|
+
This gem is a lean, easy to use CLI framework with a very small footprint.
|
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,12 +68,16 @@ 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
|
78
|
+
- test/apps/sequence
|
75
79
|
- test/helper.rb
|
80
|
+
- test/mini-cli/exec_test.rb
|
76
81
|
- test/mini-cli/main_test.rb
|
77
82
|
- test/mini-cli/run_test.rb
|
78
83
|
homepage: https://github.com/mblumtritt/mini-cli
|
@@ -80,7 +85,7 @@ licenses: []
|
|
80
85
|
metadata:
|
81
86
|
source_code_uri: https://github.com/mblumtritt/mini-cli
|
82
87
|
bug_tracker_uri: https://github.com/mblumtritt/mini-cli/issues
|
83
|
-
post_install_message:
|
88
|
+
post_install_message:
|
84
89
|
rdoc_options: []
|
85
90
|
require_paths:
|
86
91
|
- lib
|
@@ -95,11 +100,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
100
|
- !ruby/object:Gem::Version
|
96
101
|
version: '0'
|
97
102
|
requirements: []
|
98
|
-
rubygems_version: 3.
|
99
|
-
signing_key:
|
103
|
+
rubygems_version: 3.2.9
|
104
|
+
signing_key:
|
100
105
|
specification_version: 4
|
101
|
-
summary: The
|
106
|
+
summary: The lean CLI framework for Ruby
|
102
107
|
test_files:
|
108
|
+
- test/apps/noop
|
109
|
+
- test/apps/sequence
|
103
110
|
- test/helper.rb
|
111
|
+
- test/mini-cli/exec_test.rb
|
104
112
|
- test/mini-cli/main_test.rb
|
105
113
|
- test/mini-cli/run_test.rb
|