docopt_ng 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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: []