docopt_ng 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +147 -0
- data/lib/docopt_ng/any_options.rb +6 -0
- data/lib/docopt_ng/argument.rb +23 -0
- data/lib/docopt_ng/child_pattern.rb +51 -0
- data/lib/docopt_ng/command.rb +21 -0
- data/lib/docopt_ng/docopt.rb +2 -0
- data/lib/docopt_ng/either.rb +24 -0
- data/lib/docopt_ng/exceptions.rb +18 -0
- data/lib/docopt_ng/one_or_more.rb +30 -0
- data/lib/docopt_ng/option.rb +63 -0
- data/lib/docopt_ng/optional.rb +13 -0
- data/lib/docopt_ng/parent_pattern.rb +24 -0
- data/lib/docopt_ng/pattern.rb +104 -0
- data/lib/docopt_ng/required.rb +17 -0
- data/lib/docopt_ng/token_stream.rb +23 -0
- data/lib/docopt_ng/version.rb +3 -0
- data/lib/docopt_ng.rb +290 -0
- metadata +68 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4ee12e3238546070b62a14408b24689dda48af1067ed5d139f3795a5cee2f9b9
|
4
|
+
data.tar.gz: 49082568fa660901f429539174e0826456d02d4efda581c09a1ecd1d53dd6276
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f78d00cfe0a68f4ce42f5c224ceb631bfcfe728a741babd65242443efc8f754a763c2caff34fedeed83ee4902813752b2e9a297da9019581182505f5fe555420
|
7
|
+
data.tar.gz: 269e933d636bc8b05ed19a51b0eef742611b6413205c64b2d156458d290d3cbe6f15c78b6ea3807582a60aec8a3681dd829a6ae0893f2e28f693106c466336ed
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# Docopt – command line option parser, that will make you smile
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
This is a detached fork of the original [docopt.rb](https://github.com/docopt/docopt.rb).
|
6
|
+
|
7
|
+
- For a drop-in replacement, simply `require 'docopt_ng/docopt'`
|
8
|
+
- Otherwise, use `require 'docopt_ng'` and `DocoptNG` instead of `Docopt`.
|
9
|
+
|
10
|
+
---
|
11
|
+
|
12
|
+
This is the ruby port of [`docopt`](https://github.com/docopt/docopt),
|
13
|
+
the awesome option parser written originally in python.
|
14
|
+
|
15
|
+
Isn't it awesome how `optparse` and `argparse` generate help messages
|
16
|
+
based on your code?!
|
17
|
+
|
18
|
+
*Hell no!* You know what's awesome? It's when the option parser *is* generated
|
19
|
+
based on the beautiful help message that you write yourself! This way
|
20
|
+
you don't need to write this stupid repeatable parser-code, and instead can
|
21
|
+
write only the help message--*the way you want it*.
|
22
|
+
|
23
|
+
`docopt` helps you create most beautiful command-line interfaces *easily*:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require "docopt_ng/docopt"
|
27
|
+
|
28
|
+
doc = <<DOCOPT
|
29
|
+
Naval Fate.
|
30
|
+
|
31
|
+
Usage:
|
32
|
+
#{__FILE__} ship new <name>...
|
33
|
+
#{__FILE__} ship <name> move <x> <y> [--speed=<kn>]
|
34
|
+
#{__FILE__} ship shoot <x> <y>
|
35
|
+
#{__FILE__} mine (set|remove) <x> <y> [--moored|--drifting]
|
36
|
+
#{__FILE__} -h | --help
|
37
|
+
#{__FILE__} --version
|
38
|
+
|
39
|
+
Options:
|
40
|
+
-h --help Show this screen.
|
41
|
+
--version Show version.
|
42
|
+
--speed=<kn> Speed in knots [default: 10].
|
43
|
+
--moored Moored (anchored) mine.
|
44
|
+
--drifting Drifting mine.
|
45
|
+
|
46
|
+
DOCOPT
|
47
|
+
|
48
|
+
begin
|
49
|
+
pp Docopt::docopt(doc)
|
50
|
+
rescue Docopt::Exit => e
|
51
|
+
puts e.message
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
Beat that! The option parser is generated based on the docstring above that is
|
56
|
+
passed to `docopt` function. `docopt` parses the usage pattern
|
57
|
+
(`Usage: ...`) and option descriptions (lines starting with dash "`-`") and
|
58
|
+
ensures that the program invocation matches the usage pattern; it parses
|
59
|
+
options, arguments and commands based on that. The basic idea is that
|
60
|
+
*a good help message has all necessary information in it to make a parser*.
|
61
|
+
|
62
|
+
## Installation
|
63
|
+
|
64
|
+
|
65
|
+
```shell
|
66
|
+
$ gem install docopt_ng
|
67
|
+
```
|
68
|
+
|
69
|
+
|
70
|
+
## API
|
71
|
+
|
72
|
+
`Docopt` takes 1 required and 1 optional argument:
|
73
|
+
|
74
|
+
- `doc` should be a string that
|
75
|
+
describes **options** in a human-readable format, that will be parsed to create
|
76
|
+
the option parser. The simple rules of how to write such a docstring
|
77
|
+
(in order to generate option parser from it successfully) are given in the next
|
78
|
+
section. Here is a quick example of such a string:
|
79
|
+
|
80
|
+
Usage: your_program.rb [options]
|
81
|
+
|
82
|
+
-h --help Show this.
|
83
|
+
-v --verbose Print more text.
|
84
|
+
--quiet Print less text.
|
85
|
+
-o FILE Specify output file [default: ./test.txt].
|
86
|
+
|
87
|
+
The optional second argument contains a hash of additional data to influence
|
88
|
+
docopt. The following keys are supported:
|
89
|
+
|
90
|
+
- `help`, by default `true`, specifies whether the parser should automatically
|
91
|
+
print the usage-message (supplied as `doc`) in case `-h` or `--help` options
|
92
|
+
are encountered. After showing the usage-message, the program will terminate.
|
93
|
+
If you want to handle `-h` or `--help` options manually (as all other options),
|
94
|
+
set `help=false`.
|
95
|
+
|
96
|
+
- `version`, by default `nil`, is an optional argument that specifies the
|
97
|
+
version of your program. If supplied, then, if the parser encounters
|
98
|
+
`--version` option, it will print the supplied version and terminate.
|
99
|
+
`version` could be any printable object, but most likely a string,
|
100
|
+
e.g. `'2.1.0rc1'`.
|
101
|
+
|
102
|
+
Note, when `docopt` is set to automatically handle `-h`, `--help` and
|
103
|
+
`--version` options, you still need to mention them in the options description
|
104
|
+
(`doc`) for your users to know about them.
|
105
|
+
|
106
|
+
The **return** value is just a hash with options, arguments and commands,
|
107
|
+
with keys spelled exactly like in a help message
|
108
|
+
(long versions of options are given priority). For example, if you invoke
|
109
|
+
the top example as:
|
110
|
+
|
111
|
+
```
|
112
|
+
$ naval_fate.rb ship Guardian move 100 150 --speed=15
|
113
|
+
```
|
114
|
+
|
115
|
+
the return hash will be::
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
{
|
119
|
+
"ship" => true,
|
120
|
+
"new" => false,
|
121
|
+
"<name>" => ["Guardian"],
|
122
|
+
"move" => true,
|
123
|
+
"<x>" => "100",
|
124
|
+
"<y>" => "150",
|
125
|
+
"--speed" => "15",
|
126
|
+
"shoot" => false,
|
127
|
+
"mine" => false,
|
128
|
+
"set" => false,
|
129
|
+
"remove" => false,
|
130
|
+
"--moored" => false,
|
131
|
+
"--drifting" => false,
|
132
|
+
"--help" => false,
|
133
|
+
"--version" => false
|
134
|
+
}
|
135
|
+
```
|
136
|
+
|
137
|
+
## Help message format
|
138
|
+
|
139
|
+
This port of docopt follows the docopt help message format.
|
140
|
+
You can find more details at
|
141
|
+
[official docopt git repo](https://github.com/docopt/docopt#help-message-format)
|
142
|
+
|
143
|
+
|
144
|
+
## Examples
|
145
|
+
|
146
|
+
The [examples directory](examples) contains several examples which cover most
|
147
|
+
aspects of the docopt functionality.
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'docopt_ng/child_pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class Argument < ChildPattern
|
5
|
+
def single_match(left)
|
6
|
+
left.each_with_index do |p, n|
|
7
|
+
if p.instance_of?(Argument)
|
8
|
+
return [n, Argument.new(name, p.value)]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
[nil, nil]
|
13
|
+
end
|
14
|
+
|
15
|
+
# TODO: This does not seem to be used, and can be the solution to having
|
16
|
+
# default values for arguments
|
17
|
+
def self.parse(class_, source)
|
18
|
+
name = /(<\S*?>)/.match(source)[0]
|
19
|
+
value = /\[default: (.*)\]/i.match(source)
|
20
|
+
class_.new(name, (value ? value[0] : nil))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'docopt_ng/pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class ChildPattern < Pattern
|
5
|
+
attr_accessor :name, :value
|
6
|
+
|
7
|
+
def initialize(name, value = nil)
|
8
|
+
@name = name
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"#{self.class.name}(#{name}, #{value})"
|
14
|
+
end
|
15
|
+
|
16
|
+
def flat(*types)
|
17
|
+
if types.empty? || types.include?(self.class)
|
18
|
+
[self]
|
19
|
+
else
|
20
|
+
[]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def match(left, collected = nil)
|
25
|
+
collected ||= []
|
26
|
+
pos, match = single_match(left)
|
27
|
+
|
28
|
+
return [false, left, collected] if match.nil?
|
29
|
+
|
30
|
+
left_ = left.dup
|
31
|
+
left_.slice!(pos)
|
32
|
+
|
33
|
+
same_name = collected.select { |a| a.name == name }
|
34
|
+
if @value.is_a?(Array) || @value.is_a?(Integer)
|
35
|
+
increment = if @value.is_a? Integer
|
36
|
+
1
|
37
|
+
else
|
38
|
+
match.value.is_a?(String) ? [match.value] : match.value
|
39
|
+
end
|
40
|
+
if same_name.count.zero?
|
41
|
+
match.value = increment
|
42
|
+
return [true, left_, collected + [match]]
|
43
|
+
end
|
44
|
+
same_name[0].value += increment
|
45
|
+
return [true, left_, collected]
|
46
|
+
end
|
47
|
+
|
48
|
+
[true, left_, collected + [match]]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'docopt_ng/argument'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class Command < Argument
|
5
|
+
def initialize(name, value = false)
|
6
|
+
@name = name
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def single_match(left)
|
11
|
+
left.each_with_index do |p, n|
|
12
|
+
next unless p.instance_of?(Argument)
|
13
|
+
return n, Command.new(name, true) if p.value == name
|
14
|
+
|
15
|
+
break
|
16
|
+
end
|
17
|
+
|
18
|
+
[nil, nil]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'docopt_ng/parent_pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class Either < ParentPattern
|
5
|
+
def match(left, collected = nil)
|
6
|
+
collected ||= []
|
7
|
+
outcomes = []
|
8
|
+
children.each do |p|
|
9
|
+
matched, = found = p.match(left, collected)
|
10
|
+
if matched
|
11
|
+
outcomes << found
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if outcomes.count.positive?
|
16
|
+
return outcomes.min_by do |outcome|
|
17
|
+
outcome[1].nil? ? 0 : outcome[1].count
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
[false, left, collected]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module DocoptNG
|
2
|
+
class DocoptLanguageError < SyntaxError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Exit < RuntimeError
|
6
|
+
class << self
|
7
|
+
attr_reader :usage
|
8
|
+
|
9
|
+
def set_usage(text = nil)
|
10
|
+
@usage = text || ''
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(message = '')
|
15
|
+
super "#{message}\n#{self.class.usage}".strip
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'docopt_ng/parent_pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class OneOrMore < ParentPattern
|
5
|
+
def match(left, collected = nil)
|
6
|
+
raise RuntimeError if children.count != 1
|
7
|
+
|
8
|
+
collected ||= []
|
9
|
+
l = left
|
10
|
+
c = collected
|
11
|
+
l_ = nil
|
12
|
+
matched = true
|
13
|
+
times = 0
|
14
|
+
while matched
|
15
|
+
# could it be that something didn't match but changed l or c?
|
16
|
+
matched, l, c = children[0].match(l, c)
|
17
|
+
times += (matched ? 1 : 0)
|
18
|
+
break if l_ == l
|
19
|
+
|
20
|
+
l_ = l
|
21
|
+
end
|
22
|
+
|
23
|
+
if times.positive?
|
24
|
+
[true, l, c]
|
25
|
+
else
|
26
|
+
[false, left, collected]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'docopt_ng/child_pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class Option < ChildPattern
|
5
|
+
attr_reader :short, :long
|
6
|
+
attr_accessor :argcount
|
7
|
+
|
8
|
+
def initialize(short = nil, long = nil, argcount = 0, value = false)
|
9
|
+
raise RuntimeError unless [0, 1].include? argcount
|
10
|
+
|
11
|
+
@short = short
|
12
|
+
@long = long
|
13
|
+
@argcount = argcount
|
14
|
+
@value = value
|
15
|
+
|
16
|
+
@value = if (value == false) && argcount.positive?
|
17
|
+
nil
|
18
|
+
else
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse(option_description)
|
24
|
+
short = nil
|
25
|
+
long = nil
|
26
|
+
argcount = 0
|
27
|
+
value = false
|
28
|
+
options, _, description = option_description.strip.partition(' ')
|
29
|
+
|
30
|
+
options = options.tr(',', ' ').tr('=', ' ')
|
31
|
+
|
32
|
+
options.split.each do |s|
|
33
|
+
if s.start_with?('--')
|
34
|
+
long = s
|
35
|
+
elsif s.start_with?('-')
|
36
|
+
short = s
|
37
|
+
else
|
38
|
+
argcount = 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if argcount.positive?
|
42
|
+
matched = description.scan(/\[default: (.*)\]/i)
|
43
|
+
value = matched[0][0] if matched.count.positive?
|
44
|
+
end
|
45
|
+
new(short, long, argcount, value)
|
46
|
+
end
|
47
|
+
|
48
|
+
def single_match(left)
|
49
|
+
left.each_with_index do |p, n|
|
50
|
+
return [n, p] if name == p.name
|
51
|
+
end
|
52
|
+
[nil, nil]
|
53
|
+
end
|
54
|
+
|
55
|
+
def name
|
56
|
+
long || short
|
57
|
+
end
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"Option(#{short}, #{long}, #{argcount}, #{value})"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'docopt_ng/parent_pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class Optional < ParentPattern
|
5
|
+
def match(left, collected = nil)
|
6
|
+
collected ||= []
|
7
|
+
children.each do |p|
|
8
|
+
_, left, collected = p.match(left, collected)
|
9
|
+
end
|
10
|
+
[true, left, collected]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'docopt_ng/pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class ParentPattern < Pattern
|
5
|
+
attr_accessor :children
|
6
|
+
|
7
|
+
def initialize(*children)
|
8
|
+
@children = children
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
childstr = children.map(&:inspect)
|
13
|
+
"#{self.class.name}(#{childstr.join(', ')})"
|
14
|
+
end
|
15
|
+
|
16
|
+
def flat(*types)
|
17
|
+
if types.include?(self.class)
|
18
|
+
[self]
|
19
|
+
else
|
20
|
+
children.map { |c| c.flat(*types) }.flatten
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module DocoptNG
|
2
|
+
class Pattern
|
3
|
+
attr_accessor :children
|
4
|
+
|
5
|
+
def ==(other)
|
6
|
+
inspect == other.inspect
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_str
|
10
|
+
inspect
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump
|
14
|
+
puts ::Docopt.dump_patterns(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fix
|
18
|
+
fix_identities
|
19
|
+
fix_repeating_arguments
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def fix_identities(uniq = nil)
|
24
|
+
unless instance_variable_defined?(:@children)
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
|
28
|
+
uniq ||= flat.uniq
|
29
|
+
|
30
|
+
@children.each_with_index do |c, i|
|
31
|
+
if c.instance_variable_defined?(:@children)
|
32
|
+
c.fix_identities(uniq)
|
33
|
+
else
|
34
|
+
unless uniq.include?(c)
|
35
|
+
raise RuntimeError
|
36
|
+
end
|
37
|
+
|
38
|
+
@children[i] = uniq[uniq.index(c)]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def fix_repeating_arguments
|
44
|
+
either.children.map(&:children).each do |case_|
|
45
|
+
case_.select { |c| case_.count(c) > 1 }.each do |e|
|
46
|
+
if e.instance_of?(Argument) || (e.instance_of?(Option) && e.argcount.positive?)
|
47
|
+
if e.value.nil?
|
48
|
+
e.value = []
|
49
|
+
elsif e.value.class != Array
|
50
|
+
e.value = e.value.split
|
51
|
+
end
|
52
|
+
end
|
53
|
+
if e.instance_of?(Command) || (e.instance_of?(Option) && e.argcount.zero?)
|
54
|
+
e.value = 0
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def either
|
63
|
+
ret = []
|
64
|
+
groups = [[self]]
|
65
|
+
while groups.count.positive?
|
66
|
+
children = groups.shift
|
67
|
+
types = children.map(&:class)
|
68
|
+
|
69
|
+
if types.include?(Either)
|
70
|
+
either = children.find { |child| child.instance_of?(Either) }
|
71
|
+
children.slice!(children.index(either))
|
72
|
+
either.children.each do |c|
|
73
|
+
groups << ([c] + children)
|
74
|
+
end
|
75
|
+
elsif types.include?(Required)
|
76
|
+
required = children.find { |child| child.instance_of?(Required) }
|
77
|
+
children.slice!(children.index(required))
|
78
|
+
groups << (required.children + children)
|
79
|
+
|
80
|
+
elsif types.include?(Optional)
|
81
|
+
optional = children.find { |child| child.instance_of?(Optional) }
|
82
|
+
children.slice!(children.index(optional))
|
83
|
+
groups << (optional.children + children)
|
84
|
+
|
85
|
+
elsif types.include?(AnyOptions)
|
86
|
+
anyoptions = children.find { |child| child.instance_of?(AnyOptions) }
|
87
|
+
children.slice!(children.index(anyoptions))
|
88
|
+
groups << (anyoptions.children + children)
|
89
|
+
|
90
|
+
elsif types.include?(OneOrMore)
|
91
|
+
oneormore = children.find { |child| child.instance_of?(OneOrMore) }
|
92
|
+
children.slice!(children.index(oneormore))
|
93
|
+
groups << ((oneormore.children * 2) + children)
|
94
|
+
|
95
|
+
else
|
96
|
+
ret << children
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
args = ret.map { |e| Required.new(*e) }
|
101
|
+
Either.new(*args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'docopt_ng/parent_pattern'
|
2
|
+
|
3
|
+
module DocoptNG
|
4
|
+
class Required < ParentPattern
|
5
|
+
def match(left, collected = nil)
|
6
|
+
collected ||= []
|
7
|
+
l = left
|
8
|
+
c = collected
|
9
|
+
|
10
|
+
children.each do |p|
|
11
|
+
matched, l, c = p.match(l, c)
|
12
|
+
return [false, left, collected] unless matched
|
13
|
+
end
|
14
|
+
[true, l, c]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DocoptNG
|
2
|
+
class TokenStream < Array
|
3
|
+
attr_reader :error
|
4
|
+
|
5
|
+
def initialize(source, error)
|
6
|
+
if !source
|
7
|
+
source = []
|
8
|
+
elsif source.class != ::Array
|
9
|
+
source = source.split
|
10
|
+
end
|
11
|
+
super(source)
|
12
|
+
@error = error
|
13
|
+
end
|
14
|
+
|
15
|
+
def move
|
16
|
+
shift
|
17
|
+
end
|
18
|
+
|
19
|
+
def current
|
20
|
+
self[0]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/docopt_ng.rb
ADDED
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'docopt_ng/exceptions'
|
2
|
+
require 'docopt_ng/pattern'
|
3
|
+
require 'docopt_ng/child_pattern'
|
4
|
+
require 'docopt_ng/parent_pattern'
|
5
|
+
require 'docopt_ng/argument'
|
6
|
+
require 'docopt_ng/command'
|
7
|
+
require 'docopt_ng/option'
|
8
|
+
require 'docopt_ng/required'
|
9
|
+
require 'docopt_ng/optional'
|
10
|
+
require 'docopt_ng/any_options'
|
11
|
+
require 'docopt_ng/one_or_more'
|
12
|
+
require 'docopt_ng/either'
|
13
|
+
require 'docopt_ng/token_stream'
|
14
|
+
|
15
|
+
module DocoptNG
|
16
|
+
module_function
|
17
|
+
|
18
|
+
def parse_long(tokens, options)
|
19
|
+
long, eq, value = tokens.move.partition('=')
|
20
|
+
|
21
|
+
raise RuntimeError unless long.start_with?('--')
|
22
|
+
|
23
|
+
value = nil if (eq == value) && (eq == '')
|
24
|
+
similar = options.select { |o| o.long and o.long == long }
|
25
|
+
|
26
|
+
if (tokens.error == Exit) && (similar == [])
|
27
|
+
similar = options.select { |o| o.long and o.long.start_with?(long) }
|
28
|
+
end
|
29
|
+
|
30
|
+
if similar.count > 1
|
31
|
+
ostr = similar.map(&:long).join(', ')
|
32
|
+
raise tokens.error, "#{long} is not a unique prefix: #{ostr}?"
|
33
|
+
elsif similar.count < 1
|
34
|
+
argcount = (eq == '=' ? 1 : 0)
|
35
|
+
o = Option.new(nil, long, argcount)
|
36
|
+
options << o
|
37
|
+
if tokens.error == Exit
|
38
|
+
o = Option.new(nil, long, argcount, (argcount == 1 ? value : true))
|
39
|
+
end
|
40
|
+
else
|
41
|
+
s0 = similar[0]
|
42
|
+
o = Option.new(s0.short, s0.long, s0.argcount, s0.value)
|
43
|
+
if o.argcount.zero?
|
44
|
+
unless value.nil?
|
45
|
+
raise tokens.error, "#{o.long} must not have an argument"
|
46
|
+
end
|
47
|
+
elsif value.nil?
|
48
|
+
if tokens.current.nil?
|
49
|
+
raise tokens.error, "#{o.long} requires argument"
|
50
|
+
end
|
51
|
+
|
52
|
+
value = tokens.move
|
53
|
+
end
|
54
|
+
if tokens.error == Exit
|
55
|
+
o.value = (value.nil? ? true : value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
[o]
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_shorts(tokens, options)
|
62
|
+
token = tokens.move
|
63
|
+
unless token.start_with?('-') && !token.start_with?('--')
|
64
|
+
raise RuntimeError
|
65
|
+
end
|
66
|
+
|
67
|
+
left = token[1..]
|
68
|
+
parsed = []
|
69
|
+
while left != ''
|
70
|
+
short = "-#{left[0]}"
|
71
|
+
left = left[1..]
|
72
|
+
similar = options.select { |o| o.short == short }
|
73
|
+
if similar.count > 1
|
74
|
+
raise tokens.error, "#{short} is specified ambiguously #{similar.count} times"
|
75
|
+
elsif similar.count < 1
|
76
|
+
o = Option.new(short, nil, 0)
|
77
|
+
options << o
|
78
|
+
if tokens.error == Exit
|
79
|
+
o = Option.new(short, nil, 0, true)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
s0 = similar[0]
|
83
|
+
o = Option.new(short, s0.long, s0.argcount, s0.value)
|
84
|
+
value = nil
|
85
|
+
if o.argcount != 0
|
86
|
+
if left == ''
|
87
|
+
if tokens.current.nil?
|
88
|
+
raise tokens.error, "#{short} requires argument"
|
89
|
+
end
|
90
|
+
|
91
|
+
value = tokens.move
|
92
|
+
else
|
93
|
+
value = left
|
94
|
+
left = ''
|
95
|
+
end
|
96
|
+
end
|
97
|
+
if tokens.error == Exit
|
98
|
+
o.value = (value.nil? ? true : value)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
parsed << o
|
103
|
+
end
|
104
|
+
parsed
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_pattern(source, options)
|
108
|
+
tokens = TokenStream.new(source.gsub(/([\[\]()|]|\.\.\.)/, ' \1 '), DocoptLanguageError)
|
109
|
+
|
110
|
+
result = parse_expr(tokens, options)
|
111
|
+
unless tokens.current.nil?
|
112
|
+
raise tokens.error, "unexpected ending: #{tokens.join(' ')}"
|
113
|
+
end
|
114
|
+
|
115
|
+
Required.new(*result)
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_expr(tokens, options)
|
119
|
+
seq = parse_seq(tokens, options)
|
120
|
+
if tokens.current != '|'
|
121
|
+
return seq
|
122
|
+
end
|
123
|
+
|
124
|
+
result = seq.count > 1 ? [Required.new(*seq)] : seq
|
125
|
+
|
126
|
+
while tokens.current == '|'
|
127
|
+
tokens.move
|
128
|
+
seq = parse_seq(tokens, options)
|
129
|
+
result += seq.count > 1 ? [Required.new(*seq)] : seq
|
130
|
+
end
|
131
|
+
result.count > 1 ? [Either.new(*result)] : result
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_seq(tokens, options)
|
135
|
+
result = []
|
136
|
+
stop = [nil, ']', ')', '|']
|
137
|
+
until stop.include?(tokens.current)
|
138
|
+
atom = parse_atom(tokens, options)
|
139
|
+
if tokens.current == '...'
|
140
|
+
atom = [OneOrMore.new(*atom)]
|
141
|
+
tokens.move
|
142
|
+
end
|
143
|
+
result += atom
|
144
|
+
end
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
def parse_atom(tokens, options)
|
149
|
+
token = tokens.current
|
150
|
+
|
151
|
+
if ['(', '['].include? token
|
152
|
+
tokens.move
|
153
|
+
if token == '('
|
154
|
+
matching = ')'
|
155
|
+
pattern = Required
|
156
|
+
else
|
157
|
+
matching = ']'
|
158
|
+
pattern = Optional
|
159
|
+
end
|
160
|
+
|
161
|
+
result = pattern.new(*parse_expr(tokens, options))
|
162
|
+
|
163
|
+
if tokens.move != matching
|
164
|
+
raise tokens.error, "unmatched '#{token}'"
|
165
|
+
end
|
166
|
+
|
167
|
+
[result]
|
168
|
+
elsif token == 'options'
|
169
|
+
tokens.move
|
170
|
+
[AnyOptions.new]
|
171
|
+
elsif token.start_with?('--') && (token != '--')
|
172
|
+
parse_long(tokens, options)
|
173
|
+
elsif token.start_with?('-') && !['-', '--'].include?(token)
|
174
|
+
parse_shorts(tokens, options)
|
175
|
+
elsif (token.start_with?('<') && token.end_with?('>')) || (token.upcase == token && token.match(/[A-Z]/))
|
176
|
+
[Argument.new(tokens.move)]
|
177
|
+
else
|
178
|
+
[Command.new(tokens.move)]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def parse_argv(tokens, options, options_first = false)
|
183
|
+
parsed = []
|
184
|
+
until tokens.current.nil?
|
185
|
+
if tokens.current == '--' || options_first
|
186
|
+
return parsed + tokens.map { |v| Argument.new(nil, v) }
|
187
|
+
elsif tokens.current.start_with?('--')
|
188
|
+
parsed += parse_long(tokens, options)
|
189
|
+
elsif tokens.current.start_with?('-') && (tokens.current != '-')
|
190
|
+
parsed += parse_shorts(tokens, options)
|
191
|
+
else
|
192
|
+
parsed << Argument.new(nil, tokens.move)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
parsed
|
196
|
+
end
|
197
|
+
|
198
|
+
def parse_defaults(doc)
|
199
|
+
split = doc.split(/^ *(<\S+?>|-\S+?)/).drop(1)
|
200
|
+
split = split.each_slice(2).select { |pair| pair.count == 2 }.map { |s1, s2| s1 + s2 }
|
201
|
+
split.select { |s| s.start_with?('-') }.map { |s| Option.parse(s) }
|
202
|
+
end
|
203
|
+
|
204
|
+
def printable_usage(doc)
|
205
|
+
usage_split = doc.split(/([Uu][Ss][Aa][Gg][Ee]:)/)
|
206
|
+
if usage_split.count < 3
|
207
|
+
raise DocoptLanguageError, '"usage:" (case-insensitive) not found.'
|
208
|
+
end
|
209
|
+
if usage_split.count > 3
|
210
|
+
raise DocoptLanguageError, 'More than one "usage:" (case-insensitive).'
|
211
|
+
end
|
212
|
+
|
213
|
+
usage_split.drop(1).join.split(/\n\s*\n/)[0].strip
|
214
|
+
end
|
215
|
+
|
216
|
+
def formal_usage(printable_usage)
|
217
|
+
pu = printable_usage[/usage:(.*)/mi, 1]
|
218
|
+
|
219
|
+
lines = pu.lines(chomp: true).reject(&:empty?).map do |a|
|
220
|
+
"( #{a.split.drop(1).join(' ')} )"
|
221
|
+
end
|
222
|
+
|
223
|
+
lines.join ' | '
|
224
|
+
end
|
225
|
+
|
226
|
+
def dump_patterns(pattern, indent = 0)
|
227
|
+
ws = ' ' * 4 * indent
|
228
|
+
out = ''
|
229
|
+
if pattern.instance_of?(Array)
|
230
|
+
if pattern.count.positive?
|
231
|
+
out << ws << "[\n"
|
232
|
+
pattern.each do |p|
|
233
|
+
out << dump_patterns(p, indent + 1).rstrip << "\n"
|
234
|
+
end
|
235
|
+
out << ws << "]\n"
|
236
|
+
else
|
237
|
+
out << ws << "[]\n"
|
238
|
+
end
|
239
|
+
|
240
|
+
elsif pattern.class.ancestors.include?(ParentPattern)
|
241
|
+
out << ws << pattern.class.name << "(\n"
|
242
|
+
pattern.children.each do |p|
|
243
|
+
out << dump_patterns(p, indent + 1).rstrip << "\n"
|
244
|
+
end
|
245
|
+
out << ws << ")\n"
|
246
|
+
|
247
|
+
else
|
248
|
+
out << ws << pattern.inspect
|
249
|
+
end
|
250
|
+
out
|
251
|
+
end
|
252
|
+
|
253
|
+
def extras(help, version, options, doc)
|
254
|
+
help_flags = ['-h', '--help']
|
255
|
+
if help && options.any? { |o| help_flags.include?(o.name) && o.value }
|
256
|
+
Exit.set_usage(nil)
|
257
|
+
raise Exit, doc.strip
|
258
|
+
end
|
259
|
+
return unless version && options.any? { |o| o.name == '--version' && o.value }
|
260
|
+
|
261
|
+
Exit.set_usage(nil)
|
262
|
+
raise Exit, version
|
263
|
+
end
|
264
|
+
|
265
|
+
def docopt(doc, params = {})
|
266
|
+
default = { version: nil, argv: nil, help: true, options_first: false }
|
267
|
+
params = default.merge(params)
|
268
|
+
params[:argv] = ARGV unless params[:argv]
|
269
|
+
|
270
|
+
Exit.set_usage(printable_usage(doc))
|
271
|
+
options = parse_defaults(doc)
|
272
|
+
pattern = parse_pattern(formal_usage(Exit.usage), options)
|
273
|
+
argv = parse_argv(TokenStream.new(params[:argv], Exit), options, params[:options_first])
|
274
|
+
pattern_options = pattern.flat(Option).uniq
|
275
|
+
pattern.flat(AnyOptions).each do |ao|
|
276
|
+
doc_options = parse_defaults(doc)
|
277
|
+
ao.children = doc_options.reject { |o| pattern_options.include?(o) }.uniq
|
278
|
+
end
|
279
|
+
extras(params[:help], params[:version], argv, doc)
|
280
|
+
|
281
|
+
matched, left, collected = pattern.fix.match(argv)
|
282
|
+
collected ||= []
|
283
|
+
|
284
|
+
if matched && left.count.zero?
|
285
|
+
return (pattern.flat + collected).to_h { |a| [a.name, a.value] }
|
286
|
+
end
|
287
|
+
|
288
|
+
raise Exit
|
289
|
+
end
|
290
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docopt_ng
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Blake Williams
|
8
|
+
- Vladimir Keleshev
|
9
|
+
- Alex Speller
|
10
|
+
- Nima Johari
|
11
|
+
- Danny Ben Shitrit
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
date: 2023-02-02 00:00:00.000000000 Z
|
16
|
+
dependencies: []
|
17
|
+
description: Parse command line arguments from nothing more than a usage message
|
18
|
+
email: db@dannyben.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- README.md
|
24
|
+
- lib/docopt_ng.rb
|
25
|
+
- lib/docopt_ng/any_options.rb
|
26
|
+
- lib/docopt_ng/argument.rb
|
27
|
+
- lib/docopt_ng/child_pattern.rb
|
28
|
+
- lib/docopt_ng/command.rb
|
29
|
+
- lib/docopt_ng/docopt.rb
|
30
|
+
- lib/docopt_ng/either.rb
|
31
|
+
- lib/docopt_ng/exceptions.rb
|
32
|
+
- lib/docopt_ng/one_or_more.rb
|
33
|
+
- lib/docopt_ng/option.rb
|
34
|
+
- lib/docopt_ng/optional.rb
|
35
|
+
- lib/docopt_ng/parent_pattern.rb
|
36
|
+
- lib/docopt_ng/pattern.rb
|
37
|
+
- lib/docopt_ng/required.rb
|
38
|
+
- lib/docopt_ng/token_stream.rb
|
39
|
+
- lib/docopt_ng/version.rb
|
40
|
+
homepage: https://github.com/dannyben/docopt_ng
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata:
|
44
|
+
bug_tracker_uri: https://github.com/DannyBen/docopt_ng/issues
|
45
|
+
changelog_uri: https://github.com/DannyBen/docopt_ng/blob/master/CHANGELOG.md
|
46
|
+
homepage_uri: https://docopt.org/
|
47
|
+
source_code_uri: https://github.com/DannyBen/docopt_ng
|
48
|
+
rubygems_mfa_required: 'true'
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 2.7.0
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubygems_version: 3.4.6
|
65
|
+
signing_key:
|
66
|
+
specification_version: 4
|
67
|
+
summary: A command line option parser that will make you smile
|
68
|
+
test_files: []
|