executable 1.1.0 → 1.2.0

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