shellopts 2.4.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -14
- data/TODO +22 -0
- data/lib/shellopts/analyzer.rb +10 -10
- data/lib/shellopts/args.rb +2 -2
- data/lib/shellopts/argument_type.rb +8 -8
- data/lib/shellopts/dump.rb +8 -8
- data/lib/shellopts/formatter.rb +8 -8
- data/lib/shellopts/grammar.rb +5 -4
- data/lib/shellopts/interpreter.rb +12 -4
- data/lib/shellopts/lexer.rb +9 -9
- data/lib/shellopts/option.rb +27 -0
- data/lib/shellopts/parser.rb +8 -8
- data/lib/shellopts/program.rb +15 -40
- data/lib/shellopts/renderer.rb +13 -6
- data/lib/shellopts/token.rb +4 -4
- data/lib/shellopts/version.rb +1 -1
- data/lib/shellopts.rb +19 -18
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc8c49d6d58b38a3fdff578630ef13abe8f3c6c5939cf5ff8098198eb8a17fbe
|
4
|
+
data.tar.gz: 181c71564cd5a755bc3e17a19652748b8a276c2cd910a25c1f8d3d39bd6d27c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6fb75cc8baf4738276d25ebddb0dd7827ff816cd124486e5ac681f3fe2d9f80aee1bd9bc2d409eec4e65d476d897c481505f95ca60eb27b70259aac548657b1
|
7
|
+
data.tar.gz: ef3e4129aea6baab3b683b25c51bd77d0df222c0cfb18effc0b26e9d8497e66fc2735c32e53e1b67030030824e78bc4fe59c7fd8f17d6e0c8daffb31949f9434
|
data/README.md
CHANGED
@@ -74,11 +74,11 @@ to check presence and return an optional argument. Given the options "--alpha
|
|
74
74
|
|
75
75
|
```ruby
|
76
76
|
# Returns true if the option is present and false otherwise
|
77
|
-
opts.alpha?()
|
77
|
+
opts.alpha?()
|
78
78
|
opts.beta?()
|
79
79
|
|
80
80
|
# Returns the argument of the beta option or nil if missing
|
81
|
-
opts.beta()
|
81
|
+
opts.beta()
|
82
82
|
```
|
83
83
|
|
84
84
|
Given the commands "cmd1! cmd2!" the following methods are available:
|
@@ -87,7 +87,7 @@ Given the commands "cmd1! cmd2!" the following methods are available:
|
|
87
87
|
# Returns the sub-command object or nil if not present
|
88
88
|
opts.cmd1!
|
89
89
|
opts.cmd2!
|
90
|
-
|
90
|
+
|
91
91
|
opts.subcommand! # Returns the sub-command object or nil if not present
|
92
92
|
opts.subcommand # Returns the sub-command's identifier (eg. :cmd1!)
|
93
93
|
```
|
@@ -96,7 +96,7 @@ It is used like this
|
|
96
96
|
|
97
97
|
```ruby
|
98
98
|
case opts.subcommand
|
99
|
-
when :cmd1
|
99
|
+
when :cmd1
|
100
100
|
# opts.cmd1 is defined here
|
101
101
|
when :cmd2
|
102
102
|
# opts.cmd2 is defined here
|
@@ -160,7 +160,7 @@ is paragraphs
|
|
160
160
|
-a,alpha @ Brief comment for -a and --alpha options
|
161
161
|
Longer description of the option that is used by `::help`
|
162
162
|
|
163
|
-
cmd!
|
163
|
+
cmd!
|
164
164
|
@ Alternative style of brief comment
|
165
165
|
|
166
166
|
Longer description of the command
|
@@ -179,23 +179,28 @@ error messages:
|
|
179
179
|
The general syntax for options is
|
180
180
|
|
181
181
|
```
|
182
|
-
<prefix><optionlist>[=argspec][?]
|
182
|
+
<prefix><optionlist>[=argspec][,][?]
|
183
183
|
```
|
184
184
|
|
185
|
+
(TODO: Restructure: prefix,options,argument-spec,argument-spec-modifier,etc.)
|
186
|
+
|
185
187
|
The option list is a comma-separated list of option names. It is prefixed with
|
186
188
|
a '-' if the option list starts with a short option name and '--' if the option
|
187
189
|
list starts with a long name. '-' and '--' can be replaced with '+' or '++' to
|
188
190
|
indicate that the option can be repeated
|
189
191
|
|
190
192
|
```
|
191
|
-
-a,alpha
|
192
|
-
++beta
|
193
|
-
--gamma=ARG?
|
193
|
+
-a,alpha @ '-a' and '--alpha'
|
194
|
+
++beta @ '--beta', can be repeated
|
195
|
+
--gamma=ARG? @ '--gamma', takes an optional argument
|
196
|
+
--delta=ARG, @ '--delta', takes a mandatory comma-separated list of arguments
|
197
|
+
--epsilon=ARG,? @ '--delta', takes an optional list
|
194
198
|
```
|
195
199
|
|
196
200
|
An option argument has a name and a type. The type can be specified as '#'
|
197
|
-
(integer), '$' (float), or as a comma-separated list of allowed
|
198
|
-
|
201
|
+
(integer), '$' (float), ',' (list) or as a comma-separated list of allowed
|
202
|
+
values (enum). The name should be in capital letters. Some names are keywords
|
203
|
+
with a special meaning:
|
199
204
|
|
200
205
|
| Keyword | Type |
|
201
206
|
| --------- | ---- |
|
@@ -219,7 +224,7 @@ explicitly by separating it from the type with a ':'. Examples:
|
|
219
224
|
-c=red,blue,green @ -c takes one of the listed words
|
220
225
|
-d=FILE @ Fails if file exists and is not a file
|
221
226
|
-d=EDIR @ Fails if directory doesn't exist or is not a directory
|
222
|
-
-d=INPUT:EFILE @
|
227
|
+
-d=INPUT:EFILE @ Expects an existing file. Shown as '-d=INPUT' in messages
|
223
228
|
```
|
224
229
|
|
225
230
|
## Commands
|
@@ -234,7 +239,7 @@ command line like this
|
|
234
239
|
cmd!
|
235
240
|
-b @ Command level option
|
236
241
|
subcmd!
|
237
|
-
-c @ Sub-command level option
|
242
|
+
-c @ Sub-command level option
|
238
243
|
```
|
239
244
|
|
240
245
|
In single-line format, subcommands are specified by prefixing the supercommand's name:
|
@@ -256,7 +261,7 @@ SPEC = %(
|
|
256
261
|
-f,force @ ignore nonexisten files and arguments, never prompt
|
257
262
|
-i @ prompt before every removal
|
258
263
|
|
259
|
-
-I
|
264
|
+
-I
|
260
265
|
@ prompt once
|
261
266
|
|
262
267
|
prompt once before removing more than three files, or when removing
|
data/TODO
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
# TODO
|
2
|
+
o A program framework to make it easier to handle commands:
|
3
|
+
class Program
|
4
|
+
def check!() end
|
5
|
+
def fix!() end
|
6
|
+
def list!() end
|
7
|
+
|
8
|
+
def initialize(spec, argv)
|
9
|
+
end
|
10
|
+
|
11
|
+
def command(cmd)
|
12
|
+
self.send(cmd.subcommand)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
o We really need a global #indent/#outdent set of methods
|
17
|
+
o Make it possible to override the documentation of built-in options (ex. --verbose)
|
18
|
+
o Create global IO-like objects for each output channel: $mesg, $verb, $notice,
|
19
|
+
$warn, $error, $failure so that they can be used together with the usual
|
20
|
+
output functions #puts, #print, #printf, #indent etc.
|
21
|
+
|
22
|
+
ShellOpts.mesg "string" -> $mesg.puts "string"
|
1
23
|
|
2
24
|
o Remove IFILE and OFILE. Alternatively document them
|
3
25
|
o Document special-case file argument '-'
|
data/lib/shellopts/analyzer.rb
CHANGED
@@ -14,8 +14,8 @@ module ShellOpts
|
|
14
14
|
children.delete_if { |node| node.is_a?(ArgSpec) }
|
15
15
|
end
|
16
16
|
|
17
|
-
def analyzer_error(token, message)
|
18
|
-
raise AnalyzerError.new(token), message
|
17
|
+
def analyzer_error(token, message)
|
18
|
+
raise AnalyzerError.new(token), message
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -27,7 +27,7 @@ module ShellOpts
|
|
27
27
|
# Move options before first command or before explicit COMMAND section
|
28
28
|
def reorder_options
|
29
29
|
if commands.any?
|
30
|
-
i = children.find_index { |child|
|
30
|
+
i = children.find_index { |child|
|
31
31
|
child.is_a?(Command) || child.is_a?(Section) && child.name == "COMMAND"
|
32
32
|
}
|
33
33
|
if i
|
@@ -40,10 +40,10 @@ module ShellOpts
|
|
40
40
|
def compute_option_hashes
|
41
41
|
options.each { |option|
|
42
42
|
option.idents.zip(option.names).each { |ident, name|
|
43
|
-
!@options_hash.key?(name) or
|
43
|
+
!@options_hash.key?(name) or
|
44
44
|
analyzer_error option.token, "Duplicate option name: #{name}"
|
45
45
|
@options_hash[name] = option
|
46
|
-
!@options_hash.key?(ident) or
|
46
|
+
!@options_hash.key?(ident) or
|
47
47
|
analyzer_error option.token, "Can't use both #{@options_hash[ident].name} and #{name}"
|
48
48
|
@options_hash[ident] = option
|
49
49
|
}
|
@@ -53,7 +53,7 @@ module ShellOpts
|
|
53
53
|
# TODO Check for dash-collision
|
54
54
|
def compute_command_hashes
|
55
55
|
commands.each { |command|
|
56
|
-
!@commands_hash.key?(command.name) or
|
56
|
+
!@commands_hash.key?(command.name) or
|
57
57
|
analyzer_error command.token, "Duplicate command name: #{command.name}"
|
58
58
|
@commands_hash[command.name] = command
|
59
59
|
@commands_hash[command.ident] = command
|
@@ -75,7 +75,7 @@ module ShellOpts
|
|
75
75
|
def create_implicit_commands(cmd)
|
76
76
|
path = cmd.path[0..-2]
|
77
77
|
|
78
|
-
|
78
|
+
|
79
79
|
end
|
80
80
|
|
81
81
|
# Link up commands with supercommands. This is only done for commands that
|
@@ -83,7 +83,7 @@ module ShellOpts
|
|
83
83
|
# parent/child relationship is not changed Example:
|
84
84
|
#
|
85
85
|
# cmd!
|
86
|
-
# cmd.subcmd!
|
86
|
+
# cmd.subcmd!
|
87
87
|
#
|
88
88
|
# Here subcmd is added to cmd's list of commands. It keeps its position in
|
89
89
|
# the program's parent/child relationship so that documentation will print the
|
@@ -147,8 +147,8 @@ module ShellOpts
|
|
147
147
|
|
148
148
|
@grammar.compute_command_hashes
|
149
149
|
|
150
|
-
@grammar.traverse { |node|
|
151
|
-
node.remove_brief_nodes
|
150
|
+
@grammar.traverse { |node|
|
151
|
+
node.remove_brief_nodes
|
152
152
|
node.remove_arg_descr_nodes
|
153
153
|
node.remove_arg_spec_nodes
|
154
154
|
}
|
data/lib/shellopts/args.rb
CHANGED
@@ -21,7 +21,7 @@ module ShellOpts
|
|
21
21
|
# array. If the count is negative, the elements will be removed from the
|
22
22
|
# end of the array. If +count_or_range+ is a range, the number of elements
|
23
23
|
# returned will be in that range. Note that the range can't contain
|
24
|
-
# negative numbers
|
24
|
+
# negative numbers
|
25
25
|
#
|
26
26
|
# #extract raise a ShellOpts::Error exception if there's is not enough
|
27
27
|
# elements in the array to satisfy the request
|
@@ -51,7 +51,7 @@ module ShellOpts
|
|
51
51
|
# As #extract except the array is expected to be emptied by the operation.
|
52
52
|
# Raise a #inoa exception if count is negative
|
53
53
|
#
|
54
|
-
# #expect raise a ShellOpts::Error exception if the array is not emptied
|
54
|
+
# #expect raise a ShellOpts::Error exception if the array is not emptied
|
55
55
|
# by the operation
|
56
56
|
#
|
57
57
|
# TODO: Better handling of ranges. Allow: 2..-1, -2..-4, etc.
|
@@ -37,9 +37,9 @@ module ShellOpts
|
|
37
37
|
end
|
38
38
|
|
39
39
|
class IntegerArgument < ArgumentType
|
40
|
-
def match?(name, literal)
|
41
|
-
literal =~ /^-?\d+$/ or
|
42
|
-
set_message "Illegal integer value in #{name}: #{literal}"
|
40
|
+
def match?(name, literal)
|
41
|
+
literal =~ /^-?\d+$/ or
|
42
|
+
set_message "Illegal integer value in #{name}: #{literal}"
|
43
43
|
end
|
44
44
|
|
45
45
|
def value?(value) value.is_a?(Integer) end
|
@@ -47,9 +47,9 @@ module ShellOpts
|
|
47
47
|
end
|
48
48
|
|
49
49
|
class FloatArgument < ArgumentType
|
50
|
-
def match?(name, literal)
|
50
|
+
def match?(name, literal)
|
51
51
|
# https://stackoverflow.com/a/21891705/2130986
|
52
|
-
literal =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
|
52
|
+
literal =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
|
53
53
|
set_message "Illegal decimal value in #{name}: #{literal}"
|
54
54
|
end
|
55
55
|
|
@@ -61,7 +61,7 @@ module ShellOpts
|
|
61
61
|
attr_reader :kind
|
62
62
|
|
63
63
|
def subject # Used in error messages
|
64
|
-
@subject ||=
|
64
|
+
@subject ||=
|
65
65
|
case kind
|
66
66
|
when :file, :efile; "regular file"
|
67
67
|
when :nfile, :ifile, :ofile; "file"
|
@@ -74,7 +74,7 @@ module ShellOpts
|
|
74
74
|
|
75
75
|
def initialize(kind)
|
76
76
|
constrain kind, :file, :dir, :path, :efile, :edir, :epath, :nfile, :ndir, :npath, :ifile, :ofile
|
77
|
-
@kind = kind
|
77
|
+
@kind = kind
|
78
78
|
end
|
79
79
|
|
80
80
|
def match?(name, literal)
|
@@ -168,7 +168,7 @@ module ShellOpts
|
|
168
168
|
end
|
169
169
|
|
170
170
|
# file does not exist
|
171
|
-
else
|
171
|
+
else
|
172
172
|
if [:default, :new].include? mode
|
173
173
|
dir = File.dirname(literal)
|
174
174
|
if !File.directory?(dir)
|
data/lib/shellopts/dump.rb
CHANGED
@@ -79,9 +79,9 @@ module ShellOpts
|
|
79
79
|
def dump_idr(short = false)
|
80
80
|
if short
|
81
81
|
s = [
|
82
|
-
name,
|
83
|
-
argument? ? argument_type.name : nil,
|
84
|
-
optional? ? "?" : nil,
|
82
|
+
name,
|
83
|
+
argument? ? argument_type.name : nil,
|
84
|
+
optional? ? "?" : nil,
|
85
85
|
repeatable? ? "*" : nil
|
86
86
|
].compact.join(" ")
|
87
87
|
puts s
|
@@ -89,9 +89,9 @@ module ShellOpts
|
|
89
89
|
puts "#{name}: #{classname}"
|
90
90
|
dump_attrs(
|
91
91
|
:uid, :path, :attr, :ident, :name, :idents, :names,
|
92
|
-
:repeatable?,
|
93
|
-
:argument?, argument? && :argument_name, argument? && :argument_type,
|
94
|
-
:enum?, enum? && :argument_enum,
|
92
|
+
:repeatable?,
|
93
|
+
:argument?, argument? && :argument_name, argument? && :argument_type,
|
94
|
+
:enum?, enum? && :argument_enum,
|
95
95
|
:optional?)
|
96
96
|
indent { puts "brief: #{group.brief}" }
|
97
97
|
end
|
@@ -102,11 +102,11 @@ module ShellOpts
|
|
102
102
|
def dump_idr(short = false)
|
103
103
|
if short
|
104
104
|
puts name
|
105
|
-
indent {
|
105
|
+
indent {
|
106
106
|
options.each { |option| option.dump_idr(short) }
|
107
107
|
commands.each { |command| command.dump_idr(short) }
|
108
108
|
descrs.each { |descr| descr.dump_idr(short) }
|
109
|
-
}
|
109
|
+
}
|
110
110
|
else
|
111
111
|
puts "#{name}: #{classname}"
|
112
112
|
dump_attrs :uid, :path, :ident, :name, :options, :commands, :specs, :descrs, :brief
|
data/lib/shellopts/formatter.rb
CHANGED
@@ -34,15 +34,15 @@ module ShellOpts
|
|
34
34
|
width = [Formatter.rest, max_width].min
|
35
35
|
if descrs.size == 0
|
36
36
|
print (lead = Formatter.command_prefix || "")
|
37
|
-
indent(lead.size, ' ', bol: bol && lead == "") {
|
38
|
-
puts render(:multi, width)
|
37
|
+
indent(lead.size, ' ', bol: bol && lead == "") {
|
38
|
+
puts render(:multi, width)
|
39
39
|
}
|
40
40
|
else
|
41
41
|
lead = Formatter.command_prefix || ""
|
42
42
|
descrs.each { |descr|
|
43
43
|
print lead
|
44
|
-
puts render(:multi, width, args: descr.text.split(' '))
|
45
|
-
}
|
44
|
+
puts render(:multi, width, args: descr.text.split(' '))
|
45
|
+
}
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
@@ -120,7 +120,7 @@ module ShellOpts
|
|
120
120
|
Command => "COMMAND"
|
121
121
|
}
|
122
122
|
seen_sections = {}
|
123
|
-
newline = false # True if a newline should be printed before child
|
123
|
+
newline = false # True if a newline should be printed before child
|
124
124
|
indent {
|
125
125
|
children.each { |child|
|
126
126
|
klass = child.is_a?(Section) ? section.key(child.name) : child.class
|
@@ -174,7 +174,7 @@ module ShellOpts
|
|
174
174
|
module WrappedNode
|
175
175
|
def puts_descr
|
176
176
|
width = [Formatter.rest, Formatter::HELP_MAX_WIDTH].min
|
177
|
-
puts lines(width)
|
177
|
+
puts lines(width)
|
178
178
|
end
|
179
179
|
end
|
180
180
|
|
@@ -220,7 +220,7 @@ module ShellOpts
|
|
220
220
|
HELP_INDENT = 4
|
221
221
|
|
222
222
|
# Max. width of help text (not including indent)
|
223
|
-
HELP_MAX_WIDTH = 85
|
223
|
+
HELP_MAX_WIDTH = 85
|
224
224
|
|
225
225
|
# Command prefix when subject is a sub-command
|
226
226
|
def self.command_prefix() @command_prefix end
|
@@ -279,7 +279,7 @@ module ShellOpts
|
|
279
279
|
# +fields+ is an array of [subject-string, descr-text] tuples where the
|
280
280
|
# descr is an array of words
|
281
281
|
def self.compute_columns(width, fields)
|
282
|
-
first_max =
|
282
|
+
first_max =
|
283
283
|
fields.map { |first, _| first.size }.select { |size| size <= BRIEF_COL1_MAX_WIDTH }.max ||
|
284
284
|
BRIEF_COL1_MIN_WIDTH
|
285
285
|
second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1 : 0 }.max
|
data/lib/shellopts/grammar.rb
CHANGED
@@ -54,8 +54,8 @@ module ShellOpts
|
|
54
54
|
# for the Program object. It is the dot-joined elements of path with
|
55
55
|
# internal exclamation marks removed (eg. "cmd.opt" or "cmd.cmd!").
|
56
56
|
# Initialize by the analyzer
|
57
|
-
def uid()
|
58
|
-
@uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
|
57
|
+
def uid()
|
58
|
+
@uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
|
59
59
|
end
|
60
60
|
|
61
61
|
# Path from Program object and down to this node. Array of identifiers.
|
@@ -142,6 +142,7 @@ module ShellOpts
|
|
142
142
|
def repeatable?() @repeatable end
|
143
143
|
def argument?() @argument end
|
144
144
|
def optional?() @optional end
|
145
|
+
def list?() @list end
|
145
146
|
|
146
147
|
def integer?() @argument_type.is_a? IntegerArgument end
|
147
148
|
def float?() @argument_type.is_a? FloatArgument end
|
@@ -250,7 +251,7 @@ module ShellOpts
|
|
250
251
|
|
251
252
|
def key?(key) !self.[](key).nil? end
|
252
253
|
|
253
|
-
# Mostly for debug. Has questional semantics because it only lists local keys
|
254
|
+
# Mostly for debug. Has questional semantics because it only lists local keys
|
254
255
|
def keys() @options_hash.keys + @commands_hash.keys end
|
255
256
|
|
256
257
|
# Shorthand to get the associated Grammar::Command object from a Program
|
@@ -312,7 +313,7 @@ module ShellOpts
|
|
312
313
|
alias_method :spec, :parent
|
313
314
|
end
|
314
315
|
|
315
|
-
# DocNode object has no children but lines.
|
316
|
+
# DocNode object has no children but lines.
|
316
317
|
#
|
317
318
|
class DocNode < Node
|
318
319
|
# Array of :text tokens. Assigned by the parser
|
@@ -80,15 +80,23 @@ module ShellOpts
|
|
80
80
|
elsif !value.nil?
|
81
81
|
error "No argument allowed for option '#{opt_name}'"
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
Command.add_option(option_command, Option.new(option, name, value))
|
85
85
|
end
|
86
86
|
|
87
87
|
def interpret_option_value(option, name, value)
|
88
|
+
if option.list?
|
89
|
+
value.split(",").map { |elem| interpret_option_value_element(option, name, elem) }
|
90
|
+
else
|
91
|
+
interpret_option_value_element(option, name, value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def interpret_option_value_element(option, name, elem)
|
88
96
|
type = option.argument_type
|
89
|
-
if type.match?(name,
|
90
|
-
type.convert(
|
91
|
-
elsif
|
97
|
+
if type.match?(name, elem)
|
98
|
+
type.convert(elem)
|
99
|
+
elsif elem == ""
|
92
100
|
nil
|
93
101
|
else
|
94
102
|
error type.message
|
data/lib/shellopts/lexer.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
module ShellOpts
|
3
3
|
class Line
|
4
4
|
attr_reader :source
|
5
|
-
attr_reader :lineno
|
5
|
+
attr_reader :lineno
|
6
6
|
attr_reader :charno
|
7
7
|
attr_reader :text
|
8
8
|
|
@@ -52,7 +52,7 @@ module ShellOpts
|
|
52
52
|
attr_reader :name # Name of program
|
53
53
|
attr_reader :source
|
54
54
|
attr_reader :tokens
|
55
|
-
|
55
|
+
|
56
56
|
def oneline?() @oneline end
|
57
57
|
|
58
58
|
def initialize(name, source, oneline)
|
@@ -88,7 +88,7 @@ module ShellOpts
|
|
88
88
|
@tokens << Token.new(:blank, line.lineno, line.charno, "")
|
89
89
|
next
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
92
|
# Ignore meta comments
|
93
93
|
if line.charno < initial_indent
|
94
94
|
next if line =~ /^#/
|
@@ -129,8 +129,8 @@ module ShellOpts
|
|
129
129
|
@tokens << Token.new(:usage_string, line.lineno, charno, source)
|
130
130
|
when "++" # FIXME Rename argspec
|
131
131
|
@tokens << Token.new(:spec, line.lineno, charno, "++")
|
132
|
-
words.shift_while { |c,w|
|
133
|
-
w =~ SPEC_RE and @tokens << Token.new(:argument, line.lineno, c, w)
|
132
|
+
words.shift_while { |c,w|
|
133
|
+
w =~ SPEC_RE and @tokens << Token.new(:argument, line.lineno, c, w)
|
134
134
|
}
|
135
135
|
when /^-|\+/
|
136
136
|
@tokens << Token.new(:option, line.lineno, charno, word)
|
@@ -143,7 +143,7 @@ module ShellOpts
|
|
143
143
|
end
|
144
144
|
|
145
145
|
# TODO: Move to parser and remove @oneline from Lexer
|
146
|
-
(token = @tokens.last).kind != :brief || !oneline? or
|
146
|
+
(token = @tokens.last).kind != :brief || !oneline? or
|
147
147
|
lexer_error token, "Briefs are only allowed in multi-line specifications"
|
148
148
|
|
149
149
|
# Paragraph lines
|
@@ -151,13 +151,13 @@ module ShellOpts
|
|
151
151
|
@tokens << Token.new(:text, line.lineno, line.charno, source)
|
152
152
|
end
|
153
153
|
# FIXME Not sure about this
|
154
|
-
# last_nonblank = @tokens.last
|
155
|
-
last_nonblank = @tokens.last if ![:blank, :usage_string, :argument].include? @tokens.last.kind
|
154
|
+
# last_nonblank = @tokens.last
|
155
|
+
last_nonblank = @tokens.last if ![:blank, :usage_string, :argument].include? @tokens.last.kind
|
156
156
|
end
|
157
157
|
|
158
158
|
# Move arguments and briefs before first command if one-line source
|
159
159
|
# if oneline? && cmd_index = @tokens.index { |token| token.kind == :command }
|
160
|
-
# @tokens =
|
160
|
+
# @tokens =
|
161
161
|
# @tokens[0...cmd_index] +
|
162
162
|
# @tokens[cmd_index..-1].partition { |token| ![:command, :option].include?(token.kind) }.flatten
|
163
163
|
# end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
# Option models an option as given by the user on the subcommand line.
|
3
|
+
# Compiled options (and possibly aggregated) options are stored in the
|
4
|
+
# Command#__option_values__ array
|
5
|
+
class Option
|
6
|
+
# Associated Grammar::Option object
|
7
|
+
attr_reader :grammar
|
8
|
+
|
9
|
+
# The actual name used on the shell command-line (String)
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Argument value or nil if not present. The value is a String, Integer,
|
13
|
+
# or Float depending the on the type of the option
|
14
|
+
attr_accessor :argument
|
15
|
+
|
16
|
+
forward_to :grammar,
|
17
|
+
:uid, :ident,
|
18
|
+
:repeatable?, :argument?, :integer?, :float?,
|
19
|
+
:file?, :enum?, :string?, :optional?, :list?,
|
20
|
+
:argument_name, :argument_type, :argument_enum,
|
21
|
+
:short_idents, :long_idents
|
22
|
+
|
23
|
+
def initialize(grammar, name, argument)
|
24
|
+
@grammar, @name, @argument = grammar, name, argument
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/shellopts/parser.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
module ShellOpts
|
3
|
+
# Extend Grammar classes with parse methods
|
3
4
|
module Grammar
|
4
5
|
class Node
|
5
6
|
def parse() end
|
@@ -28,13 +29,13 @@ module ShellOpts
|
|
28
29
|
NAME_RE = /(?:#{SHORT_NAME_RE}|#{LONG_NAME_RE})(?:,#{LONG_NAME_RE})*/
|
29
30
|
|
30
31
|
def parse
|
31
|
-
token.source =~ /^(-|--|\+|\+\+)(#{NAME_RE})(?:=(.+?)(
|
32
|
+
token.source =~ /^(-|--|\+|\+\+)(#{NAME_RE})(?:=(.+?)(,\??|\?,?)?)?$/ or
|
32
33
|
parser_error token, "Illegal option: #{token.source.inspect}"
|
33
34
|
initial = $1
|
34
35
|
name_list = $2
|
35
36
|
arg = $3
|
36
|
-
optional = $4
|
37
|
-
|
37
|
+
@optional = $4&.include?(??) || false
|
38
|
+
@list = $4&.include?(?,) || false
|
38
39
|
@repeatable = %w(+ ++).include?(initial)
|
39
40
|
|
40
41
|
@short_idents = []
|
@@ -48,8 +49,8 @@ module ShellOpts
|
|
48
49
|
end
|
49
50
|
end
|
50
51
|
|
51
|
-
names.each { |name|
|
52
|
-
name.size > 1 or
|
52
|
+
names.each { |name|
|
53
|
+
name.size > 1 or
|
53
54
|
parser_error token, "Long names should be at least two characters long: '#{name}'"
|
54
55
|
}
|
55
56
|
|
@@ -94,7 +95,6 @@ module ShellOpts
|
|
94
95
|
@argument_name = arg
|
95
96
|
@argument_type = StringType.new
|
96
97
|
end
|
97
|
-
@optional = !optional.nil?
|
98
98
|
else
|
99
99
|
@argument_type = StringType.new
|
100
100
|
end
|
@@ -182,7 +182,7 @@ module ShellOpts
|
|
182
182
|
when :option
|
183
183
|
# Collect options into option groups if on the same line and not in
|
184
184
|
# oneline mode
|
185
|
-
options = [token] + @tokens.shift_while { |follow|
|
185
|
+
options = [token] + @tokens.shift_while { |follow|
|
186
186
|
!oneline && follow.kind == :option && follow.lineno == token.lineno
|
187
187
|
}
|
188
188
|
group = Grammar::OptionGroup.new(cmds.top, token)
|
@@ -266,7 +266,7 @@ module ShellOpts
|
|
266
266
|
else
|
267
267
|
if nodes.top.is_a?(Grammar::Command) || nodes.top.is_a?(Grammar::OptionGroup)
|
268
268
|
Grammar::Brief.new(nodes.top, token, token.source.sub(/\..*/, "")) if !nodes.top.brief
|
269
|
-
parent = nodes.top
|
269
|
+
parent = nodes.top
|
270
270
|
else
|
271
271
|
parent = nodes.top.parent
|
272
272
|
end
|
data/lib/shellopts/program.rb
CHANGED
@@ -174,7 +174,7 @@ module ShellOpts
|
|
174
174
|
|
175
175
|
# The parent command or nil. Initialized by #add_command
|
176
176
|
attr_accessor :__supercommand__
|
177
|
-
|
177
|
+
|
178
178
|
# The subcommand identifier (a Symbol incl. the exclamation mark) or nil
|
179
179
|
# if not present. Use #subcommand!, or the dynamically generated
|
180
180
|
# '#<identifier>!' method to get the actual subcommand object
|
@@ -197,7 +197,7 @@ module ShellOpts
|
|
197
197
|
def __initialize__(grammar)
|
198
198
|
@__grammar__ = grammar
|
199
199
|
@__option_values__ = {}
|
200
|
-
@__option_list__ = []
|
200
|
+
@__option_list__ = []
|
201
201
|
@__option_hash__ = {}
|
202
202
|
@__option_values__ = {}
|
203
203
|
@__subcommand__ = nil
|
@@ -209,22 +209,22 @@ module ShellOpts
|
|
209
209
|
@__grammar__.options.each { |opt|
|
210
210
|
if !opt.repeatable?
|
211
211
|
self.instance_eval %(
|
212
|
-
def #{opt.attr}?()
|
213
|
-
@__option_values__.key?(:#{opt.attr})
|
212
|
+
def #{opt.attr}?()
|
213
|
+
@__option_values__.key?(:#{opt.attr})
|
214
214
|
end
|
215
215
|
)
|
216
216
|
end
|
217
|
-
|
217
|
+
|
218
218
|
if opt.repeatable?
|
219
219
|
if opt.argument?
|
220
220
|
self.instance_eval %(
|
221
|
-
def #{opt.attr}?()
|
222
|
-
(@__option_values__[:#{opt.attr}]&.size || 0) > 0
|
221
|
+
def #{opt.attr}?()
|
222
|
+
(@__option_values__[:#{opt.attr}]&.size || 0) > 0
|
223
223
|
end
|
224
224
|
)
|
225
225
|
self.instance_eval %(
|
226
226
|
def #{opt.attr}(default = [])
|
227
|
-
if @__option_values__.key?(:#{opt.attr})
|
227
|
+
if @__option_values__.key?(:#{opt.attr})
|
228
228
|
@__option_values__[:#{opt.attr}]
|
229
229
|
else
|
230
230
|
default
|
@@ -233,12 +233,12 @@ module ShellOpts
|
|
233
233
|
)
|
234
234
|
else
|
235
235
|
self.instance_eval %(
|
236
|
-
def #{opt.attr}?()
|
237
|
-
(@__option_values__[:#{opt.attr}] || 0) > 0
|
236
|
+
def #{opt.attr}?()
|
237
|
+
(@__option_values__[:#{opt.attr}] || 0) > 0
|
238
238
|
end
|
239
239
|
)
|
240
240
|
self.instance_eval %(
|
241
|
-
def #{opt.attr}(default = 0)
|
241
|
+
def #{opt.attr}(default = 0)
|
242
242
|
if default > 0 && (@__option_values__[:#{opt.attr}] || 0) == 0
|
243
243
|
default
|
244
244
|
else
|
@@ -251,7 +251,7 @@ module ShellOpts
|
|
251
251
|
elsif opt.argument?
|
252
252
|
self.instance_eval %(
|
253
253
|
def #{opt.attr}(default = nil)
|
254
|
-
if @__option_values__.key?(:#{opt.attr})
|
254
|
+
if @__option_values__.key?(:#{opt.attr})
|
255
255
|
@__option_values__[:#{opt.attr}]
|
256
256
|
else
|
257
257
|
default
|
@@ -261,8 +261,8 @@ module ShellOpts
|
|
261
261
|
|
262
262
|
else
|
263
263
|
self.instance_eval %(
|
264
|
-
def #{opt.attr}()
|
265
|
-
@__option_values__.key?(:#{opt.attr})
|
264
|
+
def #{opt.attr}()
|
265
|
+
@__option_values__.key?(:#{opt.attr})
|
266
266
|
end
|
267
267
|
)
|
268
268
|
end
|
@@ -279,7 +279,7 @@ module ShellOpts
|
|
279
279
|
@__grammar__.commands.each { |cmd|
|
280
280
|
next if cmd.attr.nil?
|
281
281
|
self.instance_eval %(
|
282
|
-
def #{cmd.attr}()
|
282
|
+
def #{cmd.attr}()
|
283
283
|
:#{cmd.attr} == __subcommand__ ? __subcommand__! : nil
|
284
284
|
end
|
285
285
|
)
|
@@ -328,29 +328,4 @@ module ShellOpts
|
|
328
328
|
@__debug__ = false
|
329
329
|
end
|
330
330
|
end
|
331
|
-
|
332
|
-
# Option models an option as given by the user on the subcommand line.
|
333
|
-
# Compiled options (and possibly aggregated) options are stored in the
|
334
|
-
# Command#__option_values__ array
|
335
|
-
class Option
|
336
|
-
# Associated Grammar::Option object
|
337
|
-
attr_reader :grammar
|
338
|
-
|
339
|
-
# The actual name used on the shell command-line (String)
|
340
|
-
attr_reader :name
|
341
|
-
|
342
|
-
# Argument value or nil if not present. The value is a String, Integer,
|
343
|
-
# or Float depending the on the type of the option
|
344
|
-
attr_accessor :argument
|
345
|
-
|
346
|
-
forward_to :grammar,
|
347
|
-
:uid, :ident,
|
348
|
-
:repeatable?, :argument?, :integer?, :float?,
|
349
|
-
:file?, :enum?, :string?, :optional?,
|
350
|
-
:argument_name, :argument_type, :argument_enum
|
351
|
-
|
352
|
-
def initialize(grammar, name, argument)
|
353
|
-
@grammar, @name, @argument = grammar, name, argument
|
354
|
-
end
|
355
|
-
end
|
356
331
|
end
|
data/lib/shellopts/renderer.rb
CHANGED
@@ -26,7 +26,7 @@ require 'terminfo'
|
|
26
26
|
# [cmd1|cmd2] ARG1 ARG2
|
27
27
|
# cmd --all --beta
|
28
28
|
# <commands> ARGS
|
29
|
-
#
|
29
|
+
#
|
30
30
|
module ShellOpts
|
31
31
|
module Grammar
|
32
32
|
class Option
|
@@ -38,16 +38,23 @@ module ShellOpts
|
|
38
38
|
#
|
39
39
|
def render(format)
|
40
40
|
constrain format, :enum, :long, :short
|
41
|
-
s =
|
41
|
+
s =
|
42
42
|
case format
|
43
43
|
when :enum; names.join(", ")
|
44
44
|
when :long; name
|
45
45
|
when :short; short_names.first || name
|
46
46
|
else
|
47
47
|
raise ArgumentError, "Illegal format: #{format.inspect}"
|
48
|
-
end
|
48
|
+
end
|
49
49
|
if argument?
|
50
|
-
|
50
|
+
short = long_idents.empty? || format == :short
|
51
|
+
arg = ""
|
52
|
+
arg += "=" if !short
|
53
|
+
arg += argument_name
|
54
|
+
arg += "..." if list?
|
55
|
+
arg = "[#{arg}]" if optional?
|
56
|
+
arg = " " + arg if short
|
57
|
+
s += arg
|
51
58
|
else
|
52
59
|
s
|
53
60
|
end
|
@@ -56,7 +63,7 @@ module ShellOpts
|
|
56
63
|
|
57
64
|
class OptionGroup
|
58
65
|
# Formats:
|
59
|
-
#
|
66
|
+
#
|
60
67
|
# :enum -a, --all -r, --recursive
|
61
68
|
# :long --all --recursive
|
62
69
|
# :short -a -r
|
@@ -115,7 +122,7 @@ module ShellOpts
|
|
115
122
|
|
116
123
|
# Force one line and compact options to "[OPTIONS]"
|
117
124
|
def render_abbr
|
118
|
-
args = get_args
|
125
|
+
args = get_args
|
119
126
|
([name] + [options.empty? ? nil : "[OPTIONS]"] + args).compact.join(" ")
|
120
127
|
end
|
121
128
|
|
data/lib/shellopts/token.rb
CHANGED
@@ -4,7 +4,7 @@ module ShellOpts
|
|
4
4
|
# Each kind should have a corresponding Grammar class with the same name
|
5
5
|
KINDS = [
|
6
6
|
:program, :section, :option, :command, :spec, :argument, :usage,
|
7
|
-
:usage_string, :brief, :text, :blank
|
7
|
+
:usage_string, :brief, :text, :blank
|
8
8
|
]
|
9
9
|
|
10
10
|
# Kind of token
|
@@ -27,13 +27,13 @@ module ShellOpts
|
|
27
27
|
|
28
28
|
forward_to :source, :to_s, :empty?
|
29
29
|
|
30
|
-
def pos(start_lineno = 1, start_charno = 1)
|
31
|
-
"#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
|
30
|
+
def pos(start_lineno = 1, start_charno = 1)
|
31
|
+
"#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
|
32
32
|
end
|
33
33
|
|
34
34
|
def to_s() source end
|
35
35
|
|
36
|
-
def inspect()
|
36
|
+
def inspect()
|
37
37
|
"<#{self.class.to_s.sub(/.*::/, "")} #{pos} #{kind.inspect} #{source.inspect}>"
|
38
38
|
end
|
39
39
|
|
data/lib/shellopts/version.rb
CHANGED
data/lib/shellopts.rb
CHANGED
@@ -16,6 +16,7 @@ require_relative 'shellopts/stack.rb'
|
|
16
16
|
require_relative 'shellopts/token.rb'
|
17
17
|
require_relative 'shellopts/grammar.rb'
|
18
18
|
require_relative 'shellopts/program.rb'
|
19
|
+
require_relative 'shellopts/option.rb'
|
19
20
|
require_relative 'shellopts/args.rb'
|
20
21
|
require_relative 'shellopts/lexer.rb'
|
21
22
|
require_relative 'shellopts/argument_type.rb'
|
@@ -32,7 +33,7 @@ require_relative 'shellopts/dump.rb'
|
|
32
33
|
# Notes
|
33
34
|
# * Two kinds of exceptions: Expected & unexpected. Expected exceptions are
|
34
35
|
# RuntimeError or IOError. Unexpected exceptions are the rest. Both results
|
35
|
-
# in shellopts.failure messages if shellopts error handling is enabled
|
36
|
+
# in shellopts.failure messages if shellopts error handling is enabled
|
36
37
|
# * Describe the difference between StandardError, RuntimeError, and IOError
|
37
38
|
# * Add an #internal error handling for the production environment that
|
38
39
|
# prints an intelligble error message and prettyfies stack dump. This
|
@@ -60,7 +61,7 @@ module ShellOpts
|
|
60
61
|
# <program>: <message>
|
61
62
|
# Usage: <program> ...
|
62
63
|
#
|
63
|
-
class Error < ShellOptsError; end
|
64
|
+
class Error < ShellOptsError; end
|
64
65
|
|
65
66
|
# Default class for program failures. Failures are raised on missing files or
|
66
67
|
# illegal paths. When ShellOpts handles the exception a message with the
|
@@ -74,7 +75,7 @@ module ShellOpts
|
|
74
75
|
# source. Messages are formatted as '<file> <lineno>:<charno> <message>' when
|
75
76
|
# handled by ShellOpts
|
76
77
|
class CompilerError < ShellOptsError; end
|
77
|
-
class LexerError < CompilerError; end
|
78
|
+
class LexerError < CompilerError; end
|
78
79
|
class ParserError < CompilerError; end
|
79
80
|
class AnalyzerError < CompilerError; end
|
80
81
|
|
@@ -92,7 +93,7 @@ module ShellOpts
|
|
92
93
|
attr_reader :spec
|
93
94
|
|
94
95
|
# Array of arguments. Initialized by #interpret
|
95
|
-
attr_reader :argv
|
96
|
+
attr_reader :argv
|
96
97
|
|
97
98
|
# Grammar. Grammar::Program object. Initialized by #compile
|
98
99
|
attr_reader :grammar
|
@@ -139,10 +140,10 @@ module ShellOpts
|
|
139
140
|
attr_reader :tokens
|
140
141
|
alias_method :ast, :grammar
|
141
142
|
|
142
|
-
def initialize(name: nil,
|
143
|
+
def initialize(name: nil,
|
143
144
|
# Options
|
144
|
-
help: true,
|
145
|
-
version: true,
|
145
|
+
help: true,
|
146
|
+
version: true,
|
146
147
|
silent: nil,
|
147
148
|
quiet: nil,
|
148
149
|
verbose: nil,
|
@@ -157,7 +158,7 @@ module ShellOpts
|
|
157
158
|
# Let exceptions through
|
158
159
|
exception: false
|
159
160
|
)
|
160
|
-
|
161
|
+
|
161
162
|
@name = name || File.basename($PROGRAM_NAME)
|
162
163
|
@help = help
|
163
164
|
@version = version || (version.nil? && !version_number.nil?)
|
@@ -187,24 +188,24 @@ module ShellOpts
|
|
187
188
|
verbose_spec = (@verbose == true ? "+v,verbose" : @verbose)
|
188
189
|
debug_spec = (@debug == true ? "--debug" : @debug)
|
189
190
|
|
190
|
-
@silent_option =
|
191
|
+
@silent_option =
|
191
192
|
ast.inject_option(silent_spec, "Quiet", "Do not write anything to standard output") if @silent
|
192
|
-
@quiet_option =
|
193
|
+
@quiet_option =
|
193
194
|
ast.inject_option(quiet_spec, "Quiet", "Do not write anything to standard output") if @quiet
|
194
|
-
@verbose_option =
|
195
|
+
@verbose_option =
|
195
196
|
ast.inject_option(verbose_spec, "Increase verbosity", "Write verbose output") if @verbose
|
196
|
-
@debug_option =
|
197
|
+
@debug_option =
|
197
198
|
ast.inject_option(debug_spec, "Write debug information") if @debug
|
198
|
-
@help_option =
|
199
|
+
@help_option =
|
199
200
|
ast.inject_option(help_spec, "Write short or long help") { |option|
|
200
|
-
short_option = option.short_names.first
|
201
|
+
short_option = option.short_names.first
|
201
202
|
long_option = option.long_names.first
|
202
203
|
[
|
203
204
|
short_option && "#{short_option} prints a brief help text",
|
204
205
|
long_option && "#{long_option} prints a longer man-style description of the command"
|
205
206
|
].compact.join(", ")
|
206
207
|
} if @help
|
207
|
-
@version_option =
|
208
|
+
@version_option =
|
208
209
|
ast.inject_option(version_spec, "Write version number and exit") if @version
|
209
210
|
|
210
211
|
@grammar = Analyzer.analyze(ast)
|
@@ -216,7 +217,7 @@ module ShellOpts
|
|
216
217
|
# ShellOpts::Args tuple
|
217
218
|
#
|
218
219
|
def interpret(argv)
|
219
|
-
handle_exceptions {
|
220
|
+
handle_exceptions {
|
220
221
|
@argv = argv.dup
|
221
222
|
@program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
|
222
223
|
|
@@ -356,7 +357,7 @@ module ShellOpts
|
|
356
357
|
char_z = 0
|
357
358
|
|
358
359
|
(0 ... text_lines.size).each { |text_i|
|
359
|
-
curr_char_i, curr_char_z =
|
360
|
+
curr_char_i, curr_char_z =
|
360
361
|
LCS.find_longest_common_substring_index(text_lines[text_i], spec_lines.first.strip)
|
361
362
|
if curr_char_z > char_z
|
362
363
|
line_i = text_i
|
@@ -372,7 +373,7 @@ module ShellOpts
|
|
372
373
|
compare_lines(text_lines[text_i + spec_i], spec_lines[spec_i])
|
373
374
|
}
|
374
375
|
} or return [nil, nil]
|
375
|
-
char_i, char_z =
|
376
|
+
char_i, char_z =
|
376
377
|
LCS.find_longest_common_substring_index(text_lines[line_i], spec_lines.first.strip)
|
377
378
|
[line_i, char_i || 0]
|
378
379
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shellopts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: forward_to
|
@@ -147,6 +147,7 @@ files:
|
|
147
147
|
- lib/shellopts/grammar.rb
|
148
148
|
- lib/shellopts/interpreter.rb
|
149
149
|
- lib/shellopts/lexer.rb
|
150
|
+
- lib/shellopts/option.rb
|
150
151
|
- lib/shellopts/parser.rb
|
151
152
|
- lib/shellopts/program.rb
|
152
153
|
- lib/shellopts/renderer.rb
|