executable 1.1.0 → 1.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.
Files changed (50) hide show
  1. data/.ruby +61 -0
  2. data/.yardopts +7 -0
  3. data/COPYING.rdoc +35 -0
  4. data/DEMO.rdoc +568 -0
  5. data/HISTORY.rdoc +55 -0
  6. data/README.rdoc +101 -51
  7. data/Schedule.reap +17 -0
  8. data/demo/00_introduction.rdoc +6 -0
  9. data/demo/01_single_command.rdoc +44 -0
  10. data/demo/02_multiple_commands.rdoc +125 -0
  11. data/demo/03_help_text.rdoc +109 -0
  12. data/demo/04_manpage.rdoc +14 -0
  13. data/demo/05_optparse_example.rdoc +152 -0
  14. data/demo/06_delegate_example.rdoc +40 -0
  15. data/demo/07_command_methods.rdoc +36 -0
  16. data/demo/08_dispatach.rdoc +29 -0
  17. data/demo/applique/ae.rb +1 -0
  18. data/demo/applique/compare.rb +4 -0
  19. data/demo/applique/exec.rb +1 -0
  20. data/demo/samples/bin/hello +31 -0
  21. data/demo/samples/man/hello.1 +22 -0
  22. data/demo/samples/man/hello.1.html +102 -0
  23. data/demo/samples/man/hello.1.ronn +19 -0
  24. data/lib/executable.rb +67 -128
  25. data/lib/executable/core_ext.rb +102 -0
  26. data/lib/executable/dispatch.rb +30 -0
  27. data/lib/executable/domain.rb +106 -0
  28. data/lib/executable/errors.rb +22 -0
  29. data/lib/executable/help.rb +430 -0
  30. data/lib/executable/parser.rb +208 -0
  31. data/lib/executable/utils.rb +41 -0
  32. data/lib/executable/version.rb +23 -0
  33. data/meta/authors +2 -0
  34. data/meta/copyrights +3 -0
  35. data/meta/created +1 -0
  36. data/meta/description +6 -0
  37. data/meta/name +1 -0
  38. data/meta/organization +1 -0
  39. data/meta/repositories +2 -0
  40. data/meta/requirements +6 -0
  41. data/meta/resources +7 -0
  42. data/meta/summary +1 -0
  43. data/meta/version +1 -0
  44. data/test/test_executable.rb +40 -19
  45. metadata +124 -68
  46. data/History.rdoc +0 -35
  47. data/NOTICE.rdoc +0 -23
  48. data/Profile +0 -30
  49. data/Version +0 -1
  50. data/meta/license/Apache2.txt +0 -177
@@ -0,0 +1,106 @@
1
+ module Executable
2
+
3
+ #
4
+ module Domain
5
+
6
+ #
7
+ # Helper method for creating switch attributes.
8
+ #
9
+ # This is equivalent to:
10
+ #
11
+ # def name=(val)
12
+ # @name = val
13
+ # end
14
+ #
15
+ # def name?
16
+ # @name
17
+ # end
18
+ #
19
+ #
20
+ def attr_switch(name)
21
+ attr_writer name
22
+ module_eval %{
23
+ def #{name}?
24
+ @#{name}
25
+ end
26
+ }
27
+ end
28
+
29
+ #
30
+ # Inspection method. This must be redefined b/c #to_s is overridden.
31
+ #
32
+ def inspect
33
+ name
34
+ end
35
+
36
+ #
37
+ # Returns `help.to_s`.
38
+ #
39
+ def to_s
40
+ cli.to_s
41
+ end
42
+
43
+ #
44
+ # Interface with cooresponding cli/help object.
45
+ #
46
+ def help
47
+ @help ||= Help.new(self)
48
+ end
49
+
50
+ #
51
+ # Interface with cooresponding cli/help object.
52
+ #
53
+ alias_method :cli, :help
54
+
55
+ #
56
+ # Execute the command.
57
+ #
58
+ # @param argv [Array] command-line arguments
59
+ #
60
+ def execute(argv=ARGV)
61
+ cli, args = parser.parse(argv)
62
+ cli.call(*args)
63
+ return cli
64
+ end
65
+
66
+ #
67
+ # Executables don't run, they execute! But...
68
+ #
69
+ alias_method :run, :execute
70
+
71
+ #
72
+ #
73
+ # @return [Array<Executable,Array>] The executable and call arguments.
74
+ def parse(argv)
75
+ parser.parse(argv)
76
+ end
77
+
78
+ #
79
+ # The parser for this command.
80
+ #
81
+ def parser
82
+ @parser ||= Parser.new(self)
83
+ end
84
+
85
+ #
86
+ # Index of subcommands.
87
+ #
88
+ # @return [Hash] name mapped to subcommnd class
89
+ #
90
+ def subcommands
91
+ @subcommands ||= (
92
+ consts = constants - superclass.constants
93
+ consts.inject({}) do |h, c|
94
+ c = const_get(c)
95
+ if Class === c && Executable > c
96
+ n = c.name.split('::').last.downcase
97
+ h[n] = c
98
+ end
99
+ h
100
+ end
101
+ )
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,22 @@
1
+ module Executable
2
+
3
+ class NoOptionError < ::NoMethodError # ArgumentError ?
4
+ def initialize(name, *arg)
5
+ super("unknown option -- #{name}", name, *args)
6
+ end
7
+ end
8
+
9
+ #class NoCommandError < ::NoMethodError
10
+ # def initialize(name, *args)
11
+ # super("unknown command -- #{name}", name, *args)
12
+ # end
13
+ #end
14
+
15
+ class NoCommandError < ::NoMethodError
16
+ def initialize(*args)
17
+ super("missing command", *args)
18
+ end
19
+ end
20
+
21
+ end
22
+
@@ -0,0 +1,430 @@
1
+ require 'executable/core_ext'
2
+
3
+ module Executable
4
+
5
+ # Encpsulates command help for defining and displaying well formated help
6
+ # output in plain text, markdown or via manpages if found.
7
+ #
8
+ # TODO: Currently doesn't hande aliases/shortcuts well and simply
9
+ # lists them as separate entries.
10
+ #
11
+ # Creating text help in the fly is fine for personal projects, but
12
+ # for production app, ideally you want to have man pages. You can
13
+ # use the #markdown method to generate `.ronn` files and use the
14
+ # ronn tool to build manpages for your project. There is also the
15
+ # binman and md2man projects which can be used similarly.
16
+ #
17
+ class Help
18
+
19
+ #
20
+ def self.section(name, &default)
21
+ define_method("default_#{name}", &default)
22
+ class_eval %{
23
+ def #{name}(text=nil)
24
+ @#{name} = text.to_s unless text.nil?
25
+ @#{name} ||= default_#{name}
26
+ end
27
+ def #{name}=(text)
28
+ @#{name} = text.to_s
29
+ end
30
+ }
31
+ end
32
+
33
+ # Setup new help object.
34
+ def initialize(cli_class)
35
+ @cli_class = cli_class
36
+
37
+ @name = nil
38
+ @usage = nil
39
+ @decription = nil
40
+ @copying = nil
41
+ @see_also = nil
42
+
43
+ @options = {}
44
+ @subcmds = {}
45
+ end
46
+
47
+ #
48
+ alias_method :inspect, :to_s
49
+
50
+ #
51
+ # The Executable subclass to which this help applies.
52
+ #
53
+ attr :cli_class
54
+
55
+ #
56
+ # Get or set command name.
57
+ #
58
+ # By default the name is assumed to be the class name, substituting
59
+ # dashes for double colons.
60
+ #
61
+ # @todo Should this instead default to `File.basename($0)` ?
62
+ #
63
+ # @method name(text=nil)
64
+ #
65
+ section(:name) do
66
+ str = cli_class.name.sub(/\#\<.*?\>\:\:/,'').downcase.gsub('::','-')
67
+ str.chomp!('command')
68
+ str.chomp!('cli')
69
+ str
70
+ end
71
+
72
+ #
73
+ # Get or set command usage string.
74
+ #
75
+ # @method usage(text=nil)
76
+ #
77
+ section(:usage) do
78
+ "Usage: " + name + ' [options...] [subcommand]'
79
+ end
80
+
81
+ #
82
+ # Get or set command description.
83
+ #
84
+ # @method description(text=nil)
85
+ #
86
+ section(:description) do
87
+ nil
88
+ end
89
+
90
+ #
91
+ # Get or set copyright text.
92
+ #
93
+ # @method copyright(text=nil)
94
+ #
95
+ section(:copyright) do
96
+ 'Copyright (c) ' + Time.now.strftime('%Y')
97
+ end
98
+
99
+ #
100
+ # Get or set "see also" text.
101
+ #
102
+ # @method see_also(text=nil)
103
+ #
104
+ section(:see_also) do
105
+ nil
106
+ end
107
+
108
+ #
109
+ # Set description of an option.
110
+ #
111
+ def option(name, description)
112
+ @options[name.to_s] = description
113
+ end
114
+
115
+ #
116
+ # Set desciption of a subcommand.
117
+ #
118
+ def subcommand(name, description)
119
+ @subcmds[name.to_s] = description
120
+ end
121
+
122
+ #
123
+ # Show help.
124
+ #
125
+ # @todo man-pages will probably fail on Windows
126
+ #
127
+ def show_help(hint=nil)
128
+ if file = manpage(hint)
129
+ show_manpage(file)
130
+ else
131
+ puts self
132
+ end
133
+ end
134
+
135
+ #
136
+ def show_manpage(file)
137
+ #exec "man #{file}"
138
+ system "man #{file}"
139
+ end
140
+
141
+ #
142
+ # Get man-page if there is one.
143
+ #
144
+ def manpage(dir=nil)
145
+ @manpage ||= (
146
+ man = []
147
+ dir = nil
148
+
149
+ if dir
150
+ raise unless File.directory?(dir)
151
+ end
152
+
153
+ if !dir && call_method
154
+ file, line = call_method.source_location
155
+ dir = File.dirname(file)
156
+ end
157
+
158
+ if dir
159
+ glob = "man/{man1/,}#{name}.1"
160
+ lookup(glob, dir)
161
+ else
162
+ nil
163
+ end
164
+ )
165
+ end
166
+
167
+ #
168
+ # Render help text to a given +format+. If no format it given
169
+ # then renders to plain text.
170
+ #
171
+ def to_s(format=nil)
172
+ case format
173
+ when :markdown, :md
174
+ markdown
175
+ else
176
+ text
177
+ end
178
+ end
179
+
180
+ #
181
+ # Generate plain text output.
182
+ #
183
+ def text
184
+ commands = text_subcommands
185
+ options = text_options
186
+
187
+ s = []
188
+
189
+ s << usage
190
+ s << text_description
191
+
192
+ if !commands.empty?
193
+ s << "COMMANDS\n" + commands.map{ |cmd, desc|
194
+ " %-17s %s" % [cmd, desc]
195
+ }.join("\n")
196
+ end
197
+
198
+ if !options.empty?
199
+ s << "OPTIONS\n" + options.map{ |max, opt|
200
+ " %2s%-#{max}s %s" % [opt.mark, opt.usage, opt.description]
201
+ }.join("\n")
202
+ end
203
+
204
+ s << copyright
205
+ s << see_also
206
+
207
+ s.compact.join("\n\n")
208
+ end
209
+
210
+ #
211
+ alias_method :txt, :text
212
+
213
+ #
214
+ # Generate a RONN-style Markdown.
215
+ #
216
+ def markdown
217
+ commands = text_subcommands
218
+ options = text_options
219
+
220
+ s = []
221
+
222
+ h = "#{name}(1) - #{text_description}"
223
+ s << h + "\n" + ("=" * h.size)
224
+
225
+ s << "## SYNOPSIS"
226
+ s << "`" + name + "` [options...] [subcommand]"
227
+
228
+ s << "## DESCRIPTION"
229
+ s << text_description
230
+
231
+ if !commands.empty?
232
+ s << "## COMMANDS"
233
+ s << commands.map{ |cmd, desc|
234
+ " * `%s:`\n %s" % [cmd, desc]
235
+ }.join("\n")
236
+ end
237
+
238
+ if !options.empty?
239
+ s << "## OPTIONS"
240
+ s << options.map{ |max, opt|
241
+ " * `#{opt.mark}%s`:\n %s" % [opt.usage, opt.description]
242
+ }.join("\n\n")
243
+ end
244
+
245
+ if copyright && !copyright.empty?
246
+ s << "## COPYRIGHT"
247
+ s << copyright
248
+ end
249
+
250
+ if see_also && !see_also.empty?
251
+ s << "## SEE ALSO"
252
+ s << see_also
253
+ end
254
+
255
+ s.compact.join("\n\n")
256
+ end
257
+
258
+ #
259
+ alias_method :md, :markdown
260
+
261
+ #
262
+ # Description of command in printable form.
263
+ # But will return +nil+ if there is no description.
264
+ #
265
+ # @return [String,NilClass] command description
266
+ #
267
+ def text_description
268
+ return description if description
269
+ #return Source.get_above_comment(@file, @line) if @file
270
+
271
+ call_method ? call_method.comment : nil
272
+ end
273
+
274
+ #
275
+ # List of subcommands converted to a printable string.
276
+ # But will return +nil+ if there are no subcommands.
277
+ #
278
+ # @return [String,NilClass] subcommand list text
279
+ #
280
+ def text_subcommands
281
+ commands = @cli_class.subcommands
282
+ commands.map do |cmd, klass|
283
+ desc = klass.help.text_description.to_s.split("\n").first
284
+ [cmd, desc]
285
+ end
286
+ end
287
+
288
+ #
289
+ # List of options coverted to a printable string.
290
+ # But will return +nil+ if there are no options.
291
+ #
292
+ # @return [Array<Fixnum, Options>] option list for output
293
+ #
294
+ def text_options
295
+ option_list.each do |opt|
296
+ if @options.key?(opt.name)
297
+ opt.description = @options[opt.name]
298
+ end
299
+ end
300
+
301
+ max = option_list.map{ |opt| opt.usage.size }.max.to_i + 2
302
+
303
+ option_list.map do |opt|
304
+ [max, opt]
305
+ end
306
+ end
307
+
308
+ #
309
+ #def text_common_options
310
+ #s << "\nCOMMON OPTIONS:\n\n"
311
+ #global_options.each do |(name, meth)|
312
+ # if name.size == 1
313
+ # s << " -%-15s %s\n" % [name, descriptions[meth]]
314
+ # else
315
+ # s << " --%-15s %s\n" % [name, descriptions[meth]]
316
+ # end
317
+ #end
318
+ #end
319
+
320
+ #
321
+ def option_list
322
+ @option_list ||= (
323
+ method_list.map do |meth|
324
+ case meth.name
325
+ when /^(.*?)[\!\=]$/
326
+ Option.new(meth)
327
+ end
328
+ end.compact.sort
329
+ )
330
+ end
331
+
332
+ private
333
+
334
+ #
335
+ def call_method
336
+ @call_method ||= method_list.find{ |m| m.name == :call }
337
+ end
338
+
339
+ #
340
+ # Produce a list relavent methods.
341
+ #
342
+ def method_list
343
+ list = []
344
+ methods = []
345
+ stop_at = cli_class.ancestors.index(Executable::Command) ||
346
+ cli_class.ancestors.index(Executable) ||
347
+ -1
348
+ ancestors = cli_class.ancestors[0...stop_at]
349
+ ancestors.reverse_each do |a|
350
+ a.instance_methods(false).each do |m|
351
+ list << cli_class.instance_method(m)
352
+ end
353
+ end
354
+ list
355
+ end
356
+
357
+ #
358
+ #
359
+ #
360
+ def lookup(glob, dir)
361
+ dir = File.expand_path(dir)
362
+ root = '/'
363
+ home = File.expand_path('~')
364
+ list = []
365
+
366
+ while dir != home && dir != root
367
+ list.concat(Dir.glob(File.join(dir, glob)))
368
+ break unless list.empty?
369
+ dir = File.dirname(dir)
370
+ end
371
+
372
+ list.first
373
+ end
374
+
375
+ # Encapsualtes a command line option.
376
+ #
377
+ class Option
378
+ def initialize(method)
379
+ @method = method
380
+ end
381
+
382
+ def name
383
+ @method.name.to_s.chomp('!').chomp('=')
384
+ end
385
+
386
+ def comment
387
+ @method.comment
388
+ end
389
+
390
+ def description
391
+ @description ||= comment.split("\n").first
392
+ end
393
+
394
+ # Set description manually.
395
+ def description=(desc)
396
+ @description = desc
397
+ end
398
+
399
+ def parameter
400
+ begin
401
+ @method.owner.instance_method(@method.name.to_s.chomp('=') + '?')
402
+ false
403
+ rescue
404
+ param = @method.parameters.first
405
+ param.last if param
406
+ end
407
+ end
408
+
409
+ #
410
+ def usage
411
+ if parameter
412
+ "#{name}=#{parameter.to_s.upcase}"
413
+ else
414
+ "#{name}"
415
+ end
416
+ end
417
+
418
+ def <=>(other)
419
+ self.name <=> other.name
420
+ end
421
+
422
+ #
423
+ def mark
424
+ name.to_s.size == 1 ? '-' : '--'
425
+ end
426
+
427
+ end
428
+ end
429
+
430
+ end