cli_base 0.5.1

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
+ SHA256:
3
+ metadata.gz: 4e6c9333d54368e8012669ea89d3b1ec55fba5f5a8851dfaa4c44df9d922bb41
4
+ data.tar.gz: 91a489bfed668142eb0efda172b5b4ea6d4441e5d93a34783a2e78c13f12f5e6
5
+ SHA512:
6
+ metadata.gz: 4a586009ff6a6c1f94476b65c529763c20a2303253072ea31762c6f390049518d437944b0a43184ec86218d40c1d58e63694fd69c49aa4cf127d2b35732ae367
7
+ data.tar.gz: bdbf99d8468944e32d86d1107e4cf786e5c51d52e507eb6adfa9344b7d765c793b4ee285a665cfa97828dd51075113098439b59fa8f1d48b7be63ddd6cf529d1
data/HISTORY.md ADDED
@@ -0,0 +1,48 @@
1
+ # RELEASE HISTORY
2
+
3
+ ## 0.5.1 / 2026-04-01
4
+
5
+ Maintenance release. Modernized project tooling.
6
+
7
+ Changes:
8
+
9
+ * Replace custom metadata system with standard gemspec.
10
+ * Simplify version handling.
11
+ * Update README to markdown.
12
+ * Clean up obsolete files and .gitignore.
13
+
14
+
15
+ ## 0.5.0 / 2011-10-08
16
+
17
+ The library has been renamed to CLI::Base (gem name `cli_base`).
18
+
19
+ Changes:
20
+
21
+ * Renamed to CLI::Base.
22
+
23
+
24
+ ## 0.4.0 / 2010-10-12
25
+
26
+ New API uses nested classes instead of methods for subcommands.
27
+
28
+ Changes:
29
+
30
+ * New API!
31
+
32
+
33
+ ## 0.3.0 / 2010-09-12
34
+
35
+ The most significant change is the use of bang methods (foo!) for option flags.
36
+
37
+ Changes:
38
+
39
+ * Allow bang methods for option flags.
40
+
41
+
42
+ ## 0.2.0 / 2010-03-02
43
+
44
+ Initial (non-public) release. Rebranding of older library called TieClip.
45
+
46
+ Changes:
47
+
48
+ * Happy Birthday!
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # CLI::Base
2
+
3
+ [Source Code](https://github.com/rubyworks/cli_base) |
4
+ [Report Issue](https://github.com/rubyworks/cli_base/issues)
5
+
6
+ [![Gem Version](https://img.shields.io/gem/v/cli_base.svg?style=flat)](https://rubygems.org/gems/cli_base)
7
+
8
+
9
+ ## Description
10
+
11
+ CLI::Base is a quick-and-dirty CLI framework for Ruby. Need a command-line
12
+ interface without the ceremony? Just subclass `CLI::Base` and define methods —
13
+ writer methods (`=`) become options, query methods (`?`) become flags, and
14
+ nested classes become subcommands. No DSL to learn.
15
+
16
+ Think of it as a COM (Command-to-Object Mapper).
17
+
18
+
19
+ ## Synopsis
20
+
21
+ ```ruby
22
+ require 'cli/base'
23
+
24
+ class MyCLI < CLI::Base
25
+ # Require LIBRARY before executing your script.
26
+ def require=(lib)
27
+ require lib
28
+ end
29
+ alias :r= :require=
30
+
31
+ # Include PATH in $LOAD_PATH.
32
+ def include=(path)
33
+ $:.unshift path
34
+ end
35
+ alias :I= :include=
36
+
37
+ # Run in DEBUG mode.
38
+ def debug?
39
+ $DEBUG = true
40
+ end
41
+
42
+ # Show this message.
43
+ def help?
44
+ puts self
45
+ exit
46
+ end
47
+ alias :h? :help?
48
+
49
+ # Run the command.
50
+ def main(script)
51
+ load(script)
52
+ end
53
+ end
54
+ ```
55
+
56
+ That's it. The comments above each method become the help text automatically.
57
+
58
+
59
+ ## Copyright
60
+
61
+ Copyright (c) 2008 Thomas Sawyer, Rubyworks
62
+
63
+ Distributed under the terms of the **BSD-2-Clause** license.
64
+
65
+ See COPYING.rdoc for details.
data/lib/cli/base.rb ADDED
@@ -0,0 +1,182 @@
1
+ module CLI
2
+
3
+ # = CLI::Base
4
+ #
5
+ # class MyCLI < CLI::Base
6
+ #
7
+ # # cmd --debug
8
+ #
9
+ # def debug?
10
+ # $DEBUG
11
+ # end
12
+ #
13
+ # def debug=(bool)
14
+ # $DEBUG = bool
15
+ # end
16
+ #
17
+ # # $ foo remote
18
+ #
19
+ # class Remote < CLI::Base
20
+ #
21
+ # # $ foo remote --verbose
22
+ #
23
+ # def verbose?
24
+ # @verbose
25
+ # end
26
+ #
27
+ # def verbose=(bool)
28
+ # @verbose = bool
29
+ # end
30
+ #
31
+ # # $ foo remote --force
32
+ #
33
+ # def force?
34
+ # @force
35
+ # end
36
+ #
37
+ # def force=(bool)
38
+ # @force = bool
39
+ # end
40
+ #
41
+ # # $ foo remote --output <path>
42
+ #
43
+ # def output=(path)
44
+ # @path = path
45
+ # end
46
+ #
47
+ # # $ foo remote -o <path>
48
+ #
49
+ # alias_method :o=, :output=
50
+ #
51
+ # # $ foo remote add
52
+ #
53
+ # class Add < self
54
+ #
55
+ # def main(name, branch)
56
+ # # ...
57
+ # end
58
+ #
59
+ # end
60
+ #
61
+ # # $ foo remote show
62
+ #
63
+ # class Show < self
64
+ #
65
+ # def main(name)
66
+ # # ...
67
+ # end
68
+ #
69
+ # end
70
+ #
71
+ # end
72
+ #
73
+ # end
74
+ #
75
+ class Base
76
+
77
+ require 'cli/errors'
78
+ require 'cli/parser'
79
+ require 'cli/help'
80
+ require 'cli/config'
81
+ require 'cli/utils'
82
+
83
+ # TODO: Should #main be called #call instead?
84
+
85
+ #
86
+ def main(*args)
87
+ #puts self.class # TODO: fix help
88
+ raise NoCommandError
89
+ end
90
+
91
+ # Override option_missing if needed. This receives the name of the option
92
+ # and the remaining argumentslist. It must consume any arguments it uses
93
+ # from the begining of the list (i.e. in-place manipulation).
94
+ #
95
+ def option_missing(opt, argv)
96
+ raise NoOptionError, opt
97
+ end
98
+
99
+ # Access the help instance of the class of the command object.
100
+ def cli_help
101
+ self.class.help
102
+ end
103
+
104
+ class << self
105
+
106
+ # Helper method for creating switch attributes.
107
+ #
108
+ # This is equivalent to:
109
+ #
110
+ # def name=(val)
111
+ # @name = val
112
+ # end
113
+ #
114
+ # def name?
115
+ # @name
116
+ # end
117
+ #
118
+ def attr_switch(name)
119
+ attr_writer name
120
+ module_eval %{
121
+ def #{name}?
122
+ @#{name}
123
+ end
124
+ }
125
+ end
126
+
127
+ # Run the command.
128
+ #
129
+ # @param argv [Array] command-line arguments
130
+ #
131
+ def execute(argv=ARGV)
132
+ cli, args = parser.parse(argv)
133
+ cli.main(*args)
134
+ return cli
135
+ end
136
+
137
+ # CLI::Base classes don't run, they execute! But...
138
+ alias_method :run, :execute
139
+
140
+ # Command configuration options.
141
+ #
142
+ # @todo: This isn't used yet. Eventually the idea is to allow
143
+ # some additional flexibility in the parser behavior.
144
+ def config
145
+ @config ||= Config.new
146
+ end
147
+
148
+ # The parser for this command.
149
+ def parser
150
+ @parser ||= Parser.new(self)
151
+ end
152
+
153
+ # List of subcommands.
154
+ def subcommands
155
+ parser.subcommands
156
+ end
157
+
158
+ # Interface with cooresponding help object.
159
+ def help
160
+ @help ||= Help.new(self)
161
+ end
162
+
163
+ #
164
+ def inspect
165
+ name
166
+ end
167
+
168
+ # When inherited, setup up the +file+ and +line+ of the
169
+ # subcommand via +caller+. If for some odd reason this
170
+ # does not work then manually use +setup+ method.
171
+ #
172
+ def inherited(subclass)
173
+ file, line, _ = *caller.first.split(':')
174
+ file = File.expand_path(file)
175
+ subclass.help.setup(file,line.to_i)
176
+ end
177
+
178
+ end
179
+
180
+ end
181
+
182
+ end
data/lib/cli/config.rb ADDED
@@ -0,0 +1,16 @@
1
+ module CLI
2
+
3
+ # Config is essentially an OpenHash.
4
+ class Config < Hash
5
+ def method_missing(s, *a, &b)
6
+ name = s.to_s
7
+ case name
8
+ when /=$/
9
+ self[name.chomp('=')] = a.first
10
+ else
11
+ self[name]
12
+ end
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,20 @@
1
+ class UnboundMethod
2
+ if !method_defined?(:source_location)
3
+ if Proc.method_defined? :__file__ # /ree/
4
+ def source_location
5
+ [__file__, __line__] rescue nil
6
+ end
7
+ elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/
8
+ require 'java'
9
+ def source_location
10
+ to_java.source_location(Thread.current.to_java.getContext())
11
+ end
12
+ end
13
+ end
14
+
15
+ #
16
+ def comment
17
+ Source.get_above_comment(*source_location)
18
+ end
19
+ end
20
+
data/lib/cli/errors.rb ADDED
@@ -0,0 +1,26 @@
1
+ module CLI
2
+
3
+ class Base
4
+
5
+ class NoOptionError < ::NoMethodError # ArgumentError ?
6
+ def initialize(name, *arg)
7
+ super("unknown option -- #{name}", name, *args)
8
+ end
9
+ end
10
+
11
+ #class NoCommandError < ::NoMethodError
12
+ # def initialize(name, *args)
13
+ # super("unknown command -- #{name}", name, *args)
14
+ # end
15
+ #end
16
+
17
+ class NoCommandError < ::NoMethodError
18
+ def initialize(*args)
19
+ super("missing command", *args)
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
data/lib/cli/help.rb ADDED
@@ -0,0 +1,295 @@
1
+ module CLI
2
+
3
+ require 'cli/source'
4
+ require 'cli/core_ext'
5
+
6
+ # Encpsulates command help for deefining and displaying well formated help
7
+ # output in plain text or via manpages if found.
8
+ class Help
9
+
10
+ # Setup new help object.
11
+ def initialize(cli_class)
12
+ @cli_class = cli_class
13
+
14
+ @usage = nil
15
+ @footer = nil
16
+
17
+ @options = {}
18
+ @subcmds = {}
19
+ end
20
+
21
+ # Set file and line under which the CLI::Base subclass is defined.
22
+ def setup(file, line=nil)
23
+ @file = file
24
+ @line = line
25
+ end
26
+
27
+ # The CLI::Base subclass to which this help applies.
28
+ attr :cli_class
29
+
30
+ # Get or set command name.
31
+ #
32
+ # By default the name is assumed to be the class name, substituting
33
+ # dashes for double colons.
34
+ def name(name=nil)
35
+ @name = name if name
36
+ @name ||= cli_class.name.downcase.gsub('::','-')
37
+ #File.basename($0)
38
+ @name
39
+ end
40
+
41
+ # Get or set command usage.
42
+ def usage(text=nil)
43
+ @usage ||= "Usage: " + File.basename($0) + ' [options...] [subcommand]'
44
+ @usage = text unless text.nil?
45
+ @usage
46
+ end
47
+
48
+ # Set command usage.
49
+ def usage=(text)
50
+ @usage = text
51
+ end
52
+
53
+ # Get or set command description.
54
+ def description(text=nil)
55
+ @description = text unless text.nil?
56
+ end
57
+ alias_method :header, :description
58
+
59
+ # Get or set command help footer.
60
+ def footer(text=nil)
61
+ @footer = text unless text.nil?
62
+ @footer
63
+ end
64
+
65
+ # Set comamnd help footer.
66
+ def footer=(text)
67
+ @footer = text
68
+ end
69
+
70
+ # Set description of an option.
71
+ def option(name, description)
72
+ @options[name.to_s] = description
73
+ end
74
+
75
+ # Set desciption of a subcommand.
76
+ def subcommand(name, description)
77
+ @subcmds[name.to_s] = description
78
+ end
79
+
80
+ #alias_method :inspect, :to_s
81
+
82
+ # Show help.
83
+ #
84
+ # @todo man-pages will probably fail on Windows
85
+ def show_help(hint=nil)
86
+ if file = manpage(hint)
87
+ system "man #{file}"
88
+ else
89
+ puts text
90
+ end
91
+ end
92
+
93
+ # M A N P A G E
94
+
95
+ # Get man-page if there is one.
96
+ def manpage(hint=nil)
97
+ @manpage ||= (
98
+ man = []
99
+ dir = @file ? File.dirname(@file) : nil
100
+ glob = "man/#{name}.1"
101
+
102
+ if hint
103
+ if File.exist?(hint)
104
+ return hint
105
+ elsif File.directory?(hint)
106
+ dir = hint
107
+ else
108
+ glob = hint if hint
109
+ end
110
+ end
111
+
112
+ if dir
113
+ while dir != '/'
114
+ man.concat(Dir[File.join(dir, glob)])
115
+ #man.concat(Dir[File.join(dir, "man/man1/#{name}.1")])
116
+ #man.concat(Dir[File.join(dir, "man/#{name}.1.ronn")])
117
+ #man.concat(Dir[File.join(dir, "man/man1/#{name}.1")])
118
+ break unless man.empty?
119
+ dir = File.dirname(dir)
120
+ end
121
+ end
122
+
123
+ man.first
124
+ )
125
+ end
126
+
127
+ # H E L P T E X T
128
+
129
+ #
130
+ def to_s; text; end
131
+
132
+ #
133
+ def text(file=nil)
134
+ s = []
135
+ s << text_usage
136
+ s << text_description
137
+ s << text_subcommands
138
+ s << text_options
139
+ s << text_footer
140
+ s.compact.join("\n\n")
141
+ end
142
+
143
+ # Command usage.
144
+ def text_usage
145
+ usage
146
+ end
147
+
148
+ # TODO: Maybe default description should always come from `main`
149
+ # instead of the the class comment ?
150
+
151
+ # Description of command in printable form.
152
+ # But will return +nil+ if there is no description.
153
+ #
154
+ # @return [String,NilClass] command description
155
+ def text_description
156
+ if @description
157
+ @description
158
+ elsif @file
159
+ Source.get_above_comment(@file, @line)
160
+ elsif main = method_list.find{ |m| m.name == 'main' }
161
+ main.comment
162
+ else
163
+ nil
164
+ end
165
+ end
166
+
167
+ # List of subcommands converted to a printable string.
168
+ # But will return +nil+ if there are no subcommands.
169
+ #
170
+ # @return [String,NilClass] subcommand list text
171
+ def text_subcommands
172
+ commands = @cli_class.subcommands
173
+ s = []
174
+ if !commands.empty?
175
+ s << "COMMANDS"
176
+ commands.each do |cmd, klass|
177
+ desc = klass.help.text_description.to_s.split("\n").first
178
+ s << " %-17s %s" % [cmd, desc]
179
+ end
180
+ end
181
+ return nil if s.empty?
182
+ return s.join("\n")
183
+ end
184
+
185
+ # List of options coverted to a printable string.
186
+ # But will return +nil+ if there are no options.
187
+ #
188
+ # @return [String,NilClass] option list text
189
+ def text_options
190
+ option_list.each do |opt|
191
+ if @options.key?(opt.name)
192
+ opt.description = @options[opt.name]
193
+ end
194
+ end
195
+
196
+ max = option_list.map{ |opt| opt.usage.size }.max + 2
197
+
198
+ s = []
199
+ s << "OPTIONS"
200
+ option_list.each do |opt|
201
+ mark = (opt.name.size == 1 ? ' -' : '--')
202
+ s << " #{mark}%-#{max}s %s" % [opt.usage, opt.description]
203
+ end
204
+ s.join("\n")
205
+ end
206
+
207
+ #
208
+ def text_footer
209
+ footer
210
+ end
211
+
212
+ #
213
+ #def text_common_options
214
+ #s << "\nCOMMON OPTIONS:\n\n"
215
+ #global_options.each do |(name, meth)|
216
+ # if name.size == 1
217
+ # s << " -%-15s %s\n" % [name, descriptions[meth]]
218
+ # else
219
+ # s << " --%-15s %s\n" % [name, descriptions[meth]]
220
+ # end
221
+ #end
222
+ #end
223
+
224
+ #
225
+ def option_list
226
+ @option_list ||= (
227
+ method_list.map do |meth|
228
+ case meth.name
229
+ when /^(.*?)[\!\=]$/
230
+ Option.new(meth)
231
+ end
232
+ end.compact.sort
233
+ )
234
+ end
235
+
236
+ private
237
+
238
+ # Produce a list relavent methods.
239
+ #
240
+ def method_list
241
+ list = []
242
+ methods = []
243
+ stop_at = cli_class.ancestors.index(CLI::Base) || -1
244
+ ancestors = cli_class.ancestors[0...stop_at]
245
+ ancestors.reverse_each do |a|
246
+ a.instance_methods(false).each do |m|
247
+ list << cli_class.instance_method(m)
248
+ end
249
+ end
250
+ list
251
+ end
252
+
253
+ # Encapsualtes a command line option.
254
+ class Option
255
+ def initialize(method)
256
+ @method = method
257
+ end
258
+
259
+ def name
260
+ @method.name.to_s.chomp('!').chomp('=')
261
+ end
262
+
263
+ def comment
264
+ @method.comment
265
+ end
266
+
267
+ def description
268
+ @description ||= comment.split("\n").first
269
+ end
270
+
271
+ # Set description manually.
272
+ def description=(desc)
273
+ @description = desc
274
+ end
275
+
276
+ def parameter
277
+ param = @method.parameters.first
278
+ param.last if param
279
+ end
280
+
281
+ def usage
282
+ if parameter
283
+ "#{name}=#{parameter.to_s.upcase}"
284
+ else
285
+ "#{name}"
286
+ end
287
+ end
288
+
289
+ def <=>(other)
290
+ self.name <=> other.name
291
+ end
292
+ end
293
+ end
294
+
295
+ end
data/lib/cli/parser.rb ADDED
@@ -0,0 +1,211 @@
1
+ module CLI
2
+
3
+ # The Parse class does all the heavy lifting for CLI::Base.
4
+ #
5
+ class Parser
6
+
7
+ #
8
+ # @param cli_class [CLI::Base] command class
9
+ #
10
+ def initialize(cli_class)
11
+ @cli_class = cli_class
12
+ end
13
+
14
+ attr :cli_class
15
+
16
+ # Parse command-line.
17
+ #
18
+ # @param argv [Array,String] command-line arguments
19
+ #
20
+ def parse(argv=ARGV)
21
+ argv = parse_shellwords(argv)
22
+
23
+ cmd, argv = parse_subcommand(argv)
24
+ cli = cmd.new
25
+ args = parse_arguments(cli, argv)
26
+
27
+ return cli, args
28
+ end
29
+
30
+ # Make sure arguments are an array. If argv is a String,
31
+ # then parse using Shellwords module.
32
+ #
33
+ # @param argv [Array,String] commmand-line arguments
34
+ def parse_shellwords(argv)
35
+ if String === argv
36
+ require 'shellwords'
37
+ argv = Shellwords.shellwords(argv)
38
+ end
39
+ argv.to_a
40
+ end
41
+
42
+ #
43
+ #
44
+ #
45
+ def parse_subcommand(argv)
46
+ cmd = cli_class
47
+ arg = argv.first
48
+
49
+ while c = cmd.subcommands[arg]
50
+ cmd = c
51
+ argv.shift
52
+ arg = argv.first
53
+ end
54
+
55
+ return cmd, argv
56
+ end
57
+
58
+ #
59
+ # Parse command line options based on given object.
60
+ #
61
+ # @param obj [Object] basis for command-line parsing
62
+ # @param argv [Array,String] command-line arguments
63
+ # @param args [Array] pre-seeded arguments to add to
64
+ #
65
+ # @return [Array] parsed arguments
66
+ #
67
+ def parse_arguments(obj, argv, args=[])
68
+ case argv
69
+ when String
70
+ require 'shellwords'
71
+ argv = Shellwords.shellwords(argv)
72
+ #else
73
+ # argv = argv.dup
74
+ end
75
+
76
+ #subc = nil
77
+ #@args = [] #opts, i = {}, 0
78
+
79
+ while argv.size > 0
80
+ case arg = argv.shift
81
+ when /=/
82
+ parse_equal(obj, arg, argv, args)
83
+ when /^--/
84
+ parse_long(obj, arg, argv, args)
85
+ when /^-/
86
+ parse_flags(obj, arg, argv, args)
87
+ else
88
+ #if CLI::Base === obj
89
+ # if cmd_class = obj.class.subcommands[arg]
90
+ # cmd = cmd_class.new(obj)
91
+ # subc = cmd
92
+ # parse(cmd, argv, args)
93
+ # else
94
+ args << arg
95
+ # end
96
+ #end
97
+ end
98
+ end
99
+
100
+ return args
101
+ end
102
+
103
+ #
104
+ # Parse equal setting comman-line option.
105
+ #
106
+ def parse_equal(obj, opt, argv, args)
107
+ if md = /^[-]*(.*?)=(.*?)$/.match(opt)
108
+ x, v = md[1], md[2]
109
+ else
110
+ raise ArgumentError, "#{x}"
111
+ end
112
+ if obj.respond_to?("#{x}=")
113
+ v = true if v == 'true' # yes or on ?
114
+ v = false if v == 'false' # no or off ?
115
+ obj.send("#{x}=", v)
116
+ else
117
+ obj.option_missing(x, v) # argv?
118
+ end
119
+ end
120
+
121
+ #
122
+ # Parse double-dash command-line option.
123
+ #
124
+ def parse_long(obj, opt, argv, args)
125
+ x = opt.sub(/^\-+/, '') # remove '--'
126
+ if obj.respond_to?("#{x}=")
127
+ m = obj.method("#{x}=")
128
+ if obj.respond_to?("#{x}?")
129
+ m.call(true)
130
+ else
131
+ invoke(obj, m, argv)
132
+ end
133
+ elsif obj.respond_to?("#{x}!")
134
+ invoke(obj, "#{x}!", argv)
135
+ else
136
+ obj.option_missing(x, argv)
137
+ end
138
+ end
139
+
140
+ #
141
+ # Parse single-dash command-line option.
142
+ #
143
+ # TODO: This needs some thought concerning character spliting and arguments.
144
+ def parse_flags(obj, opt, argv, args)
145
+ x = opt[1..-1]
146
+ c = 0
147
+ x.split(//).each do |k|
148
+ if obj.respond_to?("#{k}=")
149
+ m = obj.method("#{k}=")
150
+ if obj.respond_to?("#{x}?")
151
+ m.call(true)
152
+ else
153
+ invoke(obj, m, argv) #m.call(argv.shift)
154
+ end
155
+ elsif obj.respond_to?("#{k}!")
156
+ invoke(obj, "#{k}!", argv)
157
+ else
158
+ long = find_longer_option(obj, k)
159
+ if long
160
+ if long.end_with?('=') && obj.respond_to?(long.chomp('=')+'?')
161
+ invoke(obj, long, [true])
162
+ else
163
+ invoke(obj, long, argv)
164
+ end
165
+ else
166
+ obj.option_missing(x, argv)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ #
173
+ #
174
+ # TODO: Sort alphabetically?
175
+ def find_longer_option(obj, char)
176
+ meths = obj.methods.map{ |m| m.to_s }
177
+ meths = meths.select do |m|
178
+ m.start_with?(char) and (m.end_with?('=') or m.end_with?('!'))
179
+ end
180
+ meths.first
181
+ end
182
+
183
+ #
184
+ #
185
+ def invoke(obj, meth, argv)
186
+ m = Method === meth ? meth : obj.method(meth)
187
+ a = []
188
+ m.arity.abs.times{ a << argv.shift }
189
+ m.call(*a)
190
+ end
191
+
192
+ # Index of subcommands.
193
+ #
194
+ # @return [Hash] name mapped to subcommnd class
195
+ def subcommands
196
+ @subcommands ||= (
197
+ consts = @cli_class.constants - @cli_class.superclass.constants
198
+ consts.inject({}) do |h, c|
199
+ c = @cli_class.const_get(c)
200
+ if Class === c && CLI::Base > c
201
+ n = c.name.split('::').last.downcase
202
+ h[n] = c
203
+ end
204
+ h
205
+ end
206
+ )
207
+ end
208
+
209
+ end
210
+
211
+ end
data/lib/cli/source.rb ADDED
@@ -0,0 +1,81 @@
1
+ # Manage source lookup.
2
+ module Source
3
+ extend self
4
+
5
+ # Read and cache file.
6
+ #
7
+ # @param file [String] filename, should be full path
8
+ #
9
+ # @return [Array] file content in array of lines
10
+ def read(file)
11
+ @read ||= {}
12
+ @read[file] ||= File.readlines(file)
13
+ end
14
+
15
+ # Get comment from file searching up from given line number.
16
+ #
17
+ # @param file [String] filename, should be full path
18
+ # @param line [Integer] line number in file
19
+ #
20
+ def get_above_comment(file, line)
21
+ get_above_comment_lines(file, line).join("\n").strip
22
+ end
23
+
24
+ # Get comment from file searching up from given line number.
25
+ #
26
+ # @param file [String] filename, should be full path
27
+ # @param line [Integer] line number in file
28
+ #
29
+ def get_above_comment_lines(file, line)
30
+ text = read(file)
31
+ index = line - 1
32
+ while index >= 0 && text[index] !~ /^\s*\#/
33
+ return [] if text[index] =~ /^\s*end/
34
+ index -= 1
35
+ end
36
+ rindex = index
37
+ while text[index] =~ /^\s*\#/
38
+ index -= 1
39
+ end
40
+ result = text[index..rindex]
41
+ result = result.map{ |s| s.strip }
42
+ result = result.reject{ |s| s[0,1] != '#' }
43
+ result = result.map{ |s| s.sub(/^#/,'').strip }
44
+ #result = result.reject{ |s| s == "" }
45
+ result
46
+ end
47
+
48
+ # Get comment from file searching down from given line number.
49
+ #
50
+ # @param file [String] filename, should be full path
51
+ # @param line [Integer] line number in file
52
+ #
53
+ def get_following_comment(file, line)
54
+ get_following_comment_lines(file, line).join("\n").strip
55
+ end
56
+
57
+ # Get comment from file searching down from given line number.
58
+ #
59
+ # @param file [String] filename, should be full path
60
+ # @param line [Integer] line number in file
61
+ #
62
+ def get_following_comment_lines(file, line)
63
+ text = read(file)
64
+ index = line || 0
65
+ while text[index] !~ /^\s*\#/
66
+ return nil if text[index] =~ /^\s*(class|module)/
67
+ index += 1
68
+ end
69
+ rindex = index
70
+ while text[rindex] =~ /^\s*\#/
71
+ rindex += 1
72
+ end
73
+ result = text[index..(rindex-2)]
74
+ result = result.map{ |s| s.strip }
75
+ result = result.reject{ |s| s[0,1] != '#' }
76
+ result = result.map{ |s| s.sub(/^#/,'').strip }
77
+ result.join("\n").strip
78
+ end
79
+
80
+ end
81
+
data/lib/cli/utils.rb ADDED
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module CLI
4
+
5
+ # Some handy-dandy CLI utility methods.
6
+ #
7
+ module Utils
8
+ extend self
9
+
10
+ # TODO: Maybe #ask chould serve all purposes depending on degfault?
11
+ # e.g. `ask?("ok?", default=>true)`, would be same as `yes?("ok?")`.
12
+
13
+ # Strings to interprest as boolean values.
14
+ BOOLEAN_MAP = {"y"=>true, "yes"=>true, "n"=>false, "no"=>false}
15
+
16
+ # Query the user for a yes/no answer, defaulting to yes.
17
+ def yes?(question, options={})
18
+ print "#{question} [Y/n] "
19
+ input = STDIN.readline.chomp.downcase
20
+ BOOLEAN_MAP[input] || true
21
+ end
22
+
23
+ # Query the user for a yes/no answer, defaulting to no.
24
+ def no?(question, options={})
25
+ print "#{question} [y/N] "
26
+ input = STDIN.readline.chomp.downcase
27
+ BOOLEAN_MAP[input] || false
28
+ end
29
+
30
+ # Query the user for an answer.
31
+ def ask(question, options={})
32
+ print "#{question} [default: #{options[:default]}] "
33
+ reply = STDIN.readline.chomp
34
+ if reply.empty?
35
+ options[:default]
36
+ else
37
+ reply
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,5 @@
1
+ module CLI
2
+ class Base
3
+ VERSION = '0.5.1'
4
+ end
5
+ end
data/lib/cli_base.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'cli/base'
2
+
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cli_base
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Sawyer
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '13'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '13'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '5'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5'
40
+ description: Think of CLI::Base as a COM, a Commandline Object Mapper, in much the
41
+ same way that ActiveRecord::Base is an ORM. A subclass of CLI::Base can define a
42
+ complete command line tool using nothing more than Ruby method definitions.
43
+ email:
44
+ - transfire@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - HISTORY.md
50
+ - README.md
51
+ - lib/cli/base.rb
52
+ - lib/cli/config.rb
53
+ - lib/cli/core_ext.rb
54
+ - lib/cli/errors.rb
55
+ - lib/cli/help.rb
56
+ - lib/cli/parser.rb
57
+ - lib/cli/source.rb
58
+ - lib/cli/utils.rb
59
+ - lib/cli/version.rb
60
+ - lib/cli_base.rb
61
+ homepage: https://github.com/rubyworks/cli_base
62
+ licenses:
63
+ - BSD-2-Clause
64
+ metadata: {}
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '3.1'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.9
80
+ specification_version: 4
81
+ summary: Command line tools, meet your Executioner!
82
+ test_files: []