optparse-lite 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ # optparse-lite
2
+
3
+ ## glossary
4
+
5
+ ### _understanding the terms of this important new emerging field_ ###
6
+
7
+ #### cli
8
+ abbreviation for Command Line Interface
9
+
10
+ #### list
11
+ in data structures, a *ordered* collection of items [^list]
12
+
13
+ #### set
14
+ in data structures, an *unordered* collection of unique items [^set]
15
+
16
+
17
+ <br />
18
+ <hr />
19
+ ## _Die Fußnoten_ ##
20
+
21
+ [^list]: [list definition at wikipedia](http://en.wikipedia.org/wiki/List_%28computer_science%29)
22
+
23
+ [^set]: [set definition at wikipedia](http://en.wikipedia.org/wiki/Set_%28computer_science%29)
@@ -0,0 +1,29 @@
1
+ # OptparseLite
2
+
3
+ ## installation
4
+
5
+
6
+ ### easy
7
+ the traditional, easy way from rubygems:
8
+
9
+ from the command line:
10
+ ~~~
11
+ ~ > gem install optparse-lite
12
+ ~~~
13
+
14
+
15
+ ### weird purist
16
+ OptparseLite is a single file with no dependencies. If you want you can just
17
+ [grab the file](http://github.com/hipe/optparse-lite/blob/master/lib/optparse-lite.rb)
18
+ and throw it wherever.
19
+
20
+
21
+ ### hack it?
22
+ Sure, why not:
23
+ from the command line:
24
+ ~~~
25
+ ~ > git checkout blah blah @todo
26
+ ~~~
27
+
28
+
29
+ Then maybe check out the [usage](/usage/) to get started!
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
+ <!-- Generated by hipe version 0.0.0 (20100511.1600) -->
4
+ <svg width="175" height="130" viewBox="0.00 0.00 175.00 135.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
5
+ <g id="blah1" class="whatevs" transform="scale(1 1) rotate(0) translate(0 0)">
6
+ <title>this_some_awesome_thing</title>
7
+ <g id="node1" class="node">
8
+ <title>boof</title>
9
+ <polygon fill="#cceeff" stroke='#330011' points="19,0 175,0 175,132 169,122 162,132 157,122 148,130 145,119 136,127 133,116 124,124 121,113 112,120 109,109 99,116 98,104 87,109 87,98 76,104 76,92 65,97 65,86 54,91 55,80 43,84 45,73 33,75 38,64 26,64 32,54 21,53 28,44 17,42 25,34 14,32 23,25 12,21 22,16 12,12 21,7 13,3 15,0"/>
10
+ <g transform="scale(1) rotate(35) translate(12, -28)">
11
+ <text text-anchor="middle" x="71" y="11" font-size="25" fill="#330011">not</text>
12
+ </g>
13
+ <g transform="scale(1) rotate(30) translate(12, -30)">
14
+ <text text-anchor="middle" x="111" y="22" font-size="25" fill="#330011">fun</text>
15
+ </g>
16
+ <g transform="scale(1) rotate(20) translate(12, -30)">
17
+ <text text-anchor="middle" x="142" y="47" font-size="25" fill="#330011">ny</text>
18
+ </g>
19
+ <g transform="scale(1) rotate(20) translate(12, -30)">
20
+ <text text-anchor='middle' x='105' y='68' font-size='25' fill="#330011">❧</text>
21
+ </g>
22
+ </g>
23
+ </g>
24
+ </svg>
@@ -0,0 +1,240 @@
1
+ # optparse-lite
2
+
3
+ # usage
4
+
5
+ ## including optparse lite turns a class into a cli app
6
+
7
+
8
+ The excellent option parser [Trollop](http://trollop.rubyforge.org/) "doesn't require you to subclass some shit just to use a damn option parser." To use `optparse-lite`, however, you will need to make at least one module or class and `include OptparseLite` unto it. (It's probably better if you [don't ask why](/high-concept/)):
9
+
10
+ (see: test.rb - app - 'empty app' - full)
11
+
12
+ We throw the above in a file called `emtpy-app.rb` (for example), and after making sure it is executable (as per your os),
13
+
14
+ from the command line:
15
+ ~~~
16
+ ~ > chmod u+x empty-app.rb
17
+ ~~~
18
+
19
+ we ask ourselves, what can we do with an empty app with no methods (commands)? Let's try running it:
20
+
21
+ (see: test.rb - playback - 'empty-app.rb must work')
22
+
23
+ That's right. Nothing.
24
+
25
+ But things are about to get crazy-go-nuts when we turn it up a notch and add a method:
26
+
27
+ (see: test.rb - app - 'one meth')
28
+
29
+ We don't have to, but we are using the `$stdout` wrapper `ui` to call the standard output methods. It makes testing easier, and insulates the application code from having to know which stream it should actually be writing to, if so desired.
30
+
31
+ We run it by invoking the one command name from the command line:
32
+
33
+ (see: test.rb - playback - 'one-meth-app.rb runs')
34
+
35
+ So far so good. Amazing, in fact.
36
+
37
+ Why and how did it allow us to use an instance method defined in our class as a command accessible from the command line? These and more important questions will be explored below...
38
+
39
+ But what happens when we request a command (method) that we haven't defined !!??!??
40
+
41
+ (see: test.rb - playback - 'one-meth-app.rb works like help when the command is not found')
42
+
43
+ Hm. Walk north. Use door.
44
+
45
+ (see: test.rb - playback - 'one-meth-app.rb ask for help must work')
46
+
47
+ Ok, so we get a nice little listing of all the (one) commands available.
48
+
49
+ As we've seen from the above minimal example, `OptparseLite` can receive a request from the command line and route it to the appropriate method to carry out the request. But before we do anything useful with this, let's take a minute to see what's going on behind the scenes...
50
+
51
+
52
+ ## command interpreter objects are memory persistent
53
+
54
+ What happens if *in the same course of execution* we invoke `run` multiple times?
55
+
56
+ (see: test.rb - app - 'persistent' - full)
57
+
58
+ Above we set up some stuff in an initialize method, and each time the `ping` command is carried out, we increment the little jobber dohickey.
59
+
60
+ (This is a contrived example - you couldn't do this from the command line as it is written:)
61
+
62
+ (see: test.rb - playback - 'persistent-service-app.rb multiple requests')
63
+
64
+ The above output is generated from a unit test that ran the same command two times in the same process. Your mileage would vary if you actually ran it from the command line; but the uptake of it is that when the `OptparseLite` class goes to create a command interpreter object, it reuses any existing such object if one has yet been created for that class.
65
+
66
+ This would be relevant if you adapt an `OptparseLite` app to work as a service or alongside, within or as[^no] a web app. It is premature to point it out now, but I am following the order of stuff as it is shown in the unit tests `:P` I just work here.
67
+
68
+
69
+ ## method signatures and command signatures are isomorphic
70
+
71
+ Let's make an app with a single, minimal command like above (this one does absolutely nothing), but this time it uses the splat operator in its method signature (`splat` means the method can take any number of arguments):
72
+
73
+ (see: test.rb - app - 'neg arity' - full)
74
+
75
+ We run this bad boy with no arguments to see a summary of the commands available:
76
+
77
+ (see: test.rb - playback - 'one-meth-with-neg-arity-app.rb must work')
78
+
79
+ Because ruby method reflection can't discern between `def foo(bar=nil)` and `def foo(*bar)`, it treats it as the former, and gives us a command which takes an optional argument. But the point is optional arguments to a command appear as optional parameters to a method. Welcome to that idea.
80
+
81
+
82
+ ## a rake-like dsl exists for describing things
83
+
84
+ A rake-like DSL exists for describing our app and its commands:
85
+
86
+ (see: test.rb - app - 'one meth desc')
87
+
88
+ `include`ing `OptparseLite` unto your class hackishly `extend`s it with another module, as has been known to happen sometimes when you go around including stranger's modules willy-nilly. So you get a few methods, some of which are `app` and `desc`.
89
+
90
+ `app` is for describing and defining aspects of the cli application as a whole. `desc` is for describing whatever following command (method) is defined.
91
+
92
+ So now when we see the general help screen we see our description for the whole app, and any descriptions we have added for the commands:
93
+
94
+ (see: test.rb - playback - 'one-meth-desc-app.rb must work')
95
+
96
+
97
+ Of course looking at help for the specific command we see our `desc` string:
98
+
99
+ (see: test.rb - playback - 'one-meth-desc-app.rb ask for help must work')
100
+
101
+ Big whoop.
102
+
103
+
104
+ ## usage vs. description
105
+
106
+ Separate from the description of the command there is also the usage string used to describe its usage, usually following some BNF-like conventions:
107
+
108
+ (see: test.rb - app - 'one meth usage')
109
+
110
+ If you didn't want your command parameters to be described as `arg1` and `arg2`, which I understand if you don't, we instead get[^abs]:
111
+
112
+ (see: test.rb - playback - 'one-meth-usage-app.rb must work')
113
+
114
+ One thing to note about the above is that the command name is added automatically for us in our usage string. We don't get to type that ourselves.
115
+
116
+
117
+ ### more on usage:
118
+
119
+ Your usage string doesn't have to correspond at all to the method's actual signature:
120
+
121
+ (see: test.rb - app - 'more usage')
122
+
123
+ and it will still show it as you like it:
124
+
125
+ (see: test.rb - playback - 'cov-patch-app.rb displays wierd usage (no validation!?)' - {"id":"blah1"})
126
+
127
+ But if you use the string <code>[&lt;args&gt;]</code> in there, it will substitute `arg1`..`argn` there:
128
+
129
+ (see: test.rb - playback - 'cov-patch-app.rb interpolates args for no reason' - {"id":"blah2"})
130
+
131
+
132
+
133
+
134
+ If you are bored at this point it is because all of this is really boring.
135
+
136
+
137
+ ## still boring
138
+
139
+ one app. three commands. one cup.
140
+
141
+ (see: test.rb - app - 'three meth')
142
+
143
+
144
+ Help screen lists the methods with stuff aligned properly:
145
+
146
+ (see: test.rb - playback - 'three-meth-app.rb no args must work')
147
+
148
+ note the generated usage, and note that only the first `desc` line for `faz` is shown.
149
+
150
+ Ask for help for an invalid command:
151
+
152
+ (see: test.rb - playback - 'three-meth-app.rb help requested command not found must work')
153
+
154
+ Ask for help with an ambiguous command name:
155
+
156
+ (see: test.rb - playback - 'three-meth-app.rb help requested partial match must work 1')
157
+
158
+ Ask for help for an incomplete but unambiguous command name:[^cred1]
159
+
160
+ (see: test.rb - playback - 'three-meth-app.rb help requested partial match must work 2')
161
+
162
+
163
+ ## can we please parse some goddam options now
164
+
165
+ `OptparseLite`'s focus is not parsing options[^clev] for that is already a well-traveled space not in need of further innovation, even from the neo-minimalist camp. However, it tries to do a minimalist, good enough job of it; like Henry David Thoreau trying to help design the interior of a Starbuck's corporate headquarters.
166
+
167
+ (see: test.rb - app - 'finally' - {"wrap":80})
168
+
169
+ When you `include` `OptparseLite`, you get an `opts` method that takes a block for defining your options. In there you get `banner` for throwing a descriptive string in there at that point. You get an `opt` method for defining an option.
170
+
171
+ The `opt` method has a syntax that is an amalgam of some different stuff i seen before. The first argument to it is a string describing the syntax of your option. You can define either or both a short and a long version of your option and either none or an optional or a required parameter. Note the '--[no-]' form.
172
+
173
+ Any subsequent strings will be used as description strings. What the `:symbol` does will be covered later.
174
+
175
+ Here's help for the whole app. The whole syntax for the command is crammed into one line:
176
+
177
+ (see: test.rb - playback - 'finally-app.rb general help' - {"wrap":80})
178
+
179
+ If we look at help for the individual command it will show all our banner lines and description lines:
180
+
181
+ (see: test.rb - playback - 'finally-app.rb command help' - {"wrap":80})
182
+
183
+ Note that banners that 'look like' headers get treated and highlighted as headers.
184
+
185
+ If we call the command with invalid options of some form or another, all the errors get reported. This differs from every option parser I have yet seen, which craps out on the first error:
186
+
187
+ (see: test.rb - playback - 'finally-app.rb complains on optparse errors' - {"id":"blah3","wrap":80})
188
+
189
+ money has been shown.
190
+
191
+
192
+ ## this is important and useful
193
+
194
+ this is like above but it's all finackley and dankley:
195
+
196
+ (see: test.rb - app - 'agg opts' - {"wrap":80})
197
+
198
+ foodey boodey shoodey:
199
+
200
+ (see: test.rb - playback - 'agg-opts-app.rb help display' - {"wrap":80})
201
+
202
+ fazzle dazzle:
203
+
204
+ (see: test.rb - playback - 'agg-opts-app.rb opt validation' - {"wrap":80})
205
+
206
+ bliff spliff gliff:
207
+
208
+ (see: test.rb - playback - 'agg-opts-app.rb must work' - {"wrap":80})
209
+
210
+ dinkle dankle.
211
+
212
+
213
+
214
+ ## subcommands are private methods and blah blah
215
+
216
+ You have passed through a solid wall near the World 1-2 exit and now you are in "World -1", also known as the "Minus World".
217
+
218
+ What I haven't been telling you thus far is that public methods to a class or module that you extend with `OptparseLite` become commands for your app. Private/protected methods are yours to do with what you will. *Unless* you declare them as subcommands:
219
+
220
+ (see: test.rb - app - 'sub' - {"wrap":80})
221
+
222
+ If I just call the `foo` command and don't give it any arguments, it will show me a list of all its available subcommands:
223
+
224
+ (see: test.rb - playback - 'sub-app.rb with command with no arg shows subcommand list' - {"wrap":80})
225
+
226
+ I ask "foo" for help on the "fric" subcommand:
227
+
228
+ (see: test.rb - playback - 'sub-app.rb shows help on sub-command' - {"wrap":80})
229
+
230
+
231
+ I call the "foo fric" subcommand and pass it the argument "frak":
232
+
233
+ (see: test.rb - playback - 'sub-app.rb must work' - {"wrap":80})
234
+
235
+ Welcome to a world where all of your problems have already been solved for you before you even knew you had them. Welcome to `OptparseLite`.
236
+
237
+ [^no]: part of optparse-heavy vaporware
238
+ [^abs]: if we were serious with this kind of absurdity we could of course use ruby2ruby to blah blah you know whatever... generate a gui programmatically from assembler code.
239
+ [^cred1]: i first saw this pattern in wanstrath's code and in git source.
240
+ [^clev]: nor in being light; these are just clever marketing terms
@@ -0,0 +1,982 @@
1
+ module OptparseLite
2
+ @run_enabled = true
3
+ class << self
4
+ def included mod
5
+ if mod.kind_of?(Class); init_service_class(mod, AppSpec)
6
+ else init_service_module(mod, AppSpec) end
7
+ if @after_included_once
8
+ @after_included_once.call(mod)
9
+ @after_included_once = nil
10
+ end
11
+ end
12
+ def init_service_class mod, spec_class
13
+ mod.extend self # only for gentest!
14
+ mod.extend ServiceClass
15
+ mod.init_service_class spec_class
16
+ mod.send(:include, ServiceObject)
17
+ end
18
+ def init_service_module mod, spec_class
19
+ mod.extend mod # (hack) methods effectively become module_methods
20
+ mod.extend ServiceModuleSingleton
21
+ mod.init_service_class spec_class
22
+ mod.init_service_module_singleton
23
+ end
24
+ def after_included_once &b
25
+ @after_included_once = b
26
+ end
27
+ def suppress_run!; @run_enabled = false end
28
+ def enable_run!; @run_enabled = true end
29
+ def run_enabled?; @run_enabled end
30
+ end
31
+ private
32
+ # forward declarations (everything is in alphabetical order):
33
+ module Lingual; end
34
+ module HelpHelper; end
35
+ module ServiceObject; end
36
+ class AppSpec
37
+ include Lingual
38
+ def initialize mod
39
+ @app_description = Description.new
40
+ @base_commands = nil
41
+ @commands = []
42
+ @names = {}
43
+ @mod = mod
44
+ @order = []
45
+ @desc = @opts = @spec = @subcommands = @usage = nil
46
+ end
47
+ attr_reader :app_description
48
+ def base_commands
49
+ @base_commands ||=
50
+ (@order & @mod.public_instance_methods(false)).map do |meth|
51
+ get_command(meth)
52
+ end
53
+ end
54
+ def cmd_desc desc
55
+ @desc ||= []
56
+ @desc.push desc
57
+ end
58
+ def desc mixed
59
+ @app_description.push mixed
60
+ end
61
+ # this is private except for getting subcommand command objects!
62
+ def get_command meth
63
+ @names[meth] ? @commands[@names[meth]] : begin
64
+ @names[meth] = @commands.length
65
+ cmd = Command.new(self, meth)
66
+ @commands.push cmd
67
+ cmd
68
+ end
69
+ end
70
+ def find_all_local local_name
71
+ meth = methodize(local_name)
72
+ re = /^#{Regexp.escape(meth)}/
73
+ command_method_names.grep(re).map do |n|
74
+ if n == meth
75
+ return [get_command(n)]
76
+ else
77
+ get_command(n)
78
+ end
79
+ end
80
+ end
81
+ attr_writer :invocation_name
82
+ def invocation_name
83
+ @invocation_name ||= File.basename($PROGRAM_NAME)
84
+ end
85
+ def method_added_notify meth
86
+ meth = meth.to_s
87
+ @order.push meth
88
+ if @desc || @opts || @usage || @subcommands
89
+ @names[meth] = @commands.length
90
+ @commands.push Command.new(self, meth, @desc, @opts, @usage,
91
+ @subcommands)
92
+ @desc = @opts = @usage = @subcommands = nil
93
+ end
94
+ end
95
+ def opts mixed=nil, &block
96
+ @desc ||= []
97
+ @opts ||= []
98
+ fail("can't take block and arg") if mixed && block
99
+ if block
100
+ mixed = parser_from_block(&block)
101
+ else
102
+ fail("opts must be OptsLike") unless mixed.kind_of?(OptsLike)
103
+ end
104
+ @opts.push @desc.size
105
+ @desc.push mixed
106
+ end
107
+ def parser_from_block &block
108
+ OptParser.new(&block)
109
+ end
110
+ def subcommands *a
111
+ @subcommands ||= []
112
+ @subcommands.concat a
113
+ end
114
+ def unbound_method method_name
115
+ @mod.instance_method method_name
116
+ end
117
+ def usage usage
118
+ @usage ||= []
119
+ @usage.push usage
120
+ end
121
+ private
122
+ def command_method_names
123
+ @order & (@mod.public_instance_methods(false) |
124
+ @commands.map{|x| x.method_name }
125
+ )
126
+ end
127
+ end
128
+ class Command
129
+ include HelpHelper, Lingual
130
+ def initialize spec, method_name, desc=nil, opts=nil, usage=nil,
131
+ subcommands=nil
132
+ @spec = spec
133
+ @method_name = method_name
134
+ @desc = DescriptionAndOpts.new(desc || [])
135
+ @opt_indexes = opts || []
136
+ @subcommand_names = subcommands
137
+ @syntax_sexp = nil
138
+ @usage = usage || []
139
+ end
140
+ attr_accessor :given_name, :parent # used only for subcommands
141
+ attr_reader :desc, :usage, :method_name, :spec
142
+ def desc_oneline
143
+ desc.any? ? desc.first_desc_line : nil
144
+ end
145
+ def doc_sexp
146
+ common_doc_sexp @desc, :bdy
147
+ end
148
+ def opts
149
+ @opt_indexes.map{|x| @desc[x]}
150
+ end
151
+ def process_opt_parse_errors resp, opts={}
152
+ opts = {opts=>true} if opts.kind_of?(Symbol)
153
+ return help_requested(resp) if ! resp.detect do |x|
154
+ ! x.respond_to?(:error_type) || x.error_type != :help_requested
155
+ end
156
+ ui = @disp.ui.err
157
+ ui.puts "#{prefix}couldn't #{cmd(pretty)} because of "<<
158
+ Np.new(proc{|b| b ? 'the following' : 'an'},'error',resp.size)
159
+ ui.puts resp
160
+ @disp.help.command_usage self, ui if opts[:show_usage]
161
+ @disp.help.invite_to_more_command_help_specific self, ui
162
+ return -1
163
+ end
164
+ def run disp, argv
165
+ @disp = disp
166
+ opts = nil
167
+ if parser = get_parser
168
+ resp, opts = parser.parse(argv)
169
+ return process_opt_parse_errors(resp, :show_usage) if resp.errors.any?
170
+ end
171
+ argv.unshift(opts) if opts
172
+ resp = nil
173
+ begin
174
+ resp = disp.impl.send(method_name, *argv)
175
+ rescue ArgumentError => e
176
+ if one_of_ours(e)
177
+ return process_opt_parse_errors [e.message], :show_usage
178
+ else
179
+ raise e
180
+ end
181
+ end
182
+ resp
183
+ end
184
+ def pretty
185
+ given_name ? given_name.to_s : method_name.gsub(/_/,'-')
186
+ end
187
+ def pretty_full
188
+ parent ? "#{parent.pretty_full} #{pretty}" : pretty
189
+ end
190
+ def subcommands
191
+ @subcommands ||= SubCommands.new(self, @subcommand_names)
192
+ end
193
+ def syntax_sexp
194
+ return @syntax_sexp unless @syntax_sexp.nil?
195
+ # no support for union grammars yet (or evar!) (like git-branch).
196
+ usage = @usage.any? ? @usage.join(' ') : default_usage
197
+ md = (/\A(\[<opts>\] *)?(.*)\Z/m).match(usage) # matches all strings
198
+ @syntax_sexp = [cmds_sexp, opts_sexp(md[1]), args_sexp(md[2])].compact
199
+ end
200
+ private
201
+ def args_sexp str
202
+ # @todo: note that when it doesn't parse '[<opts>]' in the usage string
203
+ # then all opts are treated as args here. so this (for now) should only
204
+ # be used for presentation stuff (of course we could etc...)
205
+ if subcommands.any? && str.index('<subcommand>')
206
+ return subcommand_sexp str
207
+ elsif(
208
+ /\A\s*([^\s]+(?:\s+[^\s]+)*)?\s*(?:\[<args>\]|<args>)\s*\Z/x =~ str)
209
+ args = args_sexp_children_from_arity || []
210
+ opts = $1 ? $1.split(' ') : []
211
+ else
212
+ args = str.split(' ') # and of course this breaks on nested things
213
+ opts = []
214
+ end
215
+ (both = opts + args).empty? ? nil : [:args, *both]
216
+ end
217
+ def args_sexp_children_from_arity
218
+ arity = unbound_method.arity
219
+ return nil if arity.zero?
220
+ arity -= (arity > 0 ? 1 : -1) if opts.any?
221
+ args = (0..arity.abs-1).map{|i| "<arg#{i+1}>"}
222
+ if arity < 0
223
+ args.last.replace("[#{args.last}]") # too bad we can't etc
224
+ end
225
+ args
226
+ end
227
+ def cmds_sexp
228
+ [:cmds, pretty_full] # @todo later
229
+ end
230
+ def default_usage
231
+ [subcommands.any? ? '<subcommand>': nil, '[<opts>] [<args>]'].compact.
232
+ join(' ')
233
+ end
234
+ def get_parser
235
+ case opts.size
236
+ when 0; nil
237
+ when 1; opts.first
238
+ else OptParserAggregate.new(opts)
239
+ end
240
+ end
241
+ def help_requested errors
242
+ err = errors.first
243
+ if err.respond_to?(:"long_help?") && err.long_help?
244
+ @disp.help.command_help_full(self)
245
+ elsif err.respond_to?(:"version_requested?") && err.version_requested?
246
+ @disp.ui.puts err.parser.version
247
+ else
248
+ @disp.help.command_usage self, @disp.ui
249
+ @disp.help.invite_to_more_command_help_specific self, @disp.ui
250
+ end
251
+ 0
252
+ end
253
+ def one_of_ours e
254
+ e.backtrace.first.index(__FILE__)# hack to see where it orignated
255
+ end
256
+ def opts_sexp match
257
+ return nil if @opt_indexes.empty? # no matter what u don't take options
258
+ return nil if match.nil? # maybe want to have them but not show them?
259
+ [:opts, *opts.map{|o| o.syntax_tokens.map{|x| "[#{x}]"}}.flatten]
260
+ end
261
+ def subcommand_sexp str
262
+ md = /\A(.*)<subcommand>(.*)\Z/.match(str)
263
+ x = [md[1].empty? ? nil : [:txt, md[1]],
264
+ [:or, *subcommands.map{|x| [:cmds, x.given_name.to_s] }],
265
+ md[2].empty? ? nil : [:txt, md[2]]
266
+ ]; x.compact
267
+ end
268
+ def unbound_method
269
+ @spec.unbound_method method_name
270
+ end
271
+ end
272
+ class Description < Array
273
+ class << self; def [](m); m.extend(self) end end
274
+ def get_desc_lines
275
+ self
276
+ end
277
+ end
278
+ class DescriptionAndOpts < Array
279
+ def any?
280
+ detect{|x| x.respond_to?(:to_str)}
281
+ end
282
+ def first_desc_line
283
+ resp = nil
284
+ each do |x|
285
+ resp = x.kind_of?(String) ? x : x.first_desc_line
286
+ break if resp
287
+ end
288
+ resp
289
+ end
290
+ end
291
+ class Dispatcher
292
+ include HelpHelper
293
+ def initialize impl, spec, ui
294
+ @impl = impl
295
+ @spec = spec
296
+ @ui = ui
297
+ @help = Help.new(@spec, @ui)
298
+ end
299
+ # commands need some or all of these
300
+ attr_reader :impl, :spec, :ui, :help
301
+ def run argv
302
+ return @help.no_args if argv.empty?
303
+ return @help.requested(argv) if help_requested?(argv)
304
+ if cmd = @help.find_one_loudly(argv.shift, @spec)
305
+ cmd.run(self, argv)
306
+ else
307
+ -1 # kind of silly but whatever
308
+ end
309
+ end
310
+ private
311
+ end
312
+ module HelpHelper
313
+ def help_requested?(argv)
314
+ ['-h','--help','-?','help'].include? argv[0]
315
+ end
316
+ # Not sure about this. 'lines like this:' usually aren't headers
317
+ # but maybe 'Lines Like This:' are. are 'Lines like this:' ?
318
+ def looks_like_header? line
319
+ /\A[A-Z0-9][A-Za-z]*(?:\s[A-Za-z0-9]*)*:\s*\Z/ =~ line
320
+ end
321
+ # Codes = {:red=>'31', :green=>'32', :yellow=>'33', :bold=>'1', :blink=>5}
322
+ def hdr(str); "\e[32;m#{str}\e[0m" end
323
+ def prefix; "#{spec.invocation_name}: " end
324
+ def txt(str); str end
325
+ def cmd(str); str end # @todo change to underline
326
+ alias_method :code, :hdr
327
+ private
328
+ def common_doc_sexp items, txt_type=:txt
329
+ these = items.map{ |x| x.respond_to?(:doc_sexp) ? x.doc_sexp :
330
+ looks_like_header?(x) ? [[:hdr, x]] : [[txt_type, x]]
331
+ }; these.flatten(1)
332
+ end
333
+ end
334
+ class Help
335
+ include Lingual, HelpHelper
336
+ def initialize spec, ui
337
+ @margin_a = ' '
338
+ @margin_b = ' '
339
+ @spec = spec
340
+ @ui = ui
341
+ end
342
+ def command_help_full cmd_str, rest=[]
343
+ return command_help_full_actual(cmd_str, rest) unless
344
+ cmd_str.kind_of?(String)
345
+ if found = find_one_loudly(cmd_str, @spec)
346
+ if rest.any?
347
+ command_help_full "#{cmd_str} #{rest.shift}", rest
348
+ else
349
+ command_help_full_actual found, rest
350
+ end
351
+ end
352
+ end
353
+ def command_usage cmd, ui=@ui
354
+ sexp = cmd.syntax_sexp.dup # b/c of unshift below
355
+ sexp.unshift([:cmds, @spec.invocation_name])
356
+ ui.puts hdr('Usage:')+' '+stylize_syntax(sexp)
357
+ end
358
+ def find_one_loudly cmd, commands
359
+ all = commands.find_all_local cmd
360
+ case all.size
361
+ when 0
362
+ @ui.puts "i don't know how to #{code cmd}."
363
+ invite_to_more_help commands
364
+ nil
365
+ when 1
366
+ all.first
367
+ else
368
+ @ui.puts "did you mean " <<
369
+ oxford_comma(all.map{|x| code(x.pretty)}, ' or ') << '?'
370
+ invite_to_more_help commands
371
+ nil
372
+ end
373
+ end
374
+ def invite_to_more_command_help_specific cmd, ui=@ui
375
+ ui.puts("try #{code(@spec.invocation_name)} #{code('help')} "<<
376
+ "#{code(cmd.pretty_full)} for full syntax and usage.")
377
+ end
378
+ def requested argv
379
+ return command_help_full(argv[1], argv[2..-2]) if argv.size > 1
380
+ app_usage
381
+ app_description_full
382
+ list_base_commands
383
+ end
384
+ def no_args
385
+ app_usage_expanded
386
+ app_description_full
387
+ list_base_commands
388
+ end
389
+ private
390
+ def app_description_full
391
+ lines = @spec.app_description.get_desc_lines
392
+ @ui.puts lines.map{|line| "#{@margin_a}#{line}"}
393
+ end
394
+ def app_usage_expanded
395
+ @ui.print("#{hdr 'Usage:'} #{@spec.invocation_name}")
396
+ if @spec.base_commands.empty?
397
+ @ui.puts(" (this screen. no commands defined.)")
398
+ else
399
+ @ui.puts(' ('<<@spec.base_commands.map{|c| c.pretty }*'|'<<
400
+ ') [<opts>] [<args>]'
401
+ )
402
+ end
403
+ end
404
+ alias_method :app_usage, :app_usage_expanded
405
+ def command_help_full_actual cmd, rest
406
+ command_usage cmd
407
+ sexp = cmd.doc_sexp.dup
408
+ if sexp.any?
409
+ case sexp.first.first
410
+ when :opt; sexp.unshift([:hdr, 'Options:'])
411
+ when :bdy, :txt; sexp.unshift([:hdr, 'Description:'])
412
+ end
413
+ end
414
+ stylize_docblock sexp, @ui
415
+ if (cmds = cmd.subcommands).any?
416
+ @ui.puts
417
+ @ui.puts "#{hdr 'Sub Commands:'}"
418
+ list_commands cmds
419
+ invite_to_more_command_help_general
420
+ end
421
+ end
422
+ def invite_to_more_command_help_general
423
+ @ui.puts "type -h after a command or subcommand name for more help"
424
+ end
425
+ def invite_to_more_help cmds=@spec
426
+ @ui.puts 'try '+ [ code(@spec.invocation_name),
427
+ cmds.respond_to?(:pretty_full) ? code(cmds.pretty_full) : nil,
428
+ code('-h')].compact.join(' ') + ' for help.'
429
+ end
430
+ def list_commands cmds
431
+ width = cmds.map{|c| c.pretty.length}.max
432
+ cmds.each do |c|
433
+ cmd_desc = c.desc_oneline
434
+ cmd_desc ||= 'usage: '+stylize_syntax(c.syntax_sexp)
435
+ @ui.puts "#{@margin_a}%-#{width}s#{@margin_b}#{cmd_desc}" % [c.pretty]
436
+ end
437
+ end
438
+ def list_base_commands
439
+ cmds = @spec.base_commands
440
+ return if cmds.empty?
441
+ @ui.puts
442
+ @ui.puts "#{hdr 'Commands:'}"
443
+ list_commands cmds
444
+ invite_to_more_command_help_general
445
+ end
446
+ class SexpWrapper # hack so you can access .first on nil nodes
447
+ class NilSexpClass; def first; nil end end
448
+ NilSexp = NilSexpClass.new
449
+ def initialize(sexp); @sexp = sexp end
450
+ def each_with_index(*a, &b); @sexp.each_with_index(*a, &b); end
451
+ def [](idx); it = @sexp[idx] and it or NilSexp; end
452
+ end
453
+ def stylize_docblock sexp, ui=@ui
454
+ matrix = stylize_docblock_first_pass sexp
455
+ width = matrix.map{|x|(Array===x&&x[0])?x[0].length : nil}.compact.max
456
+ matrix.each do |row|
457
+ case row
458
+ when String; ui.puts row
459
+ when Array;
460
+ ui.print "#{@margin_a}%#{width}s" % row[0] # should be ok on nil
461
+ ui.puts row[1] ? "#{@margin_b}#{row[1]}" : "\n"
462
+ end
463
+ end
464
+ end
465
+ def stylize_docblock_first_pass sexp
466
+ idx, last = 0, sexp.size-1
467
+ sexp = SexpWrapper.new(sexp)
468
+ matrix = [] # two-pass rendering to line up columns
469
+ while idx <= last
470
+ node = sexp[idx]
471
+ case node.first
472
+ when :hdr
473
+ matrix.push hdr(node[1])
474
+ if sexp[idx+1].first == :bdy && sexp[idx+2].first != :bdy
475
+ matrix.last.concat " #{sexp[idx+1][1]}"
476
+ idx += 1 # special case: only one line of txt on same line as hdr
477
+ end
478
+ when :bdy; matrix.push "#{@margin_a}#{node[1]}"
479
+ when :txt; matrix.push node[1]
480
+ when :opt
481
+ matrix.push [node[1], node[2]] # multiline opt docs:
482
+ matrix.concat node[3..-1].map{|x| [nil, x]} if node[3]
483
+ end
484
+ idx += 1
485
+ end
486
+ matrix
487
+ end
488
+ def stylize_syntax sexp
489
+ resp =
490
+ if sexp.first.kind_of? Symbol
491
+ case sexp.first
492
+ when :cmds; sexp[1..-1].map{|c| cmd(c)}.join(' ')
493
+ when :or; '('+sexp[1..-1].map{|x| stylize_syntax(x)}*'|'+')'
494
+ else sexp[1..-1].join(' ') # :opts, :args, :txt
495
+ end
496
+ else
497
+ sexp.map{|x| stylize_syntax(x)}*' '
498
+ end
499
+ resp.strip # turn ' <args>' into '<args>'
500
+ end
501
+ end
502
+ module Lingual
503
+ def oxford_comma items, sep=' and ', comma=', '
504
+ return '()' if items.size == 0
505
+ return items[0] if items.size == 1
506
+ seps = [sep, '']
507
+ seps.insert(0,*Array.new(items.size - seps.size, comma))
508
+ items.zip(seps).flatten.join('')
509
+ end
510
+ def methodize mixed
511
+ mixed.to_s.gsub(/[^a-z0-9_\?\!]/,'_')
512
+ end
513
+ end
514
+ module OptsLike
515
+ # syntax_tokens, parse, doc_sexp
516
+ end
517
+ module OptsBlock
518
+ include OptsLike
519
+ end
520
+ module ServiceClass
521
+ def init_service_class spec_class
522
+ @argv_hook = nil
523
+ @instance ||= nil
524
+ @spec = spec_class.new(self)
525
+ @ui = Ui.new
526
+ end
527
+ attr_reader :ui, :spec
528
+ alias_method :app, :spec
529
+ def o usage
530
+ @spec.usage usage
531
+ end
532
+ def opts mixed=nil, &block
533
+ @spec.opts(mixed, &block)
534
+ end
535
+ alias_method :usage, :o
536
+ def run argv=ARGV
537
+ @argv_hook.call(argv) if @argv_hook
538
+ argv = argv.dup # never change caller's array
539
+ return @ui.err.puts('run disabled. (probably for gentesting)') unless
540
+ OptparseLite.run_enabled?
541
+ unless @instance # rcov bug?
542
+ obj = new
543
+ obj.init_service_object(@spec, @ui)
544
+ @instance = obj
545
+ end
546
+ @instance.run argv
547
+ end
548
+ def set_argv_hook &hook
549
+ @argv_hook = hook
550
+ end
551
+ def subcommands *a
552
+ @spec.subcommands(*a)
553
+ end
554
+ def x desc
555
+ @spec.cmd_desc desc
556
+ end
557
+ alias_method :desc, :x
558
+ def method_added method_sym
559
+ @spec.method_added_notify method_sym
560
+ end
561
+ end
562
+ module ServiceModuleSingleton
563
+ include ServiceClass
564
+ include ServiceObject
565
+ def init_service_module_singleton
566
+ @dispatcher = Dispatcher.new(self, @spec, @ui)
567
+ end
568
+ def run argv=ARGV
569
+ argv = argv.dup # never change caller's array
570
+ return @ui.err.puts('run disabled. (probably for gentesting)') unless
571
+ OptparseLite.run_enabled?
572
+ @dispatcher.run argv
573
+ end
574
+ end
575
+ module ServiceObject
576
+ include HelpHelper
577
+ def init_service_object spec, ui
578
+ @spec = spec
579
+ @ui = ui
580
+ @dispatcher = Dispatcher.new(self, @spec, @ui)
581
+ end
582
+ def run argv
583
+ @dispatcher.run argv
584
+ end
585
+ def subcommand_dispatch(*a)
586
+ cmd = @spec.get_command(caller.first.match(/`([^']+)'\Z/)[1])
587
+ if a.empty? || (help_requested?(a) && a.shift)
588
+ return @dispatcher.help.command_help_full(cmd.pretty_full, a)
589
+ end
590
+ if cmd = @dispatcher.help.find_one_loudly(a.shift, cmd.subcommands)
591
+ cmd.run @dispatcher, a
592
+ end
593
+ end
594
+ attr_accessor :ui; private :ui # avoid warnings
595
+ end
596
+ class SubCommands < Array
597
+ include Lingual
598
+ def initialize parent_cmd, subcommand_names
599
+ @command = parent_cmd
600
+ method_name = parent_cmd.method_name
601
+ app_spec = parent_cmd.spec
602
+ arr = subcommand_names.nil? ? [] : subcommand_names.map do |name|
603
+ cmd = app_spec.get_command "#{method_name}_#{methodize(name)}"
604
+ cmd.given_name = name
605
+ cmd.parent = parent_cmd
606
+ cmd
607
+ end
608
+ super(arr)
609
+ end
610
+ def pretty_full; @command.pretty_full end
611
+ def find_all_local local_name
612
+ re = /^#{Regexp.escape(local_name)}/
613
+ these = select do |x|
614
+ return [x] if x.given_name.to_s == local_name
615
+ re =~ x.given_name.to_s
616
+ end
617
+ these
618
+ end
619
+ end
620
+ class Sio < StringIO
621
+ def to_str; idx = tell; rewind; str = read; seek(idx); str end
622
+ alias_method :to_s, :to_str
623
+ end
624
+ class Ui
625
+ def initialize
626
+ @out = $stdout
627
+ @err = $stderr
628
+ end
629
+ attr_accessor :err
630
+
631
+ %w(print puts).each do |meth|
632
+ define_method(meth){|*a| @out.send(meth,*a) }
633
+ end
634
+
635
+ def push out=Sio.new, err=Sio.new
636
+ @stack ||= []
637
+ @stack.push [@out, @err]
638
+ @out, @err = out, err
639
+ end
640
+
641
+ def pop both=false
642
+ ret = [@out, @err]
643
+ @out, @err = @stack.pop
644
+ return ret if both
645
+ return ret[0] if ret[1].respond_to?(:to_str) && ''==ret[1].to_str
646
+ # ret # ick. tries to pretend there is only one out stream when possible
647
+ end
648
+ end
649
+ end
650
+
651
+ # temporary? as minimal as reasonable option parsing below
652
+ module OptparseLite
653
+ module ReExtra
654
+ # consumes string, allows for named captures
655
+ class << self
656
+ def[](re,*names)
657
+ re.extend(self)
658
+ re.names = names
659
+ re
660
+ end
661
+ end
662
+ attr_accessor :names
663
+ def parse str
664
+ if md = match(str)
665
+ caps = md.captures
666
+ str.replace str[md.offset(0)[1]..-1]
667
+ sing = class << caps; self end
668
+ names.each_with_index{|(n,i)| sing.send(:define_method,n){self[i]}}
669
+ caps
670
+ end
671
+ end
672
+ end
673
+ module OptHelper
674
+ def dashes key # hack, could be done in spec instead somehow
675
+ key.length == 1 ? "-#{key}" : "--#{key}"
676
+ end
677
+ end
678
+ class OptParser
679
+ include OptHelper, HelpHelper
680
+ def initialize(&block)
681
+ @block = block
682
+ @compiled = false
683
+ @items = []
684
+ @names = {}
685
+ @specs = []
686
+ end
687
+ def compile!
688
+ instance_eval(&@block)
689
+ @compiled = true
690
+ end
691
+ def doc_sexp
692
+ compile! unless @compiled
693
+ common_doc_sexp @items
694
+ end
695
+ def parse argv
696
+ opts = parse_argv argv
697
+ resp = validate_and_populate(opts)
698
+ [resp, opts]
699
+ end
700
+ def specs
701
+ compile! unless @compiled
702
+ @specs.map{|idx| @items[idx]}
703
+ end
704
+ def syntax_tokens
705
+ specs.map{|x| x.syntax_tokens * ','}
706
+ end
707
+ # @return [Response], alter opts
708
+ # this does the following: for all unrecognized opts, add one error
709
+ # (one error encompases all of them), populate defaults, normalize
710
+ # them either to the accessor or last long or last short surface form
711
+ # with a symbol key (maybe), make sure that opts that don't take parameter
712
+ # don't have them and opts that require them do.
713
+ def validate_and_populate opts
714
+ compile! unless @compiled
715
+ resp = Response.new
716
+ sing = class << opts; self end
717
+ specs = self.specs
718
+ opts.keys.each do |key|
719
+ val = opts.delete(key) # easier just to do this always
720
+ if ! @names.key?(key)
721
+ resp.unrecognized_parameter(key, val)
722
+ else
723
+ spec = specs[@names[key]]
724
+ if spec.required? && val == true
725
+ resp.required_argument_missing(spec, key)
726
+ elsif val != true && ! spec.takes_argument?
727
+ resp.argument_not_allowed(spec, key, val)
728
+ else # if resp.valid? (aggregate parses.. @todo)
729
+ opts[spec.normalized_key] = val
730
+ if spec.accessor
731
+ acc = spec.accessor
732
+ sing.send(:define_method, spec.accessor){ self[acc] }
733
+ end
734
+ end
735
+ end
736
+ end
737
+ these = specs.map{|s| s.has_default? ? s.normalized_key : nil }.compact
738
+ employ = these - opts.keys
739
+ employ.each{|k| opts[k]=specs.detect{|s| s.normalized_key == k}.default}
740
+ resp
741
+ end
742
+ private
743
+ def banner str
744
+ @items.push str
745
+ end
746
+ def opt syntax, *extra
747
+ spec = OptSpec.parse(syntax)
748
+ opts = extra.last.kind_of?(Hash) ? extra.pop : {}
749
+ unless opts[:accessor]
750
+ idxs = extra.each_with_index.map{|(m,i)| Symbol===m ? i : nil}.compact
751
+ fail("can't have more than one symbol in definition") if idxs.size > 1
752
+ opts[:accessor] = extra.slice!(idxs.first) if idxs.any?
753
+ end
754
+ spec.default = opts[:default] if opts.key?(:default)
755
+ spec.desc = extra
756
+ spec.names.each do |name|
757
+ fail("won't redefine existing opt name \"#{name}\"") if @names[name]
758
+ @names[name] = @specs.size
759
+ end
760
+ @specs.push @items.size
761
+ @items.push spec
762
+ end
763
+ def parse_argv argv
764
+ options = []; not_opts = []
765
+ argv.each{ |x| (x =~ /^-/ ? options : not_opts).push(x) }
766
+ opts = Hash[* options.map do |flag|
767
+ key,value = flag.match(/\A([^=]+)(?:=(.*))?\Z/).captures
768
+ [key.sub(/^--?/, ''), value.nil? ? true : value ]
769
+ end.flatten]
770
+ argv.replace not_opts
771
+ opts
772
+ end
773
+ class Response < Array
774
+ include OptHelper, HelpHelper
775
+ def initialize hack_start=nil
776
+ super(hack_start) if hack_start
777
+ @memoish = {}
778
+ end
779
+ def all_indexes sym
780
+ each_with_index.map{|(v,i)| v.error_type == sym ? i : nil }.compact
781
+ end
782
+ def argument_not_allowed spec, key, val
783
+ push Error.new(:argument_not_allowed,
784
+ code(dashes(key))<<" does not take an arguement (#{val.inspect})",
785
+ :norm_key => spec.normalized_key)
786
+ end
787
+ def delete sym
788
+ idxs = all_indexes(sym)
789
+ case idxs.size
790
+ when 0; nil
791
+ when 1; delete_at(idxs.first)
792
+ end
793
+ end
794
+ def errors; self end
795
+ def required_argument_missing spec, key
796
+ push Error.new(:required_argument_missing,
797
+ code(dashes(key))<<" requires a parameter ("<<
798
+ "#{spec.cannonical_name})",
799
+ :norm_key => spec.normalized_key)
800
+ end
801
+ def unrecognized_parameter key, value
802
+ memoish(:unrec_param){ UnparsedParamters.new }[key] = value
803
+ end
804
+ def valid?; empty? end
805
+ private
806
+ def memoish(name, &block)
807
+ return self[@memoish[name]] if @memoish.key? name
808
+ @memoish[name] = size
809
+ push block.call
810
+ last
811
+ end
812
+ end
813
+ module Error;
814
+ attr_accessor :error_type
815
+ class << self
816
+ def [](mixed)
817
+ mixed.extend(self)
818
+ end
819
+ def new error_type, message, opts={}
820
+ ret = self[message.dup]
821
+ ret.error_init error_type, opts
822
+ ret
823
+ end
824
+ end
825
+ def error_init error_type, opts
826
+ @error_type = error_type
827
+ opts.each do |(k,v)|
828
+ /^(.+[^?])(\?)?$/ =~ k.to_s
829
+ attr_name = $1
830
+ instance_variable_set("@#{attr_name}", v)
831
+ def!(k){ instance_variable_get("@#{attr_name}") }
832
+ end
833
+ end
834
+ private
835
+ def def! name, &block
836
+ class << self; self end.send(:define_method, name, &block)
837
+ end
838
+ end
839
+ class UnparsedParamters < Hash
840
+ include Error, OptHelper, HelpHelper
841
+ def initialize
842
+ @error_type = :unparsed_parameters
843
+ end
844
+ def to_s
845
+ "i don't recognize "<<
846
+ Np.new(:this, 'parameter'){|| keys.map{|x| code(dashes(x)) }}
847
+ end
848
+ end
849
+ end
850
+ # we take this opportunity to discover our interface for parsers:
851
+ # parse()
852
+ class OptParserAggregate
853
+ def initialize parsers
854
+ @parsers = parsers
855
+ end
856
+ def parse args
857
+ errors, opts = @parsers.first.parse(args)
858
+ @parsers[1..-1].each do |parser|
859
+ unparsed = errors.delete(:unparsed_parameters) || {}
860
+ errors.concat parser.validate_and_populate(unparsed)
861
+ opts.merge! unparsed
862
+ end
863
+ [errors, opts]
864
+ end
865
+ end
866
+ class OptSpec < Struct.new(:names, :takes_argument, :required,
867
+ :optional, :arg_name, :short, :long, :noable, :desc, :accessor, :default)
868
+ alias_method :required?, :required
869
+ alias_method :optional?, :optional
870
+ alias_method :takes_argument?, :takes_argument
871
+ # def name
872
+ # long.any? ? long.first : short.first
873
+ # end
874
+ @short_long = ReExtra[
875
+ /\A *(?:-([a-z0-9])|--(?:\[(no-)\])?([a-z0-9][-a-z0-9]+)) */i,
876
+ :short, :no, :long
877
+ ]
878
+ required = / (= \s* (?: <[a-z_][-a-z_]*> | [A-Z_]+ ) ) \s* /x
879
+ optional = /(\[\s* = \s* (?: <[a-z_][-a-z_]*> | [A-Z_]+ ) \] ) \s* /x
880
+ @param = ReExtra[Regexp.new(
881
+ '\A' + [required.source,optional.source].join('|'), Regexp::EXTENDED
882
+ )]
883
+ @param.names=[:required, :optional]
884
+ class << self
885
+ extend Lingual
886
+ def parse str
887
+ names, reqs, opts, short, long, noable, caps = [],[],[],[],[], nil,nil
888
+ str.split(/, */).each do |syn|
889
+ failed(str.inspect) unless caps = @short_long.parse(syn)
890
+ names.push(caps.short || caps.long)
891
+ long.push "--#{caps.long}" if caps.long
892
+ short.push "-#{caps.short}" if caps.short
893
+ if caps.no
894
+ failed("i dunno can u say no multiple times?") if noable
895
+ noable = caps.no
896
+ this = "#{caps.no}#{caps.long}"
897
+ long.push "--#{this}"
898
+ names.push this
899
+ end
900
+ if caps = @param.parse(syn)
901
+ (caps.required ? reqs : opts).push(caps.required || caps.optional)
902
+ end
903
+ failed("don't know how to parse: #{syn.inspect}") unless syn.empty?
904
+ end
905
+ failed("can't have both required and optional arguments: "<<
906
+ str.inspect) if reqs.any? && opts.any?
907
+ arg_names = opts | reqs
908
+ failed("let's not take arguments with no- style opts") if
909
+ noable && arg_names.any?
910
+ failed("spell the argument the same way each time: "<<
911
+ oxford_comma(arg_names)) if arg_names.length > 1
912
+ new(names, opts.any? || reqs.any?,
913
+ reqs.any?, opts.any?, arg_names.first, short, long, noable)
914
+ end
915
+ private
916
+ def failed msg
917
+ fail("parse parse fail: bad option syntax syntax: #{msg}")
918
+ end
919
+ end # class << self
920
+ def cannonical_name
921
+ syntax_tokens.last
922
+ end
923
+ def doc_sexp
924
+ [[:opt, syntax_tokens*', ', * desc]]
925
+ end
926
+ def has_default?
927
+ ! default.nil? # whatever. i don't care about nil defaults
928
+ end
929
+ def normalized_key
930
+ accessor ? accessor.to_sym : names.last.to_sym
931
+ end
932
+ def syntax_tokens
933
+ if noable
934
+ ["--[#{noable}]#{names.first}"]
935
+ else
936
+ these = long + short
937
+ these[these.length-1] = "#{these.last}#{arg_name}"
938
+ these
939
+ end
940
+ end
941
+ private
942
+ end
943
+ class Np
944
+ # Noun Phrase. silly cute extraneous way to do plurals
945
+ # this was toned down from previous versions, can be expanded
946
+ # it is half mock now
947
+ include Lingual
948
+ class << self
949
+ alias_method :[], :new
950
+ end
951
+ def initialize art, root, count=nil, &block
952
+ fail('blah blah for now') if block_given? && ! block.arity.zero?
953
+ fail('count and block mutually exclusive, one required') unless
954
+ 1 == [count, block].compact.size
955
+ @art, @root, @block, @count, @list = art, root, block, count, nil
956
+ end
957
+ def to_str
958
+ [ surface_article,
959
+ surface_root,
960
+ surface_items ].compact.join(' ')
961
+ end
962
+ alias_method :to_s, :to_str
963
+ private
964
+ def list
965
+ @block and @list ||= @block.call
966
+ end
967
+ def many?
968
+ @count ||= list.size
969
+ @count != 1
970
+ end
971
+ def surface_article
972
+ @art.kind_of?(Proc) ? @art.call(many?) :
973
+ many? ? 'these' : 'the'
974
+ end
975
+ def surface_items
976
+ oxford_comma(list) if list
977
+ end
978
+ def surface_root
979
+ many? ? "#{@root}s:" : "#{@root}:" # colons will be annoying
980
+ end
981
+ end
982
+ end