optparse-lite 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/NEWS.md +4 -0
- data/README +37 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/doc/high-concept.md +436 -0
- data/doc/high-concept/terms.md +23 -0
- data/doc/installation.md +29 -0
- data/doc/svg/not-funny.svg +24 -0
- data/doc/usage.md +240 -0
- data/lib/optparse-lite.rb +982 -0
- data/lib/optparse-lite/test/gentest/gentest.rb +322 -0
- data/lib/optparse-lite/test/gentest/tasklib.rb +12 -0
- data/lib/optparse-lite/test/gentest/tasks.rb +14 -0
- data/lib/optparse-lite/test/gentest/tasks/gentest.rb +44 -0
- data/lib/optparse-lite/test/gentest/tasks/ungen.rb +64 -0
- data/lib/optparse-lite/test/nandoc-custom-tags.rb +6 -0
- data/lib/optparse-lite/test/nandoc-custom-tags/app.rb +102 -0
- data/lib/optparse-lite/test/nandoc-custom-tags/playback.rb +63 -0
- data/lib/optparse-lite/test/setup.rb +131 -0
- data/lib/optparse-lite/treebis-extlib.rb +5 -0
- data/test/test.rb +963 -0
- metadata +87 -0
@@ -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)
|
data/doc/installation.md
ADDED
@@ -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>
|
data/doc/usage.md
ADDED
@@ -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>[<args>]</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
|