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,102 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv='content-type' value='text/html;charset=utf8'>
5
+ <meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
6
+ <title>hellocmd(1) - Say hello.</title>
7
+ <style type='text/css' media='all'>
8
+ /* style: man */
9
+ body#manpage {margin:0}
10
+ .mp {max-width:100ex;padding:0 9ex 1ex 4ex}
11
+ .mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
12
+ .mp h2 {margin:10px 0 0 0}
13
+ .mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
14
+ .mp h3 {margin:0 0 0 4ex}
15
+ .mp dt {margin:0;clear:left}
16
+ .mp dt.flush {float:left;width:8ex}
17
+ .mp dd {margin:0 0 0 9ex}
18
+ .mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
19
+ .mp pre {margin-bottom:20px}
20
+ .mp pre+h2,.mp pre+h3 {margin-top:22px}
21
+ .mp h2+pre,.mp h3+pre {margin-top:5px}
22
+ .mp img {display:block;margin:auto}
23
+ .mp h1.man-title {display:none}
24
+ .mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
25
+ .mp h2 {font-size:16px;line-height:1.25}
26
+ .mp h1 {font-size:20px;line-height:2}
27
+ .mp {text-align:justify;background:#fff}
28
+ .mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
29
+ .mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
30
+ .mp u {text-decoration:underline}
31
+ .mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
32
+ .mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
33
+ .mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
34
+ .mp b.man-ref {font-weight:normal;color:#434241}
35
+ .mp pre {padding:0 4ex}
36
+ .mp pre code {font-weight:normal;color:#434241}
37
+ .mp h2+pre,h3+pre {padding-left:0}
38
+ ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
39
+ ol.man-decor {width:100%}
40
+ ol.man-decor li.tl {text-align:left}
41
+ ol.man-decor li.tc {text-align:center;letter-spacing:4px}
42
+ ol.man-decor li.tr {text-align:right;float:right}
43
+ </style>
44
+ </head>
45
+ <!--
46
+ The following styles are deprecated and will be removed at some point:
47
+ div#man, div#man ol.man, div#man ol.head, div#man ol.man.
48
+
49
+ The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
50
+ .man-navigation should be used instead.
51
+ -->
52
+ <body id='manpage'>
53
+ <div class='mp' id='man'>
54
+
55
+ <div class='man-navigation' style='display:none'>
56
+ <a href="#NAME">NAME</a>
57
+ <a href="#SYNOPSIS">SYNOPSIS</a>
58
+ <a href="#DESCRIPTION">DESCRIPTION</a>
59
+ <a href="#OPTIONS">OPTIONS</a>
60
+ <a href="#COPYRIGHT">COPYRIGHT</a>
61
+ </div>
62
+
63
+ <ol class='man-decor man-head man head'>
64
+ <li class='tl'>hellocmd(1)</li>
65
+ <li class='tc'></li>
66
+ <li class='tr'>hellocmd(1)</li>
67
+ </ol>
68
+
69
+ <h2 id="NAME">NAME</h2>
70
+ <p class="man-name">
71
+ <code>hellocmd</code> - <span class="man-whatis">Say hello.</span>
72
+ </p>
73
+
74
+ <h2 id="SYNOPSIS">SYNOPSIS</h2>
75
+
76
+ <p><code>hellocmd</code> [options...] [subcommand]</p>
77
+
78
+ <h2 id="DESCRIPTION">DESCRIPTION</h2>
79
+
80
+ <p>Say hello.</p>
81
+
82
+ <h2 id="OPTIONS">OPTIONS</h2>
83
+
84
+ <dl>
85
+ <dt><code>--load=BOOL</code></dt><dd>Say it in uppercase?</dd>
86
+ </dl>
87
+
88
+
89
+ <h2 id="COPYRIGHT">COPYRIGHT</h2>
90
+
91
+ <p>Copyright (c) 2012</p>
92
+
93
+
94
+ <ol class='man-decor man-foot man foot'>
95
+ <li class='tl'></li>
96
+ <li class='tc'>January 2012</li>
97
+ <li class='tr'>hellocmd(1)</li>
98
+ </ol>
99
+
100
+ </div>
101
+ </body>
102
+ </html>
@@ -0,0 +1,19 @@
1
+ hellocmd(1) - Say hello.
2
+ ========================
3
+
4
+ ## SYNOPSIS
5
+
6
+ `hellocmd` [options...] [subcommand]
7
+
8
+ ## DESCRIPTION
9
+
10
+ Say hello.
11
+
12
+ ## OPTIONS
13
+
14
+ * `--load=BOOL`:
15
+ Say it in uppercase?
16
+
17
+ ## COPYRIGHT
18
+
19
+ Copyright (c) 2012
@@ -1,143 +1,82 @@
1
- # = Executable Mixin
2
- #
3
- # The Executable mixin is a very quick and and easy way to make almost
4
- # any class usable via a command line interface. It simply uses writer
5
- # methods as option setters, and the first command line argument as a
6
- # method to call with the subsequent arguments passed to the method.
7
- #
8
- # The only limitation of this approach is that non-boolean options must
9
- # be specified with `key=value` notation.
10
- #
11
- # class Example
12
- # include Executable
13
- #
14
- # attr_accessor :quiet
15
- #
16
- # def bread(*args)
17
- # ["bread", quiet, *args]
18
- # end
19
- #
20
- # def butter(*args)
21
- # ["butter", quiet, *args]
22
- # end
23
- # end
24
- #
25
- # ex = Example.new
26
- #
27
- # ex.execute!("butter yum")
28
- # => ["butter", nil, "yum"]
29
- #
30
- # ex.execute!("bread --quiet")
31
- # => ["butter", true]
32
- #
33
- # Executable also provides #option_missing, which you can overriden to provide
34
- # suitable results when a given command line option has no corresponding
35
- # writer method.
1
+ # encoding: utf-8
2
+
3
+ require 'executable/errors'
4
+ require 'executable/parser'
5
+ require 'executable/help'
6
+ require 'executable/utils'
7
+ require 'executable/domain'
8
+ require 'executable/dispatch'
9
+
10
+ # Executable is a mixin for creating robust, inheritable and
11
+ # reusable command line interfaces.
36
12
  #
37
13
  module Executable
38
14
 
39
- # Used the #excute! method to invoke the command.
40
- def execute!(argv=ARGV)
41
- Executable.execute(self, argv)
15
+ #
16
+ # When Exectuable is included into a class, the class is
17
+ # also extended by `Executable::Doamin`.
18
+ #
19
+ def self.included(base)
20
+ base.extend Domain
42
21
  end
43
22
 
44
- ## When no attribute write exists for an option that has been given on
45
- ## the command line #option_missing is called. Override #option_missing
46
- ## to handle these cases, if needed. Otherwise a NoMethodArgument will be
47
- ## raised. This callback method receives the name and value of the option.
48
- #def option_missing(opt, arg)
49
- # raise NoMethodError, "undefined option `#{opt}=' for #{self}"
50
- #end
51
-
52
- class << self
53
-
54
- # Process the arguments as an exectuable against the given object.
55
- def execute(obj, argv=ARGV)
56
- args = parse(obj, argv)
57
- subcmd = args.first
58
- if subcmd && !obj.respond_to?("#{subcmd}=")
59
- obj.send(*args)
60
- else
61
- obj.method_missing(*args)
62
- end
23
+ #
24
+ # Default initializer, simply takes a hash of settings
25
+ # to set attributes via writer methods. Not existnt
26
+ # attributes are simply ignored.
27
+ #
28
+ def initialize(settings={})
29
+ settings.each do |k,v|
30
+ __send__("#{k}=", v) if respond_to?("#{k}=")
63
31
  end
32
+ end
64
33
 
65
- # The original name for #execute.
66
- alias_method :run, :execute
34
+ public
67
35
 
68
- # Parse command line with respect to +obj+.
69
- def parse(obj, argv)
70
- case argv
71
- when String
72
- require 'shellwords'
73
- argv = Shellwords.shellwords(argv)
74
- else
75
- argv = argv.dup
76
- end
36
+ #
37
+ # Command invocation abstract method.
38
+ #
39
+ def call(*args)
40
+ #puts cli.show_help # TODO: show help instead of error ?
41
+ raise NotImplementedError
42
+ end
77
43
 
78
- argv = argv.dup
79
- args, opts, i = [], {}, 0
80
- while argv.size > 0
81
- case opt = argv.shift
82
- when /=/
83
- parse_equal(obj, opt, argv)
84
- when /^--/
85
- parse_option(obj, opt, argv)
86
- when /^-/
87
- parse_flags(obj, opt, argv)
88
- else
89
- args << opt
90
- end
91
- end
92
- return args
93
- end
44
+ #
45
+ # Convert Executable to Proc object.
46
+ #
47
+ def to_proc
48
+ lambda { |*args| call(*args) }
49
+ end
94
50
 
95
- # Parse a setting option.
96
- def parse_equal(obj, opt, argv)
97
- if md = /^[-]*(.*?)=(.*?)$/.match(opt)
98
- x, v = md[1], md[2]
99
- else
100
- raise ArgumentError, "#{x}"
101
- end
102
- # TODO: to_b if 'true' or 'false' ?
103
- #if obj.respond_to?("#{x}=")
104
- obj.send("#{x}=", v)
105
- #else
106
- # obj.option_missing(x, v)
107
- #end
108
- end
51
+ alias_method :inspect, :to_s
109
52
 
110
- # Parse a named boolean option.
111
- def parse_option(obj, opt, argv)
112
- x = opt.sub(/^--/, '')
113
- #if obj.respond_to?("#{x}=")
114
- obj.send("#{x}=", true)
115
- #else
116
- # obj.option_missing(x, true)
117
- #end
118
- end
53
+ #
54
+ # Output command line help.
55
+ #
56
+ def to_s
57
+ self.class.help.to_s # usage ?
58
+ end
119
59
 
120
- # Parse flags. Each character of a flag set is treated as a separate option.
121
- # For example:
122
- #
123
- # $ foo -abc
124
- #
125
- # Would be parsed the same as:
126
- #
127
- # $ foo -a -b -c
128
- #
129
- def parse_flags(obj, opt, args)
130
- x = opt.sub(/^-/, '')
131
- #c = 0
132
- x.split(//).each do |k|
133
- #if obj.respond_to?("#{k}=")
134
- obj.send("#{k}=", true)
135
- #else
136
- # obj.option_missing(x, true)
137
- #end
138
- end
139
- end
60
+ #
61
+ # Access to underlying Help instance.
62
+ #
63
+ def cli
64
+ self.class.cli
65
+ end
140
66
 
141
- end #class << self
67
+ #
68
+ # Override option_missing if needed. This receives the name of the option
69
+ # and the remaining arguments list. It must consume any arguments it uses
70
+ # from the begining of the list (i.e. in-place manipulation).
71
+ #
72
+ def option_missing(opt, argv)
73
+ raise NoOptionError, opt
74
+ end
75
+
76
+ # Base class alteranative ot using the mixin.
77
+ #
78
+ class Command
79
+ include Executable
80
+ end
142
81
 
143
82
  end
@@ -0,0 +1,102 @@
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
+
20
+ # Source lookup.
21
+ #
22
+ module Source
23
+ extend self
24
+
25
+ # Read and cache file.
26
+ #
27
+ # @param file [String] filename, should be full path
28
+ #
29
+ # @return [Array] file content in array of lines
30
+ def read(file)
31
+ @read ||= {}
32
+ @read[file] ||= File.readlines(file)
33
+ end
34
+
35
+ # Get comment from file searching up from given line number.
36
+ #
37
+ # @param file [String] filename, should be full path
38
+ # @param line [Integer] line number in file
39
+ #
40
+ def get_above_comment(file, line)
41
+ get_above_comment_lines(file, line).join("\n").strip
42
+ end
43
+
44
+ # Get comment from file searching up from given line number.
45
+ #
46
+ # @param file [String] filename, should be full path
47
+ # @param line [Integer] line number in file
48
+ #
49
+ def get_above_comment_lines(file, line)
50
+ text = read(file)
51
+ index = line - 1
52
+ while index >= 0 && text[index] !~ /^\s*\#/
53
+ return [] if text[index] =~ /^\s*end/
54
+ index -= 1
55
+ end
56
+ rindex = index
57
+ while text[index] =~ /^\s*\#/
58
+ index -= 1
59
+ end
60
+ result = text[index..rindex]
61
+ result = result.map{ |s| s.strip }
62
+ result = result.reject{ |s| s[0,1] != '#' }
63
+ result = result.map{ |s| s.sub(/^#/,'').strip }
64
+ #result = result.reject{ |s| s == "" }
65
+ result
66
+ end
67
+
68
+ # Get comment from file searching down from given line number.
69
+ #
70
+ # @param file [String] filename, should be full path
71
+ # @param line [Integer] line number in file
72
+ #
73
+ def get_following_comment(file, line)
74
+ get_following_comment_lines(file, line).join("\n").strip
75
+ end
76
+
77
+ # Get comment from file searching down from given line number.
78
+ #
79
+ # @param file [String] filename, should be full path
80
+ # @param line [Integer] line number in file
81
+ #
82
+ def get_following_comment_lines(file, line)
83
+ text = read(file)
84
+ index = line || 0
85
+ while text[index] !~ /^\s*\#/
86
+ return nil if text[index] =~ /^\s*(class|module)/
87
+ index += 1
88
+ end
89
+ rindex = index
90
+ while text[rindex] =~ /^\s*\#/
91
+ rindex += 1
92
+ end
93
+ result = text[index..(rindex-2)]
94
+ result = result.map{ |s| s.strip }
95
+ result = result.reject{ |s| s[0,1] != '#' }
96
+ result = result.map{ |s| s.sub(/^#/,'').strip }
97
+ result.join("\n").strip
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,30 @@
1
+ module Executable
2
+
3
+ # Variation of Executable which provides basic compatibility with
4
+ # previous versions of Executable. It provides a call method that
5
+ # automatically dispatches to publich methods.
6
+ #
7
+ # Among other uses, Dispatch can be useful for dropping into any class
8
+ # as a quick and dirty way to work with it on the command line.
9
+ #
10
+ # @since 1.2.0
11
+ module Dispatch
12
+ Executable.__send__(:append_features, self)
13
+
14
+ #
15
+ # When Dispatchable is included into a class, the class is
16
+ # also extended by `Executable::Domain`.
17
+ #
18
+ def self.included(base)
19
+ base.extend Domain
20
+ end
21
+
22
+ def call(name, *args)
23
+ public_method(name).call(*args)
24
+ end
25
+ end
26
+
27
+ # Dispatch is Legacy.
28
+ Legacy = Dispatch
29
+
30
+ end