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 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,6 @@
1
+ require 'docopt_ng/optional'
2
+
3
+ module DocoptNG
4
+ class AnyOptions < Optional
5
+ end
6
+ end
@@ -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,2 @@
1
+ require 'docopt_ng'
2
+ Docopt = DocoptNG
@@ -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
@@ -0,0 +1,3 @@
1
+ module DocoptNG
2
+ VERSION = '0.7.0'
3
+ 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: []