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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b99c5619db31b2250da1515981cf62e897939e57399e72b44c3b48d22d4c9840
4
- data.tar.gz: 9ea02f0d1077a03ca5284ef20619a9c38370d9bcece25ba106e0b834733660c1
3
+ metadata.gz: b366ee44d4b95a00a977cbfdcf3e7cc7aff2cc1d3f72c03e0df2c2858aaf22e8
4
+ data.tar.gz: 8a622678016ca76e554a6b80d40dbea7dd7d6db5a494b5d7dd83944e68d66921
5
5
  SHA512:
6
- metadata.gz: 5877c7746653b3999ec6337e86e108276f26ba9a2142bdee0892033491cce2f788ee8dea594742cc5fb0554a1496923dfe5b80eba03409e9b6b4c4f2255d2d7d
7
- data.tar.gz: 7c7d1e36a5dd1ae1f139826ad9063e68e38a864416744e383e3e308fb8d172ab6efc58c26c5e4875422884deb6e2fa820ec10cc7371d028fcdfa53b6e06523ba
6
+ metadata.gz: 73a9bee03f689d2f1e7334b6b927ba8cd45efb8422729c1abe93bf9062e26de47886ff0e70dc518ed01eeab7330e8777cd25a2c6fa270d6f16d32c0979de8d8d
7
+ data.tar.gz: cf6679d1245593b36ab18481ea4c83a14da4953cf41e6e849b92951a5dc5301c3603ef42476fe1cab0995d008b5356233f50dc2135f9f3d2530c5a61683560cd
data/gems.rb CHANGED
@@ -1 +1,2 @@
1
+ source 'https://rubygems.org'
1
2
  gemspec
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
- return if const_defined?(:SRC)
6
- const_set(:SRC, caller_locations(1, 1).first.absolute_path)
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
- return name ? @__name = name : @__name if defined?(@__name)
11
- @__name = name || File.basename(MiniCli::SRC, '.*')
13
+ __minicli__.name(name)
12
14
  end
13
15
 
14
16
  def help(helptext, *args)
15
- @__argv_parser = ArgvParser.new(helptext, args)
17
+ __minicli__.help(helptext, args)
16
18
  end
17
19
 
18
20
  def show_help
19
- __argv_parser.show_help(name)
20
- true
21
+ __minicli__.show_help
21
22
  end
22
23
 
23
- def error(code, message)
24
- $stderr.puts("#{name}: #{message}")
25
- exit(code)
24
+ def show_errors?
25
+ __minicli__.show_errors
26
26
  end
27
27
 
28
- def parse_argv(argv = nil, &block)
29
- return @__argv_converter = block if block
30
- argv ||= ARGV.dup
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
- args = __argv_parser.parse(argv, method(:error).to_proc)
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(args = nil)
37
- at_exit do
38
- yield(args || parse_argv)
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 __argv_parser
47
- @__argv_parser ||= ArgvParser.new(nil, [])
66
+ def __minicli__
67
+ self.class.const_get(:MiniCli__)
48
68
  end
49
69
 
50
- class ArgvParser
51
- def initialize(helptext, args)
52
- @helptext = helptext.to_s
53
- @args = args.flatten.map!(&:to_s).uniq
54
- @options = nil
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 show_help(name)
58
- parse_help! unless @options
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 parse(argv, error)
68
- @error = error
69
- parse_help! unless @options
70
- @result, arguments = {}, []
71
- loop do
72
- case arg = argv.shift
73
- when nil
74
- break
75
- when '--'
76
- arguments += argv
77
- break
78
- when /\A--([[[:alnum:]]-]+)\z/
79
- handle_option(Regexp.last_match[1], argv)
80
- when /\A-([[:alnum:]]+)\z/
81
- parse_options(Regexp.last_match[1], argv)
82
- else
83
- arguments << arg
84
- end
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 error(msg)
92
- @error.call(1, msg)
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 process(arguments)
96
- @args.each do |arg|
97
- next if arg.index('..')
98
- value = arguments.shift
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
- def handle_option(option, argv)
111
- key = @options[option] || error("unknown option - #{option}")
112
- return @result[key] = true if option == key
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
- next unless value.nil? || value.start_with?('-')
124
- error("parameter #{key} expected - -#{opt}")
199
+ return unless value.nil? || value.start_with?('-')
200
+ error("parameter #{key} expected - --#{option}")
125
201
  end
126
- end
127
202
 
128
- def parse_help!
129
- @options = {}
130
- @helptext.each_line do |line|
131
- case line
132
- when /-([[:alnum:]]), --([[[:alnum:]]-]+) ([[:upper:]]+)\s+\S+/
133
- named_option(Regexp.last_match)
134
- when /--([[[:alnum:]]-]+) ([[:upper:]]+)\s+\S+/
135
- named_option_short(Regexp.last_match)
136
- when /-([[:alnum:]]), --([[[:alnum:]]-]+)\s+\S+/
137
- option(Regexp.last_match)
138
- when /--([[[:alnum:]]-]+)\s+\S+/
139
- option_short(Regexp.last_match)
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
- def named_option(match)
145
- @options[match[1]] = @options[match[2]] = match[3]
146
- end
224
+ def option_with_argument(match)
225
+ @options[match[1]] = @options[match[2]] = match[3]
226
+ end
147
227
 
148
- def named_option_short(match)
149
- @options[match[1]] = match[2]
150
- end
228
+ def short_option_with_argument(match)
229
+ @options[match[1]] = match[2]
230
+ end
151
231
 
152
- def option(match)
153
- @options[match[1]] = @options[match[2]] = match[2]
154
- end
232
+ def option(match)
233
+ @options[match[1]] = @options[match[2]] = match[2]
234
+ end
155
235
 
156
- def option_short(match)
157
- @options[match[1]] = match[1]
236
+ def short_option(match)
237
+ @options[match[1]] = match[1]
238
+ end
158
239
  end
159
240
  end
160
241
 
161
- private_constant(:ArgvParser)
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
- opts = Hash === cmd.last ? __run_opt(cmd.last) : [:stdout]
8
- opts.delete(:stderr) ? __run3(opts, cmd) : __run2(opts, cmd)
9
- rescue SystemCallError
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
- private
14
-
15
- def __run3(opts, cmd)
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 __run2(opts, cmd)
23
- result = Open3.capture2e(*cmd)
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
- def __run_opt(opts)
30
- %i[stdout stderr status].keep_if{ |s| opts.delete(s) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniCli
4
- VERSION = '0.1.1'
4
+ VERSION = '0.5.0'
5
5
  end
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
- GemSpec = Gem::Specification.new do |gem|
6
- gem.name = 'mini-cli'
7
- gem.version = MiniCli::VERSION
8
- gem.summary = 'The lean CLI framework for Ruby'
9
- gem.description = <<~DESC
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
- gem.required_ruby_version = '>= 2.5.0'
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
- gem.add_development_dependency 'bundler'
24
- gem.add_development_dependency 'minitest'
25
- gem.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'minitest'
25
+ spec.add_development_dependency 'rake'
26
26
 
27
- all_files = %x(git ls-files -z).split(0.chr)
28
- gem.test_files = all_files.grep(%r{^(spec|test)/})
29
- gem.files = all_files - gem.test_files
30
- # gem.extra_rdoc_files = %w[README.MD]
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
- STDOUT.sync = STDERR.sync = true
6
+ $stdout.sync = $stderr.sync = true
7
7
 
8
- CLOBBER << 'prj'
8
+ task(:default) { exec('rake --tasks') }
9
9
 
10
- task :default do
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]
@@ -11,21 +11,21 @@ help <<~HELP, %w[TARGET [SOURCE]]
11
11
  --opt option without any argument
12
12
  HELP
13
13
 
14
- main do |cfg|
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.new(:target, :sources, :name, :url, :switch, :opt).new.tap do |cfg|
20
- cfg.target = args['TARGET']
21
- # args['FILES'] is an array containing all surplus arguments
22
- cfg.sources = args['FILES']
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
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
@@ -9,6 +9,4 @@ help <<~HELP
9
9
  This is a very simple sample without any required parameter.
10
10
  HELP
11
11
 
12
- main do |args|
13
- puts("given files: #{args['FILES']}")
14
- end
12
+ main { |args| puts("given files: #{args['FILES']}") }
data/test/apps/noop.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # completely empty command
3
+
4
+ require_relative '../../lib/mini-cli'
5
+ include(MiniCli)
6
+ main {}
@@ -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 = Class.new do
15
- include MiniCli
16
- attr_reader :exit_args
14
+ @subject =
15
+ Class.new do
16
+ include MiniCli
17
+ attr_reader :exit_args
17
18
 
18
- def exit(*args)
19
- @exit_args = args
20
- end
21
- end.new
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
@@ -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
- assert_output("Usage: helper\n"){ subject.show_help }
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
- ].sort!
21
- methods = (subject.methods - Object.new.methods).sort!
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') do
31
- subject.error(21, :error)
32
- end
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 = subject.parse_argv(%w[
129
- --named name
130
- --named-long long
131
- --unnamed
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 = subject.parse_argv(%w[
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
@@ -1,44 +1,49 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../helper'
2
4
 
3
5
  class RunTest < Test
4
- def setup
5
- super
6
- @pwd = Dir.pwd + "\n"
6
+ def test_simple
7
+ result = subject.run('pwd')
8
+ assert_equal("#{Dir.pwd}\n", result)
7
9
  end
8
10
 
9
- def test_std_out_only
10
- out = subject.run('pwd')
11
- assert_equal(@pwd, out)
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
- out = subject.run('pwd', stdout: true)
14
- assert_equal(@pwd, out)
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', stdout: false)
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 test_std_error
31
- err = subject.run('ls /no-valid-dir', stderr: true)
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.1.1
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: 2020-03-09 00:00:00.000000000 Z
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.1.2
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