luban-cli 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []