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.
- data/.ruby +61 -0
- data/.yardopts +7 -0
- data/COPYING.rdoc +35 -0
- data/DEMO.rdoc +568 -0
- data/HISTORY.rdoc +55 -0
- data/README.rdoc +101 -51
- data/Schedule.reap +17 -0
- data/demo/00_introduction.rdoc +6 -0
- data/demo/01_single_command.rdoc +44 -0
- data/demo/02_multiple_commands.rdoc +125 -0
- data/demo/03_help_text.rdoc +109 -0
- data/demo/04_manpage.rdoc +14 -0
- data/demo/05_optparse_example.rdoc +152 -0
- data/demo/06_delegate_example.rdoc +40 -0
- data/demo/07_command_methods.rdoc +36 -0
- data/demo/08_dispatach.rdoc +29 -0
- data/demo/applique/ae.rb +1 -0
- data/demo/applique/compare.rb +4 -0
- data/demo/applique/exec.rb +1 -0
- data/demo/samples/bin/hello +31 -0
- data/demo/samples/man/hello.1 +22 -0
- data/demo/samples/man/hello.1.html +102 -0
- data/demo/samples/man/hello.1.ronn +19 -0
- data/lib/executable.rb +67 -128
- data/lib/executable/core_ext.rb +102 -0
- data/lib/executable/dispatch.rb +30 -0
- data/lib/executable/domain.rb +106 -0
- data/lib/executable/errors.rb +22 -0
- data/lib/executable/help.rb +430 -0
- data/lib/executable/parser.rb +208 -0
- data/lib/executable/utils.rb +41 -0
- data/lib/executable/version.rb +23 -0
- data/meta/authors +2 -0
- data/meta/copyrights +3 -0
- data/meta/created +1 -0
- data/meta/description +6 -0
- data/meta/name +1 -0
- data/meta/organization +1 -0
- data/meta/repositories +2 -0
- data/meta/requirements +6 -0
- data/meta/resources +7 -0
- data/meta/summary +1 -0
- data/meta/version +1 -0
- data/test/test_executable.rb +40 -19
- metadata +124 -68
- data/History.rdoc +0 -35
- data/NOTICE.rdoc +0 -23
- data/Profile +0 -30
- data/Version +0 -1
- 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
|