luban-cli 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 275afb8e84ecc64947db6b5e147e37996a7d1df1
4
+ data.tar.gz: 866526700250c04f22aab3467ba7ee9dbadc1b66
5
+ SHA512:
6
+ metadata.gz: eb7fcbd25cfe82c7bfcfb99755a1272de32bddf1e12061fedc28a95298e26e1f5d06525717b4341d9debb42dc742acc81f23d93087c477e77f68915f58129416
7
+ data.tar.gz: 13c432783506cd550ec5a012f2b36e25420669af7f5851145323ccd021958f002982b39b2fa4f8285d56db3dfe99077a1937df93c869b517fbe4fe5d93c00238
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Change log
2
+
3
+ ## Version 0.1.0 (Mar 31, 2015)
4
+
5
+ Bootstrapped Luban::CLI
6
+
7
+ Features:
8
+ * Support general command-line parsing
9
+ * Support options
10
+ * Support switches (boolean options)
11
+ * Support arguments
12
+ * Support subcommand
13
+ * Provide base class (Luban::CLI::Base) for command-line application
14
+
15
+ ## Version 0.2.0 (Apr 02, 2015)
16
+
17
+ Minor enhancements:
18
+ * Refractor error class
19
+ * Refractor argument validation
20
+ * Validate required options/arguments in Luban::CLI::Base
21
+ * Create singleton action handler method on application instance
22
+ * Move parse error handling to action handler
23
+ * Exclude examples and spec from the gem itself
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in luban-cli.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Chi Man Lei
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Luban::CLI
2
+
3
+ Luban::CLI is a command-line interface for Ruby with a simple lightweight option parser and command handler based on Ruby standard library, OptionParser.
4
+
5
+ Luban::CLI requires Ruby 2.1 or later.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'luban-cli'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install luban-cli
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,48 @@
1
+ require 'pathname'
2
+
3
+ module Luban
4
+ module CLI
5
+ class Application < Base
6
+ attr_reader :rc
7
+
8
+ def initialize(starter_method = :run, &config_blk)
9
+ super(self, starter_method, &config_blk)
10
+ @rc = init_rc
11
+ validate
12
+ end
13
+
14
+ def rc_file
15
+ @rc_file ||= ".#{program_name}rc"
16
+ end
17
+
18
+ def rc_path
19
+ @rc_path ||= Pathname.new(ENV['HOME']).join(rc_file)
20
+ end
21
+
22
+ def rc_file_exists?
23
+ File.exists?(rc_path)
24
+ end
25
+
26
+ def default_rc
27
+ @default_rc ||= {}
28
+ end
29
+
30
+ protected
31
+
32
+ def validate; end
33
+
34
+ def init_rc
35
+ if rc_file_exists?
36
+ default_rc.merge(load_rc_file)
37
+ else
38
+ default_rc.clone
39
+ end
40
+ end
41
+
42
+ def load_rc_file
43
+ require 'yaml'
44
+ YAML.load_file(rc_path)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,200 @@
1
+ module Luban
2
+ module CLI
3
+ class Argument
4
+ class InvalidArgumentValue < Error; end
5
+ class TypeCastingFailed < Error; end
6
+
7
+ attr_reader :name
8
+ attr_reader :display_name
9
+ attr_reader :description
10
+ attr_accessor :value
11
+
12
+ def initialize(name, desc, **config, &blk)
13
+ @name = name
14
+ @display_name = name.to_s.upcase
15
+ @description = desc.to_s
16
+ @handler = block_given? ? blk : ->(v) { v }
17
+ @config = config
18
+ init_config
19
+ verify_config
20
+ reset
21
+ end
22
+
23
+ def kind
24
+ @kind ||= self.class.name.split('::').last.downcase
25
+ end
26
+
27
+ def reset
28
+ @value = set_default_value
29
+ end
30
+
31
+ def value=(v)
32
+ @value = process(v).tap { |v| validate(v) }
33
+ end
34
+
35
+ def default_type; :string; end
36
+ def default_imperative; true; end
37
+
38
+ def [](key); @config[key]; end
39
+
40
+ def required?; @config[:required]; end
41
+ def optional?; !@config[:required]; end
42
+ def has_default?; !@config[:default].nil?; end
43
+ def multiple?; @config[:multiple]; end
44
+
45
+ def validate(value = @value)
46
+ unless valid?(value)
47
+ raise InvalidArgumentValue, "Invalid value of #{kind} #{display_name}: #{value.inspect}"
48
+ end
49
+ end
50
+
51
+ def valid?(value = @value)
52
+ !missing?(value) and match?(value) and within?(value) and assured?(value)
53
+ end
54
+
55
+ def missing?(value = @value)
56
+ required? and value.nil?
57
+ end
58
+
59
+ def match?(value = @value)
60
+ @config[:match].nil? ? true : !!@config[:match].match(value)
61
+ end
62
+
63
+ def within?(value = @value)
64
+ @config[:within].nil? ? true : @config[:within].include?(value)
65
+ end
66
+
67
+ def assured?(value = @value)
68
+ @config[:assure].nil? ? true : !!@config[:assure].call(value)
69
+ end
70
+
71
+ protected
72
+
73
+ def init_config
74
+ @config[:type] ||= default_type
75
+ @config[:required] = default_imperative if @config[:required].nil?
76
+ @config[:required] = !!@config[:required]
77
+ @config[:multiple] = !!@config[:multiple]
78
+ end
79
+
80
+ def verify_config
81
+ verify_config_type if @config.has_key?(:type)
82
+ verify_config_default_value if @config.has_key?(:default)
83
+ verify_config_match if @config.has_key?(:match)
84
+ verify_config_within if @config.has_key?(:within)
85
+ verify_config_assurance if @config.has_key?(:assure)
86
+ end
87
+
88
+ def verify_config_type
89
+ @config[:type] = normalize_type(@config[:type])
90
+ unless respond_to?(cast_method(@config[:type]), true)
91
+ raise ArgumentError, "NOT castable type for #{kind} #{display_name}: #{@config[:type]}"
92
+ end
93
+ end
94
+
95
+ def normalize_type(type); type.to_s.downcase.to_sym; end
96
+
97
+ def verify_config_default_value
98
+ err_msg = nil
99
+ type = @config[:type]
100
+ default = @config[:default]
101
+ unless type.nil?
102
+ if multiple?
103
+ unless default.is_a?(Array) and
104
+ default.all? { |v| send("#{type}?", v) }
105
+ err_msg = "must be an array of #{type} instances"
106
+ end
107
+ else
108
+ unless send("#{type}?", default)
109
+ err_msg = "must be an instance of #{type}"
110
+ end
111
+ end
112
+ end
113
+ unless err_msg.nil?
114
+ raise ArgumentError, "Default value for #{kind} #{display_name} #{err_msg}"
115
+ end
116
+ unless (multiple? ? default : [default]).all? { |v| valid?(v) }
117
+ raise ArgumentError, "Invalid default value for #{kind} #{display_name}: #{default.inspect}"
118
+ end
119
+ end
120
+
121
+ def verify_config_match
122
+ unless @config[:match].respond_to?(:match)
123
+ raise ArgumentError, "Matching pattern of #{kind} #{display_name} must respond to #match."
124
+ end
125
+ end
126
+
127
+ def verify_config_within
128
+ unless @config[:within].respond_to?(:include?)
129
+ raise ArgumentError, "Possible values of #{kind} #{display_name} must respond to #include?."
130
+ end
131
+ end
132
+
133
+ def verify_config_assurance
134
+ unless @config[:assure].respond_to?(:call)
135
+ raise ArgumentError, "Assurance of #{kind} #{display_name} must be callable."
136
+ end
137
+ end
138
+
139
+ def set_default_value(value = nil)
140
+ if !@config[:default].nil? and value.nil?
141
+ @config[:default]
142
+ else
143
+ value
144
+ end
145
+ end
146
+
147
+ def process(value)
148
+ post_process(@handler.call(pre_process(value)))
149
+ end
150
+
151
+ def pre_process(value)
152
+ cast_type(set_default_value(value))
153
+ end
154
+
155
+ def post_process(value); value; end
156
+
157
+ def cast_type(value)
158
+ unless value.nil?
159
+ if multiple?
160
+ value.map! { |v| cast_value(v) }
161
+ else
162
+ value = cast_value(value)
163
+ end
164
+ end
165
+ value
166
+ end
167
+
168
+ def cast_value(value)
169
+ if send("#{@config[:type]}?", value)
170
+ value
171
+ else
172
+ method(cast_method(@config[:type])).call(value)
173
+ end
174
+ rescue StandardError => e
175
+ raise TypeCastingFailed, "Type casting to #{@config[:type]} for #{kind} #{display_name} failed: #{e.class.name} - #{e.message}"
176
+ end
177
+
178
+ def cast_method(type); "cast_#{type}"; end
179
+
180
+ def cast_string(value); String(value); end
181
+ def cast_integer(value); Integer(value); end
182
+ def cast_float(value); Float(value); end
183
+ def cast_symbol(value); value.to_sym; end
184
+ def cast_time(value); Time.parse(value); end
185
+ def cast_date(value); Date.parse(value); end
186
+ def cast_datetime(value); DateTime.parse(value); end
187
+ BoolValues = {"true" => true, "false" => false, "yes" => true, "no" => false }
188
+ def cast_bool(value); !!BoolValues[value.to_s.downcase]; end
189
+
190
+ def string?(value); value.is_a?(String); end
191
+ def integer?(value); value.is_a?(Integer); end
192
+ def float?(value); value.is_a?(Float); end
193
+ def symbol?(value); value.is_a?(Symbol); end
194
+ def time?(value); value.is_a?(Time); end
195
+ def date?(value); value.is_a?(Date); end
196
+ def datetime?(value); value.is_a?(DateTime); end
197
+ def bool?(value); value.is_a?(TrueClass) or value.is_a?(FalseClass); end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,260 @@
1
+ require 'optparse'
2
+
3
+ # Remove duplicate default options in OptionParser
4
+ # for ARGV which never appear in option summary
5
+ OptionParser::Officious.delete('help')
6
+ OptionParser::Officious.delete('version')
7
+
8
+ module Luban
9
+ module CLI
10
+ class Base
11
+ include Commands
12
+
13
+ class MissingCommand < Error; end
14
+ class InvalidCommand < Error; end
15
+ class MissingRequiredOptions < Error; end
16
+ class MissingRequiredArguments < Error; end
17
+
18
+ DefaultSummaryWidth = 32
19
+ DefaultSummaryIndent = 4
20
+ DefaultTitleIndent = 2
21
+
22
+ attr_reader :program_name
23
+ attr_reader :options
24
+ attr_reader :arguments
25
+ attr_reader :summary
26
+ attr_reader :description
27
+ attr_reader :version
28
+ attr_reader :result
29
+ attr_reader :default_argv
30
+
31
+ attr_accessor :title_indent
32
+ attr_accessor :summary_width
33
+ attr_accessor :summary_indent
34
+
35
+ def initialize(app, starter_method, &config_blk)
36
+ @app = app
37
+ @starter_method = starter_method
38
+ @action_defined = false
39
+
40
+ @program_name = default_program_name
41
+ @options = {}
42
+ @arguments = {}
43
+ @summary = ''
44
+ @description = ''
45
+ @version = ''
46
+ @default_argv = ARGV
47
+ @result = { cmd: nil, argv: @default_argv, args: {}, opts: {} }
48
+
49
+ @title_indent = DefaultTitleIndent
50
+ @summary_width = DefaultSummaryWidth
51
+ @summary_indent = DefaultSummaryIndent
52
+
53
+ configure(&config_blk)
54
+ setup_default_starter unless @action_defined
55
+ end
56
+
57
+ def parser
58
+ @parser ||= create_parser
59
+ end
60
+
61
+ def default_program_name
62
+ @default_program_name ||= File.basename($0, '.*')
63
+ end
64
+
65
+ def reset
66
+ @options.each_value { |o| o.reset }
67
+ @arguments.each_value { |a| a.reset }
68
+ @result = { cmd: nil, argv: @default_argv, args: {}, opts: {} }
69
+ end
70
+
71
+ protected
72
+
73
+ def configure(&blk)
74
+ config_blk = block_given? ? blk : self.class.config_blk
75
+ instance_eval(&config_blk) unless config_blk.nil?
76
+ end
77
+
78
+ def setup_default_starter
79
+ method = @starter_method
80
+ if has_commands?
81
+ action :dispatch_command
82
+ else
83
+ action do |**opts|
84
+ raise NotImplementedError, "#{self.class.name}##{method} is an abstract method."
85
+ end
86
+ end
87
+ end
88
+
89
+ def dispatch_command(cmd:, argv:, **params)
90
+ validate_command(cmd)
91
+ send(cmd, argv)
92
+ end
93
+
94
+ def validate_command(cmd)
95
+ if cmd.nil?
96
+ raise MissingCommand, "Missing command. Expected command: #{list_commands.join(', ')}"
97
+ end
98
+ unless has_command?(cmd)
99
+ raise InvalidCommand, "Invalid command. Expected command: #{list_commands.join(', ')}"
100
+ end
101
+ end
102
+
103
+ def validate_required_options
104
+ missing_opts = @options.each_value.select(&:missing?).collect(&:display_name)
105
+ unless missing_opts.empty?
106
+ raise MissingRequiredOptions, "Missing required option(s): #{missing_opts.join(', ')}"
107
+ end
108
+ end
109
+
110
+ def validate_required_arguments
111
+ missing_args = @arguments.each_value.select(&:missing?).collect(&:display_name)
112
+ unless missing_args.empty?
113
+ raise MissingRequiredArguments, "Missing required argument(s): #{missing_args.join(', ')}"
114
+ end
115
+ end
116
+
117
+ def create_parser
118
+ @parser = OptionParser.new
119
+ add_parser_usage
120
+ add_parser_version unless @version.empty?
121
+ add_parser_options unless options.empty?
122
+ add_parser_arguments unless arguments.empty?
123
+ add_parser_summary unless summary.empty?
124
+ add_parser_description unless description.empty?
125
+ add_parser_defaults unless options.values.all? { |o| o.default_str.empty? }
126
+ add_parser_commands if has_commands?
127
+ @parser
128
+ end
129
+
130
+ def add_parser_usage
131
+ parser.banner = compose_banner
132
+ text
133
+ end
134
+
135
+ def compose_banner
136
+ "Usage: #{program_name} #{compose_synopsis}"
137
+ end
138
+
139
+ def compose_synopsis
140
+ if has_commands?
141
+ compose_synopsis_with_commands
142
+ else
143
+ compose_synopsis_without_commands
144
+ end
145
+ end
146
+
147
+ def compose_synopsis_with_commands
148
+ "[options] command [command options] [arguments ...]"
149
+ end
150
+
151
+ def compose_synopsis_without_commands
152
+ "#{compose_synopsis_with_options}#{compose_synopsis_with_arguments}"
153
+ end
154
+
155
+ def compose_synopsis_with_options
156
+ options.empty? ? '' : '[options] '
157
+ end
158
+
159
+ def compose_synopsis_with_arguments
160
+ synopsis = ''
161
+ @arguments.each_value do |arg|
162
+ synopsis += arg.required? ? arg.display_name : "[#{arg.display_name}]"
163
+ synopsis += "[, #{arg.display_name}]*" if arg.multiple?
164
+ synopsis += ' '
165
+ end
166
+ synopsis
167
+ end
168
+
169
+ def text(string = nil)
170
+ @parser.separator(string)
171
+ end
172
+
173
+ def add_parser_version
174
+ parser.version = @version
175
+ end
176
+
177
+ def add_parser_options
178
+ add_section("Options") do
179
+ @options.each_value do |option|
180
+ parser.on(*option.specs) { |v| option.value = v }
181
+ end
182
+ end
183
+ end
184
+
185
+ def add_parser_arguments
186
+ add_section("Arguments") do |rows|
187
+ @arguments.each_value do |arg|
188
+ rows.concat(summarize(arg.display_name, arg.description))
189
+ end
190
+ end
191
+ end
192
+
193
+ def add_parser_summary
194
+ add_section("Summary", wrap(summary, summary_width * 2))
195
+ end
196
+
197
+ def add_parser_description
198
+ add_section("Description", wrap(description, summary_width * 2))
199
+ end
200
+
201
+ def add_parser_defaults
202
+ add_section("Defaults", options.values.map(&:default_str))
203
+ end
204
+
205
+ def add_parser_commands
206
+ add_section("Commands") do |rows|
207
+ commands.each_value do |cmd|
208
+ rows.concat(summarize(cmd.name, cmd.summary))
209
+ end
210
+ end
211
+ end
212
+
213
+ def add_section(title, rows = [], &blk)
214
+ text compose_title(title)
215
+ yield rows if block_given?
216
+ rows.each { |row| text compose_row(row) }
217
+ text
218
+ end
219
+
220
+ def compose_title(title, indent = ' ' * title_indent, suffix = ':')
221
+ "#{indent}#{title}#{suffix}"
222
+ end
223
+
224
+ def compose_row(string, indent = ' ' * summary_indent)
225
+ "#{indent}#{string}"
226
+ end
227
+
228
+ def summarize(item, summary, width = summary_width, max_width = width - 1)
229
+ item_rows = wrap(item.to_s, max_width)
230
+ summary_rows = wrap(summary, max_width)
231
+ num_of_rows = [item_rows.size, summary_rows.size].max
232
+ rows = (0...num_of_rows).collect do |i|
233
+ compose_summary(item_rows[i], summary_rows[i], width)
234
+ end
235
+ return rows
236
+ end
237
+
238
+ def compose_summary(item, summary, width)
239
+ "%-#{width}s %-#{width}s" % [item, summary]
240
+ end
241
+
242
+ def wrap(string, width)
243
+ rows = []
244
+ row = ''
245
+ string.split(/\s+/).each do |word|
246
+ if row.size + word.size >= width
247
+ rows << row
248
+ row = word
249
+ elsif row.empty?
250
+ row = word
251
+ else
252
+ row << ' ' << word
253
+ end
254
+ end
255
+ rows << row if row
256
+ rows
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,141 @@
1
+ module Luban
2
+ module CLI
3
+ class Base
4
+ include Commands
5
+
6
+ class << self
7
+ attr_reader :config_blk
8
+
9
+ def configure(&blk); @config_blk = blk; end
10
+
11
+ def help_command(auto_help: true, &blk)
12
+ if block_given?
13
+ command(:help, &blk)
14
+ else
15
+ command(:help) do
16
+ desc "List all commands or help for one command"
17
+ argument :command, "Command to help for",
18
+ type: :symbol, required: false
19
+ self.auto_help if auto_help
20
+ action :show_help_for_command
21
+ end
22
+ end
23
+ end
24
+ alias_method :auto_help_command, :help_command
25
+ end
26
+
27
+ def program(name)
28
+ @program_name = name.to_s unless name.nil?
29
+ end
30
+
31
+ def desc(string)
32
+ @summary = string.to_s unless string.nil?
33
+ end
34
+
35
+ def long_desc(string)
36
+ @description = string.to_s unless string.nil?
37
+ end
38
+
39
+ def option(name, desc, nullable: false, **config, &blk)
40
+ @options[name] = if nullable
41
+ NullableOption.new(name, desc, **config, &blk)
42
+ else
43
+ Option.new(name, desc, **config, &blk)
44
+ end
45
+ end
46
+
47
+ def switch(name, desc, negatable: false, **config, &blk)
48
+ @options[name] = if negatable
49
+ NegatableSwitch.new(name, desc, **config, &blk)
50
+ else
51
+ Switch.new(name, desc, **config, &blk)
52
+ end
53
+ end
54
+
55
+ def argument(name, desc, **config, &blk)
56
+ @arguments[name] = Argument.new(name, desc, **config, &blk)
57
+ end
58
+
59
+ def help(short: :h, desc: "Show this help message.", &blk)
60
+ switch :help, desc, short: short, &blk
61
+ end
62
+ alias_method :auto_help, :help
63
+
64
+ def show_help; puts parser.help; end
65
+
66
+ def show_help_for_command(args:, **params)
67
+ if args[:command].nil?
68
+ show_help
69
+ else
70
+ commands[args[:command]].show_help
71
+ end
72
+ end
73
+
74
+ def version(ver = nil, short: :v, desc: "Show #{program_name} version.", &blk)
75
+ if ver.nil?
76
+ @version
77
+ else
78
+ @version = ver.to_s
79
+ switch :version, desc, short: short, &blk
80
+ end
81
+ end
82
+
83
+ def show_version; puts parser.ver; end
84
+
85
+ def action(method_name = nil, &blk)
86
+ create_action(method_name, preserve_argv: true, &blk)
87
+ end
88
+
89
+ def action!(method_name = nil, &blk)
90
+ create_action(method_name, preserve_argv: false, &blk)
91
+ end
92
+
93
+ protected
94
+
95
+ def create_action(method_name = nil, preserve_argv: true, &blk)
96
+ handler = if method_name
97
+ lambda { |**opts| send(method_name, **opts) }
98
+ elsif block_given?
99
+ blk
100
+ end
101
+ if handler.nil?
102
+ raise ArgumentError, "Code block to execute command #{@starter_method} is MISSING."
103
+ end
104
+ _base = self
105
+ parse_method = preserve_argv ? :parse : :parse!
106
+ @app.define_singleton_method(@starter_method) do |argv=_base.default_argv|
107
+ _base.send(parse_method, argv)
108
+ begin
109
+ if _base.result[:opts][:help]
110
+ _base.show_help
111
+ elsif _base.result[:opts][:version]
112
+ _base.show_version
113
+ else
114
+ _base.validate_required_options
115
+ _base.validate_required_arguments
116
+ instance_exec(**_base.result, &handler)
117
+ end
118
+ rescue OptionParser::ParseError, Error => e
119
+ _base.on_parse_error(e)
120
+ end
121
+ end
122
+ @action_defined = true
123
+ end
124
+
125
+ def on_parse_error(error)
126
+ show_error_and_exit(error)
127
+ end
128
+
129
+ def show_error_and_exit(error)
130
+ show_error(error)
131
+ show_help
132
+ exit 64 # Linux standard for bad command line
133
+ end
134
+
135
+ def show_error(error)
136
+ puts "#{error.message} (#{error.class.name})"
137
+ puts
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,57 @@
1
+ module Luban
2
+ module CLI
3
+ class Option < Argument
4
+ def specs
5
+ specs = [ description ]
6
+ specs << build_long_option
7
+ specs << build_short_option if @config.has_key?(:short)
8
+ specs << Array if multiple?
9
+ specs
10
+ end
11
+
12
+ def default_imperative; false; end
13
+
14
+ def default_str
15
+ @default_str ||= has_default? ? build_default_str : ''
16
+ end
17
+
18
+ protected
19
+
20
+ def build_default_str
21
+ "--#{long_opt_name} #{default_value_str.inspect}"
22
+ end
23
+
24
+ def build_long_option
25
+ "--#{long_opt_name} #{@display_name}"
26
+ end
27
+
28
+ def build_short_option
29
+ "-#{@config[:short]}"
30
+ end
31
+
32
+ def long_opt_name
33
+ (@config[:long] || @name).to_s.gsub('_', '-')
34
+ end
35
+
36
+ def default_value_str
37
+ [*@config[:default]].map(&:to_s).join(",")
38
+ end
39
+ end
40
+
41
+ class NullableOption < Option
42
+ def kind; @kind ||= "nullable option"; end
43
+
44
+ def value=(val)
45
+ super
46
+ @value = true if @value.nil?
47
+ @value
48
+ end
49
+
50
+ protected
51
+
52
+ def build_long_option
53
+ "--#{long_opt_name} [#{@display_name}]"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ module Luban
2
+ module CLI
3
+ class Base
4
+ def parse(argv=default_argv)
5
+ argv = argv.dup
6
+ parse!(argv)
7
+ end
8
+
9
+ def parse!(argv=default_argv)
10
+ if commands.empty?
11
+ parse_without_commands(argv)
12
+ else
13
+ parse_with_commands(argv)
14
+ end
15
+ update_result(argv)
16
+ end
17
+
18
+ protected
19
+
20
+ def parse_with_commands(argv)
21
+ parser.order!(argv)
22
+ parse_command(argv)
23
+ end
24
+ alias_method :parse_posixly_correct, :parse_with_commands
25
+
26
+ def parse_command(argv)
27
+ cmd = argv.shift
28
+ cmd = cmd.to_sym unless cmd.nil?
29
+ @result[:cmd] = cmd
30
+ end
31
+
32
+ def parse_without_commands(argv)
33
+ parser.permute!(argv)
34
+ parse_arguments(argv)
35
+ end
36
+ alias_method :parse_permutationally, :parse_without_commands
37
+
38
+ def parse_arguments(argv)
39
+ @arguments.each_value do |arg|
40
+ break if argv.empty?
41
+ arg.value = arg.multiple? ? argv.slice!(0..-1) : argv.shift
42
+ end
43
+ end
44
+
45
+ def update_result(argv)
46
+ @result[:argv] = argv
47
+ @result[:opts] = options.values.inject({}) { |r, o| r[o.name] = o.value; r }
48
+ @result[:args] = arguments.values.inject({}) { |r, a| r[a.name] = a.value; r }
49
+ @result
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,39 @@
1
+ module Luban
2
+ module CLI
3
+ class Switch < Option
4
+ protected
5
+
6
+ def init_config
7
+ super
8
+ # Ensure value type to be boolean
9
+ @config[:type] = :bool
10
+ # Ensure single value instead of multiple
11
+ @config[:multiple] = false
12
+ # Ensure default switch state is set properly
13
+ @config[:default] = !!@config[:default]
14
+ end
15
+
16
+ def build_default_str
17
+ @config[:default] ? "--#{long_opt_name}" : ""
18
+ end
19
+
20
+ def build_long_option
21
+ "--#{long_opt_name}"
22
+ end
23
+ end
24
+
25
+ class NegatableSwitch < Switch
26
+ def kind; @kind ||= "negatable switch"; end
27
+
28
+ protected
29
+
30
+ def build_default_str
31
+ @config[:default] ? "--#{long_opt_name}" : "--no-#{long_opt_name}"
32
+ end
33
+
34
+ def build_long_option
35
+ "--[no-]#{long_opt_name}"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ require_relative "base/argument"
2
+ require_relative "base/option"
3
+ require_relative "base/switch"
4
+ require_relative "base/core"
5
+ require_relative "base/parse"
6
+ require_relative "base/dsl"
@@ -0,0 +1,18 @@
1
+ module Luban
2
+ module CLI
3
+ class Command < Base
4
+ attr_reader :name
5
+
6
+ def initialize(app, name, &config_blk)
7
+ super
8
+ @name = name
9
+ end
10
+
11
+ protected
12
+
13
+ def compose_banner
14
+ "Usage: #{program_name} #{name} #{compose_synopsis}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,65 @@
1
+ module Luban
2
+ module CLI
3
+ module Commands
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.send(:include, InstanceMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def inherited(subclass)
11
+ # Ensure commands from base class
12
+ # got inherited to its subclasses
13
+ subclass.instance_variable_set(
14
+ '@commands',
15
+ Marshal.load(Marshal.dump(instance_variable_get('@commands')))
16
+ )
17
+ super
18
+ end
19
+
20
+ def commands
21
+ @commands ||= {}
22
+ end
23
+
24
+ def list_commands
25
+ commands.keys
26
+ end
27
+
28
+ def has_command?(cmd)
29
+ commands.has_key?(cmd)
30
+ end
31
+
32
+ def has_commands?
33
+ !commands.empty?
34
+ end
35
+
36
+ def command(cmd, &blk)
37
+ commands[cmd] = Command.new(self, cmd, &blk)
38
+ end
39
+
40
+ def undef_command(cmd)
41
+ commands.delete(cmd)
42
+ undef_method(cmd)
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ def commands
48
+ self.class.commands
49
+ end
50
+
51
+ def list_commands
52
+ commands.keys
53
+ end
54
+
55
+ def has_command?(cmd)
56
+ self.class.has_command?(cmd)
57
+ end
58
+
59
+ def has_commands?
60
+ self.class.has_commands?
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ module Luban
2
+ module CLI
3
+ class Error < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Luban
2
+ module CLI
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
data/lib/luban/cli.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative "cli/error"
2
+ require_relative "cli/version"
3
+ require_relative "cli/commands"
4
+ require_relative "cli/base"
5
+ require_relative "cli/command"
6
+ require_relative "cli/application"
data/lib/luban-cli.rb ADDED
File without changes
data/luban-cli.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'luban/cli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "luban-cli"
8
+ spec.version = Luban::CLI::VERSION
9
+ spec.authors = ["Rubyist Chi"]
10
+ spec.email = ["rubyist.chi@gmail.com"]
11
+ spec.description = %q{Command-line interface for Ruby}
12
+ spec.summary = %q{Luban::CLI is a command-line interface for Ruby with a simple lightweight option parser and command handler based on Ruby standard library, OptionParser}
13
+ spec.homepage = "https://github.com/lubanrb/cli"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/).grep(%r{^(?!spec|examples)})
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = ">= 2.1.0"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: luban-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Rubyist Chi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Command-line interface for Ruby
42
+ email:
43
+ - rubyist.chi@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - CHANGELOG.md
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - lib/luban-cli.rb
55
+ - lib/luban/cli.rb
56
+ - lib/luban/cli/application.rb
57
+ - lib/luban/cli/base.rb
58
+ - lib/luban/cli/base/argument.rb
59
+ - lib/luban/cli/base/core.rb
60
+ - lib/luban/cli/base/dsl.rb
61
+ - lib/luban/cli/base/option.rb
62
+ - lib/luban/cli/base/parse.rb
63
+ - lib/luban/cli/base/switch.rb
64
+ - lib/luban/cli/command.rb
65
+ - lib/luban/cli/commands.rb
66
+ - lib/luban/cli/error.rb
67
+ - lib/luban/cli/version.rb
68
+ - luban-cli.gemspec
69
+ homepage: https://github.com/lubanrb/cli
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.1.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.4.5
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Luban::CLI is a command-line interface for Ruby with a simple lightweight
93
+ option parser and command handler based on Ruby standard library, OptionParser
94
+ test_files: []