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 +7 -0
- data/.gitignore +17 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +1 -0
- data/lib/luban/cli/application.rb +48 -0
- data/lib/luban/cli/base/argument.rb +200 -0
- data/lib/luban/cli/base/core.rb +260 -0
- data/lib/luban/cli/base/dsl.rb +141 -0
- data/lib/luban/cli/base/option.rb +57 -0
- data/lib/luban/cli/base/parse.rb +53 -0
- data/lib/luban/cli/base/switch.rb +39 -0
- data/lib/luban/cli/base.rb +6 -0
- data/lib/luban/cli/command.rb +18 -0
- data/lib/luban/cli/commands.rb +65 -0
- data/lib/luban/cli/error.rb +5 -0
- data/lib/luban/cli/version.rb +5 -0
- data/lib/luban/cli.rb +6 -0
- data/lib/luban-cli.rb +0 -0
- data/luban-cli.gemspec +25 -0
- metadata +94 -0
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
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
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,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
|
data/lib/luban/cli.rb
ADDED
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: []
|