docopt 0.0.4 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/{LICENSE-MIT → LICENSE} +4 -1
- data/README.md +117 -128
- data/Rakefile +150 -0
- data/docopt.gemspec +83 -0
- data/examples/any_options_example.rb +24 -0
- data/examples/calculator.rb +16 -0
- data/examples/counted_example.rb +22 -0
- data/examples/example_options.rb +44 -0
- data/examples/git_example.rb +44 -0
- data/examples/naval_fate.rb +30 -0
- data/examples/odd_even_example.rb +19 -0
- data/examples/quick_example.rb +16 -0
- data/lib/docopt.rb +632 -81
- data/test/test_docopt.rb +49 -0
- data/test/testee.rb +12 -0
- metadata +48 -18
- data/example.rb +0 -30
data/docopt.gemspec
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
10
|
+
s.rubygems_version = '1.3.5'
|
11
|
+
s.required_ruby_version = '>= 1.8.7'
|
12
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
13
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
14
|
+
## the sub! line in the Rakefile
|
15
|
+
s.name = 'docopt'
|
16
|
+
s.version = '0.5.0'
|
17
|
+
s.date = '2012-09-01'
|
18
|
+
# s.rubyforge_project = 'docopt'
|
19
|
+
|
20
|
+
## Make sure your summary is short. The description may be as long
|
21
|
+
## as you like.
|
22
|
+
s.summary = "A command line option parser, that will make you smile."
|
23
|
+
s.description = "Isn't it awesome how `optparse` and other option parsers generate help and usage-messages based on your code?! Hell no!\nYou know what's awesome? It's when the option parser *is* generated based on the help and usage-message that you write in a docstring! That's what docopt does!"
|
24
|
+
|
25
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
26
|
+
## better to set the email to an email list or something. If you don't have
|
27
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
28
|
+
s.authors = ["Blake Williams", "Vladimir Keleshev", "Alex Speller", "Nima Johari"]
|
29
|
+
s.email = "code@shabbyrobe.org"
|
30
|
+
s.homepage = "http://github.com/docopt/docopt.rb"
|
31
|
+
s.license = 'MIT'
|
32
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
33
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
34
|
+
s.require_paths = %w[lib]
|
35
|
+
|
36
|
+
## This sections is only necessary if you have C extensions.
|
37
|
+
# s.require_paths << 'ext'
|
38
|
+
# s.extensions = %w[ext/extconf.rb]
|
39
|
+
|
40
|
+
## If your gem includes any executables, list them here.
|
41
|
+
# s.executables = ["name"]
|
42
|
+
|
43
|
+
## Specify any RDoc options here. You'll want to add your README and
|
44
|
+
## LICENSE files to the extra_rdoc_files list.
|
45
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
46
|
+
s.extra_rdoc_files = %w[README.md LICENSE]
|
47
|
+
|
48
|
+
## List your runtime dependencies here. Runtime dependencies are those
|
49
|
+
## that are needed for an end user to actually USE your code.
|
50
|
+
# s.add_dependency('DEPNAME', [">= 1.1.0", "< 2.0.0"])
|
51
|
+
|
52
|
+
## List your development dependencies here. Development dependencies are
|
53
|
+
## those that are only needed during development
|
54
|
+
s.add_development_dependency('json', "~> 1.6.5")
|
55
|
+
|
56
|
+
## Leave this section as-is. It will be automatically generated from the
|
57
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
58
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
59
|
+
# = MANIFEST =
|
60
|
+
s.files = %w[
|
61
|
+
Gemfile
|
62
|
+
LICENSE
|
63
|
+
README.md
|
64
|
+
Rakefile
|
65
|
+
docopt.gemspec
|
66
|
+
examples/any_options_example.rb
|
67
|
+
examples/calculator.rb
|
68
|
+
examples/counted_example.rb
|
69
|
+
examples/example_options.rb
|
70
|
+
examples/git_example.rb
|
71
|
+
examples/naval_fate.rb
|
72
|
+
examples/odd_even_example.rb
|
73
|
+
examples/quick_example.rb
|
74
|
+
lib/docopt.rb
|
75
|
+
test/test_docopt.rb
|
76
|
+
test/testee.rb
|
77
|
+
]
|
78
|
+
# = MANIFEST =
|
79
|
+
|
80
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
81
|
+
## matches what you actually use.
|
82
|
+
s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
|
83
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Example of program which uses [options] shortcut in pattern.
|
5
|
+
|
6
|
+
Usage:
|
7
|
+
#{__FILE__} [options] <port>
|
8
|
+
|
9
|
+
Options:
|
10
|
+
-h --help show this help message and exit
|
11
|
+
--version show version and exit
|
12
|
+
-n, --number N use N as a number
|
13
|
+
-t, --timeout TIMEOUT set timeout TIMEOUT seconds
|
14
|
+
--apply apply changes to database
|
15
|
+
-q operate in quiet mode
|
16
|
+
|
17
|
+
DOCOPT
|
18
|
+
|
19
|
+
|
20
|
+
begin
|
21
|
+
puts Docopt::docopt(doc, version: '1.0.0rc2').to_s
|
22
|
+
rescue Docopt::Exit => e
|
23
|
+
puts e.message
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Usage:
|
5
|
+
#{__FILE__} tcp <host> <port> [--timeout=<seconds>]
|
6
|
+
#{__FILE__} serial <port> [--baud=9600] [--timeout=<seconds>]
|
7
|
+
#{__FILE__} -h | --help | --version
|
8
|
+
|
9
|
+
DOCOPT
|
10
|
+
|
11
|
+
begin
|
12
|
+
require "pp"
|
13
|
+
pp Docopt::docopt(doc)
|
14
|
+
rescue Docopt::Exit => e
|
15
|
+
puts e.message
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Usage: #{__FILE__} --help
|
5
|
+
#{__FILE__} -v...
|
6
|
+
#{__FILE__} go [go]
|
7
|
+
#{__FILE__} (--path=<path>)...
|
8
|
+
#{__FILE__} <file> <file>
|
9
|
+
|
10
|
+
Try: #{__FILE__} -vvvvvvvvvv
|
11
|
+
#{__FILE__} go go
|
12
|
+
#{__FILE__} --path ./here --path ./there
|
13
|
+
#{__FILE__} this.txt that.txt
|
14
|
+
|
15
|
+
DOCOPT
|
16
|
+
|
17
|
+
begin
|
18
|
+
require "pp"
|
19
|
+
pp Docopt::docopt(doc)
|
20
|
+
rescue Docopt::Exit => e
|
21
|
+
puts e.message
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Example of program with many options using docopt.
|
5
|
+
|
6
|
+
Usage:
|
7
|
+
#{__FILE__} [-hvqrf NAME] [--exclude=PATTERNS]
|
8
|
+
[--select=ERRORS | --ignore=ERRORS] [--show-source]
|
9
|
+
[--statistics] [--count] [--benchmark] PATH...
|
10
|
+
#{__FILE__} (--doctest | --testsuite=DIR)
|
11
|
+
#{__FILE__} --version
|
12
|
+
|
13
|
+
Arguments:
|
14
|
+
PATH destination path
|
15
|
+
|
16
|
+
Options:
|
17
|
+
-h --help show this help message and exit
|
18
|
+
--version show version and exit
|
19
|
+
-v --verbose print status messages
|
20
|
+
-q --quiet report only file names
|
21
|
+
-r --repeat show all occurrences of the same error
|
22
|
+
--exclude=PATTERNS exclude files or directories which match these comma
|
23
|
+
separated patterns [default: .svn,CVS,.bzr,.hg,.git]
|
24
|
+
-f NAME --file=NAME when parsing directories, only check filenames matching
|
25
|
+
these comma separated patterns [default: *#{__FILE__}]
|
26
|
+
--select=ERRORS select errors and warnings (e.g. E,W6)
|
27
|
+
--ignore=ERRORS skip errors and warnings (e.g. E4,W)
|
28
|
+
--show-source show source code for each error
|
29
|
+
--statistics count errors and warnings
|
30
|
+
--count print total number of errors and warnings to standard
|
31
|
+
error and set exit code to 1 if total is not null
|
32
|
+
--benchmark measure processing speed
|
33
|
+
--testsuite=DIR run regression tests from dir
|
34
|
+
--doctest run doctest on myself
|
35
|
+
|
36
|
+
|
37
|
+
DOCOPT
|
38
|
+
|
39
|
+
begin
|
40
|
+
require "pp"
|
41
|
+
pp Docopt::docopt(doc)
|
42
|
+
rescue Docopt::Exit => e
|
43
|
+
puts e.message
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Usage:
|
5
|
+
#{__FILE__} remote [-v | --verbose]
|
6
|
+
#{__FILE__} remote add [-t <branch>] [-m <master>] [-f]
|
7
|
+
[--tags|--no-tags] [--mirror] <name> <url>
|
8
|
+
#{__FILE__} remote rename <old> <new>
|
9
|
+
#{__FILE__} remote rm <name>
|
10
|
+
#{__FILE__} remote set-head <name> (-a | -d | <branch>)
|
11
|
+
#{__FILE__} remote set-branches <name> [--add] <branch>...
|
12
|
+
#{__FILE__} remote set-url [--push] <name> <newurl> [<oldurl>]
|
13
|
+
#{__FILE__} remote set-url --add [--push] <name> <newurl>
|
14
|
+
#{__FILE__} remote set-url --delete [--push] <name> <url>
|
15
|
+
#{__FILE__} remote [-v | --verbose] show [-n] <name>
|
16
|
+
#{__FILE__} remote prune [-n | --dry-run] <name>
|
17
|
+
#{__FILE__} remote [-v | --verbose] update [-p | --prune]
|
18
|
+
[(<group> | <remote>)...]
|
19
|
+
|
20
|
+
Options:
|
21
|
+
-v, --verbose
|
22
|
+
-t <branch>
|
23
|
+
-m <master>
|
24
|
+
-f
|
25
|
+
--tags
|
26
|
+
--no-tags
|
27
|
+
--mittor
|
28
|
+
-a
|
29
|
+
-d
|
30
|
+
-n, --dry-run
|
31
|
+
-p, --prune
|
32
|
+
--add
|
33
|
+
--delete
|
34
|
+
--push
|
35
|
+
--mirror
|
36
|
+
|
37
|
+
DOCOPT
|
38
|
+
|
39
|
+
begin
|
40
|
+
require "pp"
|
41
|
+
pp Docopt::docopt(doc)
|
42
|
+
rescue Docopt::Exit => e
|
43
|
+
puts e.message
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
#The *popular* naval fate example
|
4
|
+
|
5
|
+
doc = <<DOCOPT
|
6
|
+
Naval Fate.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
#{__FILE__} ship new <name>...
|
10
|
+
#{__FILE__} ship <name> move <x> <y> [--speed=<kn>]
|
11
|
+
#{__FILE__} ship shoot <x> <y>
|
12
|
+
#{__FILE__} mine (set|remove) <x> <y> [--moored|--drifting]
|
13
|
+
#{__FILE__} -h | --help
|
14
|
+
#{__FILE__} --version
|
15
|
+
|
16
|
+
Options:
|
17
|
+
-h --help Show this screen.
|
18
|
+
--version Show version.
|
19
|
+
--speed=<kn> Speed in knots [default: 10].
|
20
|
+
--moored Moored (anchored) mine.
|
21
|
+
--drifting Drifting mine.
|
22
|
+
|
23
|
+
DOCOPT
|
24
|
+
|
25
|
+
begin
|
26
|
+
require "pp"
|
27
|
+
pp Docopt::docopt(doc)
|
28
|
+
rescue Docopt::Exit => e
|
29
|
+
puts e.message
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Usage: #{__FILE__} [-h | --help] (ODD EVEN)...
|
5
|
+
|
6
|
+
Example, try:
|
7
|
+
#{__FILE__} 1 2 3 4
|
8
|
+
|
9
|
+
Options:
|
10
|
+
-h, --help
|
11
|
+
|
12
|
+
DOCOPT
|
13
|
+
|
14
|
+
begin
|
15
|
+
require "pp"
|
16
|
+
pp Docopt::docopt(doc)
|
17
|
+
rescue Docopt::Exit => e
|
18
|
+
puts e.message
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.expand_path("../../lib/docopt.rb", __FILE__)
|
2
|
+
|
3
|
+
doc = <<DOCOPT
|
4
|
+
Usage:
|
5
|
+
#{__FILE__} tcp <host> <port> [--timeout=<seconds>]
|
6
|
+
#{__FILE__} serial <port> [--baud=9600] [--timeout=<seconds>]
|
7
|
+
#{__FILE__} -h | --help | --version
|
8
|
+
|
9
|
+
DOCOPT
|
10
|
+
|
11
|
+
begin
|
12
|
+
require "pp"
|
13
|
+
pp Docopt::docopt(doc)
|
14
|
+
rescue Docopt::Exit => e
|
15
|
+
puts e.message
|
16
|
+
end
|
data/lib/docopt.rb
CHANGED
@@ -1,112 +1,663 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
class Option
|
9
|
-
attr_reader :short, :long, :argcount, :value
|
10
|
-
|
11
|
-
def initialize parse
|
12
|
-
@argcount = 0
|
13
|
-
options, _, description = parse.strip.partition(' ')
|
14
|
-
options = options.sub(',', ' ').sub('=', ' ')
|
1
|
+
module Docopt
|
2
|
+
VERSION = '0.5.0'
|
3
|
+
end
|
4
|
+
module Docopt
|
5
|
+
class DocoptLanguageError < SyntaxError
|
6
|
+
end
|
15
7
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
8
|
+
class Exit < RuntimeError
|
9
|
+
def self.usage
|
10
|
+
@@usage
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.set_usage(usage)
|
14
|
+
@@usage = usage ? usage : ''
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
@@message
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(message='')
|
22
|
+
@@message = ((message && message != '' ? (message + "\n") : '') + @@usage)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Pattern
|
27
|
+
attr_accessor :children
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
return self.inspect == other.inspect
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_str
|
34
|
+
return self.inspect
|
35
|
+
end
|
36
|
+
|
37
|
+
def dump
|
38
|
+
puts ::Docopt::dump_patterns(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
def fix
|
42
|
+
fix_identities
|
43
|
+
fix_list_arguments
|
44
|
+
return self
|
45
|
+
end
|
46
|
+
|
47
|
+
def fix_identities(uniq=nil)
|
48
|
+
if not instance_variable_defined?(:@children)
|
49
|
+
return self
|
50
|
+
end
|
51
|
+
uniq ||= flat.uniq
|
52
|
+
|
53
|
+
@children.each_with_index do |c, i|
|
54
|
+
if not c.instance_variable_defined?(:@children)
|
55
|
+
if !uniq.include?(c)
|
56
|
+
raise RuntimeError
|
57
|
+
end
|
58
|
+
@children[i] = uniq[uniq.index(c)]
|
21
59
|
else
|
22
|
-
|
60
|
+
c.fix_identities(uniq)
|
23
61
|
end
|
24
62
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
63
|
+
end
|
64
|
+
|
65
|
+
def fix_list_arguments
|
66
|
+
either.children.map { |c| c.children }.each do |case_|
|
67
|
+
case_.select { |c| case_.count(c) > 1 }.each do |e|
|
68
|
+
if e.class == Argument or (e.class == Option and e.argcount > 0)
|
69
|
+
e.value = []
|
70
|
+
end
|
71
|
+
if e.class == Command or (e.class == Option and e.argcount == 0)
|
72
|
+
e.value = 0
|
73
|
+
end
|
74
|
+
end
|
29
75
|
end
|
76
|
+
|
77
|
+
return self
|
30
78
|
end
|
31
|
-
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
79
|
+
|
80
|
+
def either
|
81
|
+
ret = []
|
82
|
+
groups = [[self]]
|
83
|
+
while groups.count > 0
|
84
|
+
children = groups.shift
|
85
|
+
types = children.map { |c| c.class }
|
86
|
+
|
87
|
+
if types.include?(Either)
|
88
|
+
either = children.select { |c| c.class == Either }[0]
|
89
|
+
children.slice!(children.index(either))
|
90
|
+
for c in either.children
|
91
|
+
groups << [c] + children
|
92
|
+
end
|
93
|
+
elsif types.include?(Required)
|
94
|
+
required = children.select { |c| c.class == Required }[0]
|
95
|
+
children.slice!(children.index(required))
|
96
|
+
groups << required.children + children
|
97
|
+
|
98
|
+
elsif types.include?(Optional)
|
99
|
+
optional = children.select { |c| c.class == Optional }[0]
|
100
|
+
children.slice!(children.index(optional))
|
101
|
+
groups << optional.children + children
|
102
|
+
|
103
|
+
elsif types.include?(OneOrMore)
|
104
|
+
oneormore = children.select { |c| c.class == OneOrMore }[0]
|
105
|
+
children.slice!(children.index(oneormore))
|
106
|
+
groups << (oneormore.children * 2) + children
|
107
|
+
|
108
|
+
else
|
109
|
+
ret << children
|
110
|
+
end
|
37
111
|
end
|
112
|
+
|
113
|
+
args = ret.map { |e| Required.new(*e) }
|
114
|
+
return Either.new(*args)
|
38
115
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
class ChildPattern < Pattern
|
120
|
+
attr_accessor :name, :value
|
121
|
+
|
122
|
+
def initialize(name, value=nil)
|
123
|
+
@name = name
|
124
|
+
@value = value
|
42
125
|
end
|
43
|
-
|
44
|
-
def
|
45
|
-
|
46
|
-
|
126
|
+
|
127
|
+
def inspect()
|
128
|
+
"#{self.class.name}(#{self.name}, #{self.value})"
|
129
|
+
end
|
130
|
+
|
131
|
+
def flat
|
132
|
+
[self]
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def match(left, collected=nil)
|
137
|
+
collected ||= []
|
138
|
+
pos, match = self.single_match(left)
|
139
|
+
if match == nil
|
140
|
+
return [false, left, collected]
|
47
141
|
end
|
142
|
+
|
143
|
+
left_ = left.dup
|
144
|
+
left_.slice!(pos)
|
145
|
+
|
146
|
+
same_name = collected.select { |a| a.name == self.name }
|
147
|
+
if @value.is_a? Array or @value.is_a? Integer
|
148
|
+
increment = @value.is_a?(Integer) ? 1 : [match.value]
|
149
|
+
if same_name.count == 0
|
150
|
+
match.value = increment
|
151
|
+
return [true, left_, collected + [match]]
|
152
|
+
end
|
153
|
+
same_name[0].value += increment
|
154
|
+
return [true, left_, collected]
|
155
|
+
end
|
156
|
+
return [true, left_, collected + [match]]
|
48
157
|
end
|
158
|
+
end
|
49
159
|
|
50
|
-
|
51
|
-
|
160
|
+
class ParentPattern < Pattern
|
161
|
+
attr_accessor :children
|
162
|
+
|
163
|
+
def initialize(*children)
|
164
|
+
@children = children
|
52
165
|
end
|
53
166
|
|
54
167
|
def inspect
|
55
|
-
|
168
|
+
childstr = self.children.map { |a| a.inspect }
|
169
|
+
return "#{self.class.name}(#{childstr.join(", ")})"
|
170
|
+
end
|
171
|
+
|
172
|
+
def flat
|
173
|
+
self.children.map { |c| c.flat }.flatten
|
56
174
|
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class Argument < ChildPattern
|
178
|
+
|
179
|
+
# def initialize(*args)
|
180
|
+
# super(*args)
|
181
|
+
# end
|
182
|
+
|
183
|
+
def single_match(left)
|
184
|
+
left.each_with_index do |p, n|
|
185
|
+
if p.class == Argument
|
186
|
+
return [n, Argument.new(self.name, p.value)]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
return [nil, nil]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
57
193
|
|
58
|
-
|
59
|
-
|
194
|
+
class Command < Argument
|
195
|
+
def initialize(name, value=false)
|
196
|
+
@name = name
|
197
|
+
@value = value
|
198
|
+
end
|
199
|
+
|
200
|
+
def single_match(left)
|
201
|
+
left.each_with_index do |p, n|
|
202
|
+
if p.class == Argument
|
203
|
+
if p.value == self.name
|
204
|
+
return n, Command.new(self.name, true)
|
205
|
+
else
|
206
|
+
break
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
return [nil, nil]
|
60
211
|
end
|
61
212
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
213
|
+
|
214
|
+
|
215
|
+
class Option < ChildPattern
|
216
|
+
attr_reader :short, :long
|
217
|
+
attr_accessor :argcount
|
218
|
+
|
219
|
+
def initialize(short=nil, long=nil, argcount=0, value=false)
|
220
|
+
unless [0, 1].include? argcount
|
221
|
+
raise RuntimeError
|
222
|
+
end
|
223
|
+
|
224
|
+
@short, @long = short, long
|
225
|
+
@argcount, @value = argcount, value
|
226
|
+
|
227
|
+
if value == false and argcount > 0
|
228
|
+
@value = nil
|
77
229
|
else
|
78
|
-
|
230
|
+
@value = value
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def self.parse(option_description)
|
235
|
+
short, long, argcount, value = nil, nil, 0, false
|
236
|
+
options, _, description = option_description.strip.partition(' ')
|
237
|
+
|
238
|
+
options.gsub!(",", " ")
|
239
|
+
options.gsub!("=", " ")
|
240
|
+
|
241
|
+
for s in options.split
|
242
|
+
if s.start_with?('--')
|
243
|
+
long = s
|
244
|
+
elsif s.start_with?('-')
|
245
|
+
short = s
|
246
|
+
else
|
247
|
+
argcount = 1
|
248
|
+
end
|
79
249
|
end
|
250
|
+
if argcount > 0
|
251
|
+
matched = description.scan(/\[default: (.*)\]/i)
|
252
|
+
value = matched[0][0] if matched.count > 0
|
253
|
+
end
|
254
|
+
ret = self.new(short, long, argcount, value)
|
255
|
+
return ret
|
256
|
+
end
|
257
|
+
|
258
|
+
def single_match(left)
|
259
|
+
left.each_with_index do |p, n|
|
260
|
+
if self.name == p.name
|
261
|
+
return [n, p]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
return [nil, nil]
|
265
|
+
end
|
266
|
+
|
267
|
+
def name
|
268
|
+
return self.long ? self.long : self.short
|
269
|
+
end
|
270
|
+
|
271
|
+
def inspect
|
272
|
+
return "Option(#{self.short}, #{self.long}, #{self.argcount}, #{self.value})"
|
80
273
|
end
|
81
274
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
275
|
+
|
276
|
+
class Required < ParentPattern
|
277
|
+
def match(left, collected=nil)
|
278
|
+
collected ||= []
|
279
|
+
l = left
|
280
|
+
c = collected
|
281
|
+
|
282
|
+
for p in self.children
|
283
|
+
matched, l, c = p.match(l, c)
|
284
|
+
if not matched
|
285
|
+
return [false, left, collected]
|
286
|
+
end
|
287
|
+
end
|
288
|
+
return [true, l, c]
|
86
289
|
end
|
87
|
-
raise UnknownOptionError.new("#{name} option not found") unless option
|
88
|
-
option
|
89
290
|
end
|
90
291
|
|
292
|
+
class Optional < ParentPattern
|
293
|
+
def match(left, collected=nil)
|
294
|
+
collected ||= []
|
295
|
+
for p in self.children
|
296
|
+
m, left, collected = p.match(left, collected)
|
297
|
+
end
|
298
|
+
return [true, left, collected]
|
299
|
+
end
|
300
|
+
end
|
91
301
|
|
92
|
-
|
93
|
-
|
94
|
-
|
302
|
+
class OneOrMore < ParentPattern
|
303
|
+
def match(left, collected=nil)
|
304
|
+
if self.children.count != 1
|
305
|
+
raise RuntimeError
|
306
|
+
end
|
307
|
+
|
308
|
+
collected ||= []
|
309
|
+
l = left
|
310
|
+
c = collected
|
311
|
+
l_ = nil
|
312
|
+
matched = true
|
313
|
+
times = 0
|
314
|
+
while matched
|
315
|
+
# could it be that something didn't match but changed l or c?
|
316
|
+
matched, l, c = self.children[0].match(l, c)
|
317
|
+
times += (matched ? 1 : 0)
|
318
|
+
if l_ == l
|
319
|
+
break
|
320
|
+
end
|
321
|
+
l_ = l
|
322
|
+
end
|
323
|
+
if times >= 1
|
324
|
+
return [true, l, c]
|
325
|
+
end
|
326
|
+
return [false, left, collected]
|
327
|
+
end
|
95
328
|
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
329
|
+
|
330
|
+
class Either < ParentPattern
|
331
|
+
def match(left, collected=nil)
|
332
|
+
collected ||= []
|
333
|
+
outcomes = []
|
334
|
+
for p in self.children
|
335
|
+
matched, _, _ = outcome = p.match(left, collected)
|
336
|
+
if matched
|
337
|
+
outcomes << outcome
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
if outcomes.count > 0
|
342
|
+
ret = outcomes.min_by do |outcome|
|
343
|
+
outcome[1] == nil ? 0 : outcome[1].count
|
344
|
+
end
|
345
|
+
return ret
|
346
|
+
end
|
347
|
+
return [false, left, collected]
|
348
|
+
end
|
100
349
|
end
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
350
|
+
|
351
|
+
class TokenStream < Array
|
352
|
+
attr_reader :error
|
353
|
+
|
354
|
+
def initialize(source, error)
|
355
|
+
if !source
|
356
|
+
source = []
|
357
|
+
elsif source.class != ::Array
|
358
|
+
source = source.split
|
359
|
+
end
|
360
|
+
super(source)
|
361
|
+
@error = error
|
362
|
+
end
|
363
|
+
|
364
|
+
def move
|
365
|
+
return self.shift
|
366
|
+
end
|
367
|
+
|
368
|
+
def current
|
369
|
+
return self[0]
|
370
|
+
end
|
106
371
|
end
|
107
|
-
end
|
108
372
|
|
109
|
-
|
110
|
-
def
|
111
|
-
|
112
|
-
|
373
|
+
class << self
|
374
|
+
def parse_long(tokens, options)
|
375
|
+
raw, eq, value = tokens.move().partition('=')
|
376
|
+
value = (eq == value and eq == '') ? nil : value
|
377
|
+
|
378
|
+
opt = options.select { |o| o.long and o.long == raw }
|
379
|
+
|
380
|
+
if tokens.error == Exit and opt == []
|
381
|
+
opt = options.select { |o| o.long and o.long.start_with?(raw) }
|
382
|
+
end
|
383
|
+
|
384
|
+
if opt.count < 1
|
385
|
+
if tokens.error == Exit
|
386
|
+
raise tokens.error, "#{raw} is not recognized"
|
387
|
+
else
|
388
|
+
o = Option.new(nil, raw, eq == '=' ? 1 : 0)
|
389
|
+
options << o
|
390
|
+
return [o]
|
391
|
+
end
|
392
|
+
end
|
393
|
+
if opt.count > 1
|
394
|
+
ostr = opt.map { |o| o.long }.join(', ')
|
395
|
+
raise tokens.error, "#{raw} is not a unique prefix: #{ostr}?"
|
396
|
+
end
|
397
|
+
o = opt[0]
|
398
|
+
opt = Option.new(o.short, o.long, o.argcount, o.value)
|
399
|
+
if opt.argcount == 1
|
400
|
+
if value == nil
|
401
|
+
if tokens.current() == nil
|
402
|
+
raise tokens.error, "#{opt.name} requires argument"
|
403
|
+
end
|
404
|
+
value = tokens.move()
|
405
|
+
end
|
406
|
+
elsif value != nil
|
407
|
+
raise tokens.error, "#{opt.name} must not have an argument"
|
408
|
+
end
|
409
|
+
|
410
|
+
if tokens.error == Exit
|
411
|
+
opt.value = value ? value : true
|
412
|
+
else
|
413
|
+
opt.value = value ? nil : false
|
414
|
+
end
|
415
|
+
return [opt]
|
416
|
+
end
|
417
|
+
|
418
|
+
def parse_shorts(tokens, options)
|
419
|
+
raw = tokens.move()[1..-1]
|
420
|
+
parsed = []
|
421
|
+
while raw != ''
|
422
|
+
first = raw.slice(0, 1)
|
423
|
+
opt = options.select { |o| o.short and o.short.sub(/^-+/, '').start_with?(first) }
|
424
|
+
|
425
|
+
if opt.count > 1
|
426
|
+
raise tokens.error, "-#{first} is specified ambiguously #{opt.count} times"
|
427
|
+
end
|
428
|
+
|
429
|
+
if opt.count < 1
|
430
|
+
if tokens.error == Exit
|
431
|
+
raise tokens.error, "-#{first} is not recognized"
|
432
|
+
else
|
433
|
+
o = Option.new('-' + first, nil)
|
434
|
+
options << o
|
435
|
+
parsed << o
|
436
|
+
raw = raw[1..-1]
|
437
|
+
next
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
o = opt[0]
|
442
|
+
opt = Option.new(o.short, o.long, o.argcount, o.value)
|
443
|
+
raw = raw[1..-1]
|
444
|
+
if opt.argcount == 0
|
445
|
+
value = tokens.error == Exit ? true : false
|
446
|
+
else
|
447
|
+
if raw == ''
|
448
|
+
if tokens.current() == nil
|
449
|
+
raise tokens.error, "-#{opt.short.slice(0, 1)} requires argument"
|
450
|
+
end
|
451
|
+
raw = tokens.move()
|
452
|
+
end
|
453
|
+
value, raw = raw, ''
|
454
|
+
end
|
455
|
+
|
456
|
+
if tokens.error == Exit
|
457
|
+
opt.value = value
|
458
|
+
else
|
459
|
+
opt.value = value ? nil : false
|
460
|
+
end
|
461
|
+
parsed << opt
|
462
|
+
end
|
463
|
+
return parsed
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
def parse_pattern(source, options)
|
468
|
+
tokens = TokenStream.new(source.gsub(/([\[\]\(\)\|]|\.\.\.)/, ' \1 '), DocoptLanguageError)
|
469
|
+
|
470
|
+
result = parse_expr(tokens, options)
|
471
|
+
if tokens.current() != nil
|
472
|
+
raise tokens.error, "unexpected ending: #{tokens.join(" ")}"
|
473
|
+
end
|
474
|
+
return Required.new(*result)
|
475
|
+
end
|
476
|
+
|
477
|
+
|
478
|
+
def parse_expr(tokens, options)
|
479
|
+
seq = parse_seq(tokens, options)
|
480
|
+
if tokens.current() != '|'
|
481
|
+
return seq
|
482
|
+
end
|
483
|
+
result = seq.count > 1 ? [Required.new(*seq)] : seq
|
484
|
+
|
485
|
+
while tokens.current() == '|'
|
486
|
+
tokens.move()
|
487
|
+
seq = parse_seq(tokens, options)
|
488
|
+
result += seq.count > 1 ? [Required.new(*seq)] : seq
|
489
|
+
end
|
490
|
+
return result.count > 1 ? [Either.new(*result)] : result
|
491
|
+
end
|
492
|
+
|
493
|
+
def parse_seq(tokens, options)
|
494
|
+
result = []
|
495
|
+
stop = [nil, ']', ')', '|']
|
496
|
+
while !stop.include?(tokens.current)
|
497
|
+
atom = parse_atom(tokens, options)
|
498
|
+
if tokens.current() == '...'
|
499
|
+
atom = [OneOrMore.new(*atom)]
|
500
|
+
tokens.move()
|
501
|
+
end
|
502
|
+
result += atom
|
503
|
+
end
|
504
|
+
return result
|
505
|
+
end
|
506
|
+
|
507
|
+
def parse_atom(tokens, options)
|
508
|
+
token = tokens.current()
|
509
|
+
result = []
|
510
|
+
|
511
|
+
if ['(' , '['].include? token
|
512
|
+
tokens.move()
|
513
|
+
if token == '('
|
514
|
+
matching = ')'
|
515
|
+
pattern = Required
|
516
|
+
else
|
517
|
+
matching = ']'
|
518
|
+
pattern = Optional
|
519
|
+
end
|
520
|
+
result = pattern.new(*parse_expr(tokens, options))
|
521
|
+
if tokens.move() != matching
|
522
|
+
raise tokens.error, "unmatched '#{token}'"
|
523
|
+
end
|
524
|
+
return [result]
|
525
|
+
elsif token == 'options'
|
526
|
+
tokens.move()
|
527
|
+
return options
|
528
|
+
elsif token.start_with?('--') and token != '--'
|
529
|
+
return parse_long(tokens, options)
|
530
|
+
elsif token.start_with?('-') and not ['-', '--'].include? token
|
531
|
+
return parse_shorts(tokens, options)
|
532
|
+
|
533
|
+
elsif token.start_with?('<') and token.end_with?('>') or token.upcase == token
|
534
|
+
return [Argument.new(tokens.move())]
|
535
|
+
else
|
536
|
+
return [Command.new(tokens.move())]
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
def parse_argv(source, options)
|
541
|
+
tokens = TokenStream.new(source, Exit)
|
542
|
+
parsed = []
|
543
|
+
while tokens.current() != nil
|
544
|
+
if tokens.current() == '--'
|
545
|
+
return parsed + tokens.map { |v| Argument.new(nil, v) }
|
546
|
+
elsif tokens.current().start_with?('--')
|
547
|
+
parsed += parse_long(tokens, options)
|
548
|
+
elsif tokens.current().start_with?('-') and tokens.current() != '-'
|
549
|
+
parsed += parse_shorts(tokens, options)
|
550
|
+
else
|
551
|
+
parsed << Argument.new(nil, tokens.move())
|
552
|
+
end
|
553
|
+
end
|
554
|
+
return parsed
|
555
|
+
end
|
556
|
+
|
557
|
+
def parse_doc_options(doc)
|
558
|
+
return doc.split(/^ *-|\n *-/)[1..-1].map { |s| Option.parse('-' + s) }
|
559
|
+
end
|
560
|
+
|
561
|
+
def printable_usage(doc)
|
562
|
+
usage_split = doc.split(/([Uu][Ss][Aa][Gg][Ee]:)/)
|
563
|
+
if usage_split.count < 3
|
564
|
+
raise DocoptLanguageError, '"usage:" (case-insensitive) not found.'
|
565
|
+
end
|
566
|
+
if usage_split.count > 3
|
567
|
+
raise DocoptLanguageError, 'More than one "usage:" (case-insensitive).'
|
568
|
+
end
|
569
|
+
return usage_split[1..-1].join().split(/\n\s*\n/)[0].strip
|
570
|
+
end
|
571
|
+
|
572
|
+
def formal_usage(printable_usage)
|
573
|
+
pu = printable_usage.split()[1..-1] # split and drop "usage:"
|
574
|
+
|
575
|
+
ret = []
|
576
|
+
for s in pu[1..-1]
|
577
|
+
if s == pu[0]
|
578
|
+
ret << ') | ('
|
579
|
+
else
|
580
|
+
ret << s
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
return '( ' + ret.join(' ') + ' )'
|
585
|
+
end
|
586
|
+
|
587
|
+
def dump_patterns(pattern, indent=0)
|
588
|
+
ws = " " * 4 * indent
|
589
|
+
out = ""
|
590
|
+
if pattern.class == Array
|
591
|
+
if pattern.count > 0
|
592
|
+
out << ws << "[\n"
|
593
|
+
for p in pattern
|
594
|
+
out << dump_patterns(p, indent+1).rstrip << "\n"
|
595
|
+
end
|
596
|
+
out << ws << "]\n"
|
597
|
+
else
|
598
|
+
out << ws << "[]\n"
|
599
|
+
end
|
600
|
+
|
601
|
+
elsif pattern.class.ancestors.include?(ParentPattern)
|
602
|
+
out << ws << pattern.class.name << "(\n"
|
603
|
+
for p in pattern.children
|
604
|
+
out << dump_patterns(p, indent+1).rstrip << "\n"
|
605
|
+
end
|
606
|
+
out << ws << ")\n"
|
607
|
+
|
608
|
+
else
|
609
|
+
out << ws << pattern.inspect
|
610
|
+
end
|
611
|
+
return out
|
612
|
+
end
|
613
|
+
|
614
|
+
def extras(help, version, options, doc)
|
615
|
+
ofound = false
|
616
|
+
vfound = false
|
617
|
+
for o in options
|
618
|
+
if o.value and (o.name == '-h' or o.name == '--help')
|
619
|
+
ofound = true
|
620
|
+
end
|
621
|
+
if o.value and (o.name == '--version')
|
622
|
+
vfound = true
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
if help and ofound
|
627
|
+
Exit.set_usage(nil)
|
628
|
+
raise Exit, doc.strip
|
629
|
+
end
|
630
|
+
if version and vfound
|
631
|
+
Exit.set_usage(nil)
|
632
|
+
raise Exit, version
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
def docopt(doc, params={})
|
637
|
+
default = {:version => nil, :argv => nil, :help => true}
|
638
|
+
params = default.merge(params)
|
639
|
+
params[:argv] = ARGV if !params[:argv]
|
640
|
+
|
641
|
+
Exit.set_usage(printable_usage(doc))
|
642
|
+
options = parse_doc_options(doc)
|
643
|
+
pattern = parse_pattern(formal_usage(Exit.usage), options)
|
644
|
+
argv = parse_argv(params[:argv], options)
|
645
|
+
extras(params[:help], params[:version], argv, doc)
|
646
|
+
|
647
|
+
matched, left, collected = pattern.fix().match(argv)
|
648
|
+
collected ||= []
|
649
|
+
|
650
|
+
if matched and (!left or left.count == 0)
|
651
|
+
ret = {}
|
652
|
+
for a in pattern.flat + options + collected
|
653
|
+
name = a.name
|
654
|
+
if name and name != ''
|
655
|
+
ret[name] = a.value
|
656
|
+
end
|
657
|
+
end
|
658
|
+
return ret
|
659
|
+
end
|
660
|
+
raise Exit
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|