docopt 0.0.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|