docopt_ng 0.7.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.
- 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: []
|