shellopts 2.0.9 → 2.0.12
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/TODO +5 -5
- data/lib/ext/array.rb +3 -1
- data/lib/shellopts/analyzer.rb +47 -18
- data/lib/shellopts/formatter.rb +52 -57
- data/lib/shellopts/grammar.rb +16 -11
- data/lib/shellopts/parser.rb +13 -10
- data/lib/shellopts/program.rb +13 -4
- data/lib/shellopts/renderer.rb +37 -48
- data/lib/shellopts/version.rb +1 -1
- data/lib/shellopts.rb +53 -170
- data/main +4 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2f378c7e7c04297221bfae81065ef06699e93b2dc4fda892b210ef4702c5836
|
4
|
+
data.tar.gz: f5d83b09b371aebd7cc07e73905edffb2f9f834ba5e382894277310e984e7ecc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6255a64a99ceefe0dc0a7a3195976d42b2db6b3a28d779cbeefbc9f36380e46a762939e2062ff9f32f4f78bd5bff8122606b5102d9d8473e86c0a742cd7db479
|
7
|
+
data.tar.gz: b54f74a5d8fd251d1492667bd8e1340c1f17a3e96936f423efd61e82fe665294936c1edd1ac9ac4ab9df118450ccefafcda117f84ef4e0726d78159196e075cf
|
data/TODO
CHANGED
@@ -1,18 +1,13 @@
|
|
1
1
|
|
2
|
-
o Add brackets to optional option arguments: '--all=FILE?' -> '--all[=FILE]'
|
3
2
|
o Ignore all text after ' # ' (doesn't conflict with option flag)
|
4
3
|
o Command aliases
|
5
4
|
o Add user-defined setions
|
6
5
|
o Add a SOURCE section with link to git repo
|
7
6
|
o Bullet-lists
|
8
7
|
o Allow a USAGE section (and NAME)
|
9
|
-
o Find source in code an adjust line number in error messages
|
10
|
-
o Rename line and char to lineno and charno
|
11
8
|
o Client-defined argument types
|
12
9
|
o Rename Expr -> ?
|
13
10
|
o Find clean(er) procedural object model
|
14
|
-
o Allow assignment to options (this makes practical stuff easier)
|
15
|
-
o Special handling of --help arguments so that '--help command' is possible
|
16
11
|
o Support for paging of help:
|
17
12
|
begin
|
18
13
|
file = Tempfile.new("prick")
|
@@ -23,6 +18,11 @@ o Support for paging of help:
|
|
23
18
|
file.close
|
24
19
|
end
|
25
20
|
|
21
|
+
+ Special handling of --help arguments so that '--help command' is possible
|
22
|
+
+ Allow assignment to options (this makes practical stuff easier)
|
23
|
+
+ Rename line and char to lineno and charno
|
24
|
+
+ Find source in code an adjust line number in error messages
|
25
|
+
+ Add brackets to optional option arguments: '--all=FILE?' -> '--all[=FILE]'
|
26
26
|
+ Bold text output
|
27
27
|
+ Recursive format of commands
|
28
28
|
+ Rename Compiler -> Interpreter
|
data/lib/ext/array.rb
CHANGED
@@ -41,7 +41,9 @@ module Ext
|
|
41
41
|
|
42
42
|
module Wrap
|
43
43
|
refine ::Array do
|
44
|
-
# Concatenate
|
44
|
+
# Concatenate array of words into lines that are at most +width+
|
45
|
+
# characters wide. +curr+ is the initial number of characters already
|
46
|
+
# used on the first line
|
45
47
|
def wrap(width, curr = 0)
|
46
48
|
lines = [[]]
|
47
49
|
curr -= 1 # Simplifies conditions below
|
data/lib/shellopts/analyzer.rb
CHANGED
@@ -20,10 +20,6 @@ module ShellOpts
|
|
20
20
|
end
|
21
21
|
|
22
22
|
class Command
|
23
|
-
def set_supercommand
|
24
|
-
commands.each { |child| child.instance_variable_set(:@supercommand, self) }
|
25
|
-
end
|
26
|
-
|
27
23
|
def collect_options
|
28
24
|
@options = option_groups.map(&:options).flatten
|
29
25
|
end
|
@@ -54,9 +50,9 @@ module ShellOpts
|
|
54
50
|
}
|
55
51
|
end
|
56
52
|
|
53
|
+
# TODO Check for dash-collision
|
57
54
|
def compute_command_hashes
|
58
55
|
commands.each { |command|
|
59
|
-
# TODO Check for dash-collision
|
60
56
|
!@commands_hash.key?(command.name) or
|
61
57
|
analyzer_error command.token, "Duplicate command name: #{command.name}"
|
62
58
|
@commands_hash[command.name] = command
|
@@ -76,41 +72,74 @@ module ShellOpts
|
|
76
72
|
@grammar = grammar
|
77
73
|
end
|
78
74
|
|
79
|
-
|
80
|
-
|
75
|
+
def create_implicit_commands(cmd)
|
76
|
+
path = cmd.path[0..-2]
|
77
|
+
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# Link up commands with supercommands. This is only done for commands that
|
82
|
+
# are nested within a different command than it belongs to. The
|
83
|
+
# parent/child relationship is not changed Example:
|
84
|
+
#
|
85
|
+
# cmd!
|
86
|
+
# cmd.subcmd!
|
87
|
+
#
|
88
|
+
# Here subcmd is added to cmd's list of commands. It keeps its position in
|
89
|
+
# the program's parent/child relationship so that documentation will print the
|
90
|
+
# commands in the given order and with the given indentation level
|
91
|
+
#
|
92
|
+
def link_commands
|
81
93
|
# We can't use Command#[] at this point so we collect the commands here
|
82
94
|
h = {}
|
83
95
|
@grammar.traverse(Grammar::Command) { |command|
|
84
96
|
h[command.path] = command
|
97
|
+
# TODO: Pick up parent-less commands
|
98
|
+
}
|
99
|
+
|
100
|
+
# Command to link
|
101
|
+
link = []
|
102
|
+
|
103
|
+
# Create implicit commands
|
104
|
+
h.sort { |l,r| l.size <=> r.size }.each { |path, command|
|
105
|
+
path = path[0..-2]
|
106
|
+
while !h.key?(path)
|
107
|
+
cmd = Grammar::Command.new(nil, command.token)
|
108
|
+
cmd.set_name(path.last.to_s.sub(/!/, ""), path.dup)
|
109
|
+
link << cmd
|
110
|
+
h[cmd.path] = cmd
|
111
|
+
path.pop
|
112
|
+
end
|
85
113
|
}
|
86
114
|
|
87
|
-
# Find commands to
|
115
|
+
# Find commands to link
|
88
116
|
#
|
89
|
-
# Commands are
|
90
|
-
# not defined when the data structure changes beneath it
|
91
|
-
|
117
|
+
# Commands are linked in two steps because the behaviour of #traverse is
|
118
|
+
# not defined when the data structure changes beneath it. (FIXME: Does it
|
119
|
+
# change when we don't touch the parent/child relationship?)
|
92
120
|
@grammar.traverse(Grammar::Command) { |command|
|
93
121
|
if command.path.size > 1 && command.parent && command.parent.path != command.path[0..-2]
|
94
|
-
|
122
|
+
# if command.path.size > 1 && command.parent.path != command.path[0..-2]
|
123
|
+
link << command
|
95
124
|
else
|
96
125
|
command.instance_variable_set(:@command, command.parent)
|
97
126
|
end
|
98
127
|
}
|
99
128
|
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
|
129
|
+
# Link commands but do not change parent/child relationship
|
130
|
+
link.each { |command|
|
131
|
+
path = command.path[0..-2]
|
132
|
+
path.pop while (supercommand = h[path]).nil?
|
133
|
+
command.parent.commands.delete(command) if command.parent
|
104
134
|
supercommand.commands << command
|
105
135
|
command.instance_variable_set(:@command, supercommand)
|
106
136
|
}
|
107
137
|
end
|
108
138
|
|
109
139
|
def analyze()
|
110
|
-
|
140
|
+
link_commands
|
111
141
|
|
112
142
|
@grammar.traverse(Grammar::Command) { |command|
|
113
|
-
command.set_supercommand
|
114
143
|
command.reorder_options
|
115
144
|
command.collect_options
|
116
145
|
command.compute_option_hashes
|
data/lib/shellopts/formatter.rb
CHANGED
@@ -1,12 +1,5 @@
|
|
1
1
|
require 'terminfo'
|
2
2
|
|
3
|
-
# TODO: Move to ext/indented_io.rb
|
4
|
-
module IndentedIO
|
5
|
-
class IndentedIO
|
6
|
-
def margin() combined_indent.size end
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
3
|
module ShellOpts
|
11
4
|
module Grammar
|
12
5
|
class Node
|
@@ -81,7 +74,15 @@ module ShellOpts
|
|
81
74
|
end
|
82
75
|
|
83
76
|
def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
|
84
|
-
|
77
|
+
# Use one-line mode if all options are declared on one line
|
78
|
+
if options.all? { |option| option.token.lineno == token.lineno }
|
79
|
+
puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
|
80
|
+
puts_options = false
|
81
|
+
else
|
82
|
+
puts Ansi.bold([prefix, render(:abbr, Formatter.rest)].flatten.compact.join(" "))
|
83
|
+
puts_options = true
|
84
|
+
end
|
85
|
+
|
85
86
|
indent {
|
86
87
|
if brief
|
87
88
|
puts self.brief.words.wrap(Formatter.rest)
|
@@ -93,7 +94,10 @@ module ShellOpts
|
|
93
94
|
|
94
95
|
if child.is_a?(Command)
|
95
96
|
child.puts_descr(prefix, name: :path)
|
96
|
-
|
97
|
+
elsif child.is_a?(OptionGroup)
|
98
|
+
child.puts_descr if puts_options
|
99
|
+
newline = false
|
100
|
+
else
|
97
101
|
child.puts_descr
|
98
102
|
end
|
99
103
|
}
|
@@ -125,9 +129,9 @@ module ShellOpts
|
|
125
129
|
section.delete(klass)
|
126
130
|
section.delete(Paragraph)
|
127
131
|
if klass <= OptionGroup
|
128
|
-
s
|
132
|
+
s += "S" if options.size > 1
|
129
133
|
elsif klass <= Command
|
130
|
-
s
|
134
|
+
s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
|
131
135
|
end
|
132
136
|
puts
|
133
137
|
indent(-1).puts Ansi.bold s
|
@@ -139,7 +143,7 @@ module ShellOpts
|
|
139
143
|
end
|
140
144
|
|
141
145
|
if child.is_a?(Command)
|
142
|
-
prefix = child.
|
146
|
+
prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
|
143
147
|
child.puts_descr(prefix, brief: false, name: :path)
|
144
148
|
newline = true
|
145
149
|
else
|
@@ -150,9 +154,10 @@ module ShellOpts
|
|
150
154
|
|
151
155
|
# Also emit commands not declared in nested scope
|
152
156
|
(commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
|
157
|
+
next if cmd.parent.nil? # Skip implicit commands
|
153
158
|
puts if newline
|
154
159
|
newline = true
|
155
|
-
prefix = cmd.
|
160
|
+
prefix = cmd.command == self ? nil : cmd.command&.name
|
156
161
|
cmd.puts_descr(prefix, brief: false, name: path)
|
157
162
|
}
|
158
163
|
}
|
@@ -204,7 +209,10 @@ module ShellOpts
|
|
204
209
|
BRIEF_COL1_MAX_WIDTH = 40
|
205
210
|
|
206
211
|
# Minimum width of second column in brief option and command lists
|
207
|
-
|
212
|
+
BRIEF_COL2_MIN_WIDTH = 30
|
213
|
+
|
214
|
+
# Maximum width of second column in brief option and command lists
|
215
|
+
BRIEF_COL2_MAX_WIDTH = 70
|
208
216
|
|
209
217
|
# Indent to use in help output
|
210
218
|
HELP_INDENT = 4
|
@@ -214,29 +222,21 @@ module ShellOpts
|
|
214
222
|
|
215
223
|
# Usage string in error messages
|
216
224
|
def self.usage(subject)
|
217
|
-
|
218
|
-
@command_prefix =
|
225
|
+
command = Grammar::Command.command(subject)
|
226
|
+
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
219
227
|
setup_indent(1) {
|
220
228
|
print lead = "#{USAGE_STRING}: "
|
221
|
-
indent(lead.size, ' ', bol: false) {
|
229
|
+
indent(lead.size, ' ', bol: false) { command.puts_usage }
|
222
230
|
}
|
223
231
|
end
|
224
232
|
|
225
|
-
# # TODO
|
226
|
-
# def self.usage=(usage_lambda)
|
227
|
-
# end
|
228
|
-
|
229
233
|
# When the user gives a -h option
|
230
|
-
def self.brief(
|
231
|
-
command = Grammar::Command.command(
|
234
|
+
def self.brief(subject)
|
235
|
+
command = Grammar::Command.command(subject)
|
232
236
|
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
233
237
|
setup_indent(BRIEF_INDENT) { command.puts_brief }
|
234
238
|
end
|
235
239
|
|
236
|
-
# # TODO
|
237
|
-
# def self.brief=(brief_lambda)
|
238
|
-
# end
|
239
|
-
|
240
240
|
# When the user gives a --help option
|
241
241
|
def self.help(subject)
|
242
242
|
subject = Grammar::Command.command(subject)
|
@@ -250,18 +250,6 @@ module ShellOpts
|
|
250
250
|
obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
|
251
251
|
end
|
252
252
|
|
253
|
-
# # TODO
|
254
|
-
# def self.help_w_lambda(program)
|
255
|
-
# if @help_lambda
|
256
|
-
# #
|
257
|
-
# else
|
258
|
-
# program = Grammar::Command.command(program)
|
259
|
-
# setup_indent(HELP_INDENT) { program.puts_descr }
|
260
|
-
# end
|
261
|
-
# end
|
262
|
-
#
|
263
|
-
# def self.help=(help_lambda) @help_lambda end
|
264
|
-
|
265
253
|
def self.puts_columns(widths, fields)
|
266
254
|
l = []
|
267
255
|
first_width, second_width = *widths
|
@@ -272,40 +260,47 @@ module ShellOpts
|
|
272
260
|
puts first
|
273
261
|
indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
|
274
262
|
elsif second
|
275
|
-
|
276
|
-
|
263
|
+
indent_size = first_width + BRIEF_COL_SEP
|
264
|
+
printf "%-#{indent_size}s", first
|
265
|
+
indent(indent_size, ' ', bol: false) { puts second.wrap(second_width) }
|
277
266
|
else
|
278
267
|
puts first
|
279
268
|
end
|
280
269
|
end
|
281
270
|
end
|
282
271
|
|
272
|
+
# Returns a tuple of [first-column-width, second-column-width]. +width+ is
|
273
|
+
# the maximum width of the colunms and the BRIEF_COL_SEP separator.
|
274
|
+
# +fields+ is an array of [subject-string, descr-text] tuples where the
|
275
|
+
# descr is an array of words
|
283
276
|
def self.compute_columns(width, fields)
|
284
|
-
first_max =
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
first_width = first_max
|
295
|
-
second_width = width - first_width - BRIEF_COL_SEP
|
277
|
+
first_max =
|
278
|
+
fields.map { |first, _| first.size }.select { |size| size <= BRIEF_COL1_MAX_WIDTH }.max ||
|
279
|
+
BRIEF_COL1_MIN_WIDTH
|
280
|
+
second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1 : 0 }.max
|
281
|
+
first_width = [[first_max, BRIEF_COL1_MIN_WIDTH].max, BRIEF_COL1_MAX_WIDTH].min
|
282
|
+
rest = width - first_width - BRIEF_COL_SEP
|
283
|
+
second_min = [BRIEF_COL2_MIN_WIDTH, second_max].min
|
284
|
+
if rest < second_min
|
285
|
+
first_width = [first_max, width - second_min - BRIEF_COL_SEP].max
|
286
|
+
second_width = [width - first_width - BRIEF_COL_SEP, BRIEF_COL2_MIN_WIDTH].max
|
296
287
|
else
|
297
|
-
|
298
|
-
second_width = BRIEF_COL2_MAX_WIDTH
|
288
|
+
second_width = [[rest, BRIEF_COL2_MIN_WIDTH].max, BRIEF_COL2_MAX_WIDTH].min
|
299
289
|
end
|
300
|
-
|
301
290
|
[first_width, second_width]
|
302
291
|
end
|
303
292
|
|
304
293
|
def self.width()
|
305
294
|
@width ||= TermInfo.screen_width - MARGIN_RIGHT
|
295
|
+
@width
|
296
|
+
end
|
297
|
+
|
298
|
+
# Used in rspec
|
299
|
+
def self.width=(width)
|
300
|
+
@width = width
|
306
301
|
end
|
307
302
|
|
308
|
-
def self.rest() width - $stdout.
|
303
|
+
def self.rest() width - $stdout.tab end
|
309
304
|
|
310
305
|
private
|
311
306
|
# TODO Get rid of?
|
data/lib/shellopts/grammar.rb
CHANGED
@@ -45,16 +45,18 @@ module ShellOpts
|
|
45
45
|
end
|
46
46
|
|
47
47
|
class IdrNode < Node
|
48
|
-
# Command of this object
|
49
|
-
#
|
50
|
-
# Initialized by the analyzer
|
48
|
+
# Command of this object (nil for the top-level Program object). This is
|
49
|
+
# different from #parent when a subcommand is nested textually on a
|
50
|
+
# higher level than its supercommand. Initialized by the analyzer
|
51
51
|
attr_reader :command
|
52
52
|
|
53
53
|
# Unique identifier of node (String) within the context of a program. nil
|
54
|
-
# for the Program object. It is the
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
54
|
+
# for the Program object. It is the dot-joined elements of path with
|
55
|
+
# internal exclamation marks removed (eg. "cmd.opt" or "cmd.cmd!").
|
56
|
+
# Initialize by the analyzer
|
57
|
+
def uid()
|
58
|
+
@uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
|
59
|
+
end
|
58
60
|
|
59
61
|
# Path from Program object and down to this node. Array of identifiers.
|
60
62
|
# Empty for the Program object. Initialized by the parser
|
@@ -84,6 +86,13 @@ module ShellOpts
|
|
84
86
|
# #ident is a reserved word. Initialized by the parser
|
85
87
|
attr_reader :attr
|
86
88
|
|
89
|
+
def set_name(name, path)
|
90
|
+
@name = name.to_s
|
91
|
+
@path = path
|
92
|
+
@ident = @path.last || :!
|
93
|
+
@attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(@ident.to_s) ? nil : @ident
|
94
|
+
end
|
95
|
+
|
87
96
|
protected
|
88
97
|
def lookup(path)
|
89
98
|
path.empty? or raise ArgumentError, "Argument should be empty"
|
@@ -189,10 +198,6 @@ module ShellOpts
|
|
189
198
|
# methods are initialized by the analyzer
|
190
199
|
#
|
191
200
|
class Command < IdrNode
|
192
|
-
# Supercommand or nil if this is the top-level Program object.
|
193
|
-
# Initialized by the analyzer
|
194
|
-
attr_reader :supercommand
|
195
|
-
|
196
201
|
# Brief description of command
|
197
202
|
attr_accessor :brief
|
198
203
|
|
data/lib/shellopts/parser.rb
CHANGED
@@ -16,9 +16,9 @@ module ShellOpts
|
|
16
16
|
class IdrNode
|
17
17
|
# Assumes that @name and @path has been defined
|
18
18
|
def parse
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
# @ident = @path.last || :!
|
20
|
+
# @attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(ident.to_s) ? nil : ident
|
21
|
+
# @uid = parent && @path.join(".").sub(/!\./, ".") # uid is nil for the Program object
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -56,8 +56,9 @@ module ShellOpts
|
|
56
56
|
@long_names = names.map { |name| "--#{name}" }
|
57
57
|
@long_idents = names.map { |name| name.tr("-", "_").to_sym }
|
58
58
|
|
59
|
-
|
60
|
-
|
59
|
+
set_name(
|
60
|
+
@long_names.first || @short_names.first,
|
61
|
+
command.path + [@long_idents.first || @short_idents.first])
|
61
62
|
|
62
63
|
@argument = !arg.nil?
|
63
64
|
|
@@ -108,11 +109,11 @@ module ShellOpts
|
|
108
109
|
def parse
|
109
110
|
if parent
|
110
111
|
path_names = token.source.sub("!", "").split(".")
|
111
|
-
|
112
|
-
|
112
|
+
set_name(
|
113
|
+
path_names.last,
|
114
|
+
path_names.map { |cmd| "#{cmd}!".to_sym })
|
113
115
|
else
|
114
|
-
|
115
|
-
@name = token.source
|
116
|
+
set_name(token.source, [])
|
116
117
|
end
|
117
118
|
super
|
118
119
|
end
|
@@ -123,13 +124,15 @@ module ShellOpts
|
|
123
124
|
super(nil, token)
|
124
125
|
end
|
125
126
|
|
126
|
-
def
|
127
|
+
def add_version_option
|
127
128
|
option_token = Token.new(:option, 1, 1, "--version")
|
128
129
|
brief_token = Token.new(:brief, 1, 1, "Write version number and exit")
|
129
130
|
group = OptionGroup.new(self, option_token)
|
130
131
|
option = Option.parse(group, option_token)
|
131
132
|
brief = Brief.parse(group, brief_token)
|
133
|
+
end
|
132
134
|
|
135
|
+
def add_help_options
|
133
136
|
option_token = Token.new(:option, 1, 1, "-h,help")
|
134
137
|
brief_token = Token.new(:brief, 1, 1, "Write help text and exit")
|
135
138
|
paragraph_token = Token.new(:text, 1, 1,
|
data/lib/shellopts/program.rb
CHANGED
@@ -49,10 +49,10 @@ module ShellOpts
|
|
49
49
|
singleton_method_removed singleton_method_undefined
|
50
50
|
)
|
51
51
|
|
52
|
-
# These methods can be overridden by an option (the value is not used -
|
52
|
+
# These methods can be overridden by an option or a command (the value is not used -
|
53
53
|
# this is just for informational purposes)
|
54
|
-
|
55
|
-
subcommand
|
54
|
+
OVERRIDEABLE_METHOD_NAMES = %w(
|
55
|
+
subcommand subcommand! supercommand!
|
56
56
|
)
|
57
57
|
|
58
58
|
# Redefine ::new to call #__initialize__
|
@@ -107,6 +107,7 @@ module ShellOpts
|
|
107
107
|
#
|
108
108
|
# Note: Can be overridden by option, in that case use #__subcommand__ or
|
109
109
|
# ShellOpts.subcommand(object) instead
|
110
|
+
#
|
110
111
|
def subcommand() __subcommand__ end
|
111
112
|
|
112
113
|
# The subcommand object or nil if not present. Per-subcommand methods
|
@@ -120,7 +121,12 @@ module ShellOpts
|
|
120
121
|
def subcommand!() __subcommand__! end
|
121
122
|
|
122
123
|
# The parent command or nil. Initialized by #add_command
|
123
|
-
|
124
|
+
#
|
125
|
+
# Note: Can be overridden by a subcommand declaration (but not an
|
126
|
+
# option), in that case use #__supercommand__! or
|
127
|
+
# ShellOpts.supercommand!(object) instead
|
128
|
+
#
|
129
|
+
def supercommand!() __supercommand__ end
|
124
130
|
|
125
131
|
# UID of command/program
|
126
132
|
def __uid__() @__grammar__.uid end
|
@@ -149,6 +155,9 @@ module ShellOpts
|
|
149
155
|
# Map from identifier to option object or to a list of option objects if
|
150
156
|
# the option is repeatable
|
151
157
|
attr_reader :__option_hash__
|
158
|
+
|
159
|
+
# The parent command or nil. Initialized by #add_command
|
160
|
+
attr_accessor :__supercommand__
|
152
161
|
|
153
162
|
# The subcommand identifier (a Symbol incl. the exclamation mark) or nil
|
154
163
|
# if not present. Use #subcommand!, or the dynamically generated
|
data/lib/shellopts/renderer.rb
CHANGED
@@ -38,13 +38,19 @@ module ShellOpts
|
|
38
38
|
#
|
39
39
|
def render(format)
|
40
40
|
constrain format, :enum, :long, :short
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
s =
|
42
|
+
case format
|
43
|
+
when :enum; names.join(", ")
|
44
|
+
when :long; name
|
45
|
+
when :short; short_names.first || name
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Illegal format: #{format.inspect}"
|
48
|
+
end
|
49
|
+
if argument?
|
50
|
+
s + (optional? ? "[=#{argument_name}]" : "=#{argument_name}")
|
45
51
|
else
|
46
|
-
|
47
|
-
end
|
52
|
+
s
|
53
|
+
end
|
48
54
|
end
|
49
55
|
end
|
50
56
|
|
@@ -75,17 +81,19 @@ module ShellOpts
|
|
75
81
|
COMMANDS_ABBR = "[COMMANDS]"
|
76
82
|
DESCRS_ABBR = "ARGS..."
|
77
83
|
|
78
|
-
# Format can be one of :single, :enum, or :multi. :
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
84
|
+
# Format can be one of :abbr, :single, :enum, or :multi. :abbr
|
85
|
+
# lists the command on one line with options abbreviated. :single force
|
86
|
+
# one-line output and compacts options and commands if needed. :enum
|
87
|
+
# outputs a :single line for each argument specification/description,
|
88
|
+
# :multi tries one-line output but wrap options if needed. Multiple
|
89
|
+
# argument specifications/descriptions are always compacted
|
83
90
|
#
|
84
91
|
def render(format, width, root: false, **opts)
|
85
92
|
case format
|
93
|
+
when :abbr; render_abbr
|
86
94
|
when :single; render_single(width, **opts)
|
87
95
|
when :enum; render_enum(width, **opts)
|
88
|
-
when :multi;
|
96
|
+
when :multi; render_multi(width, **opts)
|
89
97
|
else
|
90
98
|
raise ArgumentError, "Illegal format: #{format.inspect}"
|
91
99
|
end
|
@@ -96,6 +104,21 @@ module ShellOpts
|
|
96
104
|
end
|
97
105
|
|
98
106
|
protected
|
107
|
+
# TODO: Refactor and implement recursive detection of any argument
|
108
|
+
def get_args(args: nil)
|
109
|
+
case descrs.size
|
110
|
+
when 0; []
|
111
|
+
when 1; [descrs.first.text]
|
112
|
+
else [DESCRS_ABBR]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Force one line and compact options to "[OPTIONS]"
|
117
|
+
def render_abbr
|
118
|
+
args = get_args
|
119
|
+
([name] + [options.empty? ? nil : "[OPTIONS]"] + args).compact.join(" ")
|
120
|
+
end
|
121
|
+
|
99
122
|
# Force one line. Compact options, commands, arguments if needed
|
100
123
|
def render_single(width, args: nil)
|
101
124
|
long_options = options.map { |option| option.render(:long) }
|
@@ -104,13 +127,7 @@ module ShellOpts
|
|
104
127
|
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
105
128
|
compact_commands = commands.empty? ? [] : [COMMANDS_ABBR]
|
106
129
|
|
107
|
-
|
108
|
-
args ||=
|
109
|
-
case descrs.size
|
110
|
-
when 0; args = []
|
111
|
-
when 1; [descrs.first.text]
|
112
|
-
else [DESCRS_ABBR]
|
113
|
-
end
|
130
|
+
args ||= get_args
|
114
131
|
|
115
132
|
begin # to be able to use 'break' below
|
116
133
|
words = [name] + long_options + short_commands + args
|
@@ -149,36 +166,8 @@ module ShellOpts
|
|
149
166
|
short_options = options.map { |option| option.render(:short) }
|
150
167
|
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
151
168
|
compact_commands = [COMMANDS_ABBR]
|
152
|
-
args ||= self.descrs.size != 1 ? [DESCRS_ABBR] : descrs.map(&:text)
|
153
|
-
|
154
|
-
# On one line
|
155
|
-
words = long_options + short_commands + args
|
156
|
-
return [words.join(" ")] if pass?(words, width)
|
157
|
-
words = short_options + short_commands + args
|
158
|
-
return [words.join(" ")] if pass?(words, width)
|
159
|
-
|
160
|
-
# On multiple lines
|
161
|
-
options = long_options.wrap(width)
|
162
|
-
commands = [[short_commands, args].join(" ")]
|
163
|
-
return options + commands if pass?(commands, width)
|
164
|
-
options + [[compact_commands, args].join(" ")]
|
165
|
-
end
|
166
|
-
|
167
|
-
# Try to keep on one line but wrap options if needed. Multiple argument
|
168
|
-
# specifications/descriptions are always compacted
|
169
|
-
def render_multi2(width, args: nil)
|
170
|
-
long_options = options.map { |option| option.render(:long) }
|
171
|
-
short_options = options.map { |option| option.render(:short) }
|
172
|
-
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
173
|
-
compact_commands = [COMMANDS_ABBR]
|
174
169
|
|
175
|
-
|
176
|
-
args ||=
|
177
|
-
case descrs.size
|
178
|
-
when 0; args = []
|
179
|
-
when 1; [descrs.first.text]
|
180
|
-
else [DESCRS_ABBR]
|
181
|
-
end
|
170
|
+
args ||= get_args
|
182
171
|
|
183
172
|
# On one line
|
184
173
|
words = [name] + long_options + short_commands + args
|
data/lib/shellopts/version.rb
CHANGED
data/lib/shellopts.rb
CHANGED
@@ -1,12 +1,6 @@
|
|
1
1
|
|
2
|
-
$quiet = nil
|
3
|
-
$verb = nil
|
4
|
-
$debug = nil
|
5
|
-
$shellopts = nil
|
6
|
-
|
7
2
|
require 'indented_io'
|
8
3
|
|
9
|
-
#$LOAD_PATH.unshift "../constrain/lib"
|
10
4
|
require 'constrain'
|
11
5
|
include Constrain
|
12
6
|
|
@@ -98,14 +92,22 @@ module ShellOpts
|
|
98
92
|
# Array of remaining arguments. Initialized by #interpret
|
99
93
|
attr_reader :args
|
100
94
|
|
101
|
-
#
|
102
|
-
|
103
|
-
|
95
|
+
# Automatically add a -h and a --help option if true
|
96
|
+
attr_reader :help
|
97
|
+
|
98
|
+
# Version of client program. If not nil a --version option is added to the program
|
99
|
+
def version
|
100
|
+
return @version if @version
|
101
|
+
exe = caller.find { |line| line =~ /`<top \(required\)>'$/ }&.sub(/:.*/, "")
|
102
|
+
file = Dir.glob(File.dirname(exe) + "/../lib/*/version.rb").first
|
103
|
+
@version = IO.read(file).sub(/^.*VERSION\s*=\s*"(.*?)".*$/m, '\1') or
|
104
|
+
raise ArgumentError, "ShellOpts needs an explicit version"
|
105
|
+
end
|
104
106
|
|
105
|
-
#
|
106
|
-
|
107
|
+
# Add message options (TODO)
|
108
|
+
attr_accessor :msgopts
|
107
109
|
|
108
|
-
#
|
110
|
+
# Floating options
|
109
111
|
attr_accessor :float
|
110
112
|
|
111
113
|
# True if ShellOpts lets exceptions through instead of writing an error
|
@@ -117,11 +119,14 @@ module ShellOpts
|
|
117
119
|
|
118
120
|
# Debug: Internal variables made public
|
119
121
|
attr_reader :tokens
|
120
|
-
alias_method :ast, :grammar
|
122
|
+
alias_method :ast, :grammar
|
121
123
|
|
122
|
-
def initialize(name: nil,
|
124
|
+
def initialize(name: nil, help: true, version: true, msgopts: false, float: true, exception: false)
|
123
125
|
@name = name || File.basename($PROGRAM_NAME)
|
124
|
-
@
|
126
|
+
@help = help
|
127
|
+
@use_version = version ? true : false
|
128
|
+
@version = @use_version && @version != true ? @version : nil
|
129
|
+
@msgopts, @float, @exception = msgopts, float, exception
|
125
130
|
end
|
126
131
|
|
127
132
|
# Compile source and return grammar object. Also sets #spec and #grammar.
|
@@ -133,7 +138,8 @@ module ShellOpts
|
|
133
138
|
@file = find_caller_file
|
134
139
|
@tokens = Lexer.lex(name, @spec, @oneline)
|
135
140
|
ast = Parser.parse(tokens)
|
136
|
-
ast.
|
141
|
+
ast.add_version_option if @use_version
|
142
|
+
ast.add_help_options if @help
|
137
143
|
@grammar = Analyzer.analyze(ast)
|
138
144
|
}
|
139
145
|
self
|
@@ -146,19 +152,16 @@ module ShellOpts
|
|
146
152
|
handle_exceptions {
|
147
153
|
@argv = argv.dup
|
148
154
|
@program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
|
149
|
-
if
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
else
|
158
|
-
ShellOpts.help
|
159
|
-
end
|
160
|
-
exit
|
155
|
+
if @program.version?
|
156
|
+
puts version
|
157
|
+
exit
|
158
|
+
elsif @program.help?
|
159
|
+
if @program[:help].name == "-h"
|
160
|
+
ShellOpts.brief
|
161
|
+
else
|
162
|
+
ShellOpts.help
|
161
163
|
end
|
164
|
+
exit
|
162
165
|
end
|
163
166
|
}
|
164
167
|
self
|
@@ -189,23 +192,23 @@ module ShellOpts
|
|
189
192
|
#
|
190
193
|
# #error is supposed to be used when the user made an error and the usage
|
191
194
|
# is written to help correcting the error
|
192
|
-
#
|
193
195
|
def error(subject = nil, message)
|
194
|
-
saved = $stdout
|
195
|
-
$stdout = $stderr
|
196
196
|
$stderr.puts "#{name}: #{message}"
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
197
|
+
saved = $stdout
|
198
|
+
begin
|
199
|
+
$stdout = $stderr
|
200
|
+
Formatter.usage(grammar)
|
201
|
+
exit 1
|
202
|
+
ensure
|
203
|
+
$stdout = saved
|
204
|
+
end
|
201
205
|
end
|
202
206
|
|
203
207
|
# Write error message to standard error and terminate program with status 1
|
204
208
|
#
|
205
|
-
# #failure
|
206
|
-
#
|
207
|
-
#
|
208
|
-
#
|
209
|
+
# #failure doesn't print the program usage because is supposed to be used
|
210
|
+
# when the user specified the correct arguments but something else went
|
211
|
+
# wrong during processing
|
209
212
|
def failure(message)
|
210
213
|
$stderr.puts "#{name}: #{message}"
|
211
214
|
exit 1
|
@@ -219,7 +222,6 @@ module ShellOpts
|
|
219
222
|
|
220
223
|
# Print help for the given subject or the full documentation if +subject+
|
221
224
|
# is nil. Clears the screen beforehand if :clear is true
|
222
|
-
#
|
223
225
|
def help(subject = nil, clear: true)
|
224
226
|
node = (subject ? @grammar[subject] : @grammar) or
|
225
227
|
raise ArgumentError, "No such command: '#{subject&.sub(".", " ")}'"
|
@@ -330,7 +332,17 @@ module ShellOpts
|
|
330
332
|
def self.instance=(instance) @instance = instance end
|
331
333
|
def self.shellopts() instance end
|
332
334
|
|
333
|
-
|
335
|
+
def self.error(subject = nil, message)
|
336
|
+
instance.error(subject, message) if instance? # Never returns
|
337
|
+
$stderr.puts "#{File.basename($PROGRAM_NAME)}: #{message}"
|
338
|
+
exit 1
|
339
|
+
end
|
340
|
+
|
341
|
+
def self.failure(message)
|
342
|
+
instance.failure(message) if instance?
|
343
|
+
$stderr.puts "#{File.basename($PROGRAM_NAME)}: #{message}"
|
344
|
+
exit 1
|
345
|
+
end
|
334
346
|
|
335
347
|
# The Include module brings the reporting methods into the namespace when
|
336
348
|
# included
|
@@ -364,132 +376,3 @@ module ShellOpts
|
|
364
376
|
end
|
365
377
|
end
|
366
378
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
__END__
|
375
|
-
|
376
|
-
require "shellopts/version"
|
377
|
-
|
378
|
-
require "ext/algorithm.rb"
|
379
|
-
require "ext/ruby_env.rb"
|
380
|
-
|
381
|
-
require "shellopts/constants.rb"
|
382
|
-
require "shellopts/exceptions.rb"
|
383
|
-
|
384
|
-
require "shellopts/grammar/analyzer.rb"
|
385
|
-
require "shellopts/grammar/lexer.rb"
|
386
|
-
require "shellopts/grammar/parser.rb"
|
387
|
-
require "shellopts/grammar/command.rb"
|
388
|
-
require "shellopts/grammar/option.rb"
|
389
|
-
|
390
|
-
require "shellopts/ast/parser.rb"
|
391
|
-
require "shellopts/ast/command.rb"
|
392
|
-
require "shellopts/ast/option.rb"
|
393
|
-
|
394
|
-
require "shellopts/args.rb"
|
395
|
-
require "shellopts/formatter.rb"
|
396
|
-
|
397
|
-
if RUBY_ENV == "development"
|
398
|
-
require "shellopts/grammar/dump.rb"
|
399
|
-
require "shellopts/ast/dump.rb"
|
400
|
-
end
|
401
|
-
|
402
|
-
$verb = nil
|
403
|
-
$quiet = nil
|
404
|
-
$shellopts = nil
|
405
|
-
|
406
|
-
module ShellOpts
|
407
|
-
class ShellOpts
|
408
|
-
attr_reader :name # Name of program. Defaults to the name of the executable
|
409
|
-
attr_reader :spec
|
410
|
-
attr_reader :argv
|
411
|
-
|
412
|
-
attr_reader :grammar
|
413
|
-
attr_reader :program
|
414
|
-
attr_reader :arguments
|
415
|
-
|
416
|
-
def initialize(spec, argv, name: nil, exception: false)
|
417
|
-
@name = name || File.basename($PROGRAM_NAME)
|
418
|
-
@spec, @argv = spec, argv.dup
|
419
|
-
exprs = Grammar::Lexer.lex(@spec)
|
420
|
-
commands = Grammar::Parser.parse(@name, exprs)
|
421
|
-
@grammar = Grammar::Analyzer.analyze(commands)
|
422
|
-
|
423
|
-
begin
|
424
|
-
@program, @arguments = Ast::Parser.parse(@grammar, @argv)
|
425
|
-
rescue Error => ex
|
426
|
-
raise if exception
|
427
|
-
error(ex.subject, ex.message)
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
def error(subject = nil, message)
|
432
|
-
$stderr.puts "#{name}: #{message}"
|
433
|
-
usage(subject, device: $stderr)
|
434
|
-
exit 1
|
435
|
-
end
|
436
|
-
|
437
|
-
def fail(message)
|
438
|
-
$stderr.puts "#{name}: #{message}"
|
439
|
-
exit 1
|
440
|
-
end
|
441
|
-
|
442
|
-
def usage(subject = nil, device: $stdout, levels: 1, margin: "")
|
443
|
-
subject = find_subject(subject)
|
444
|
-
device.puts Formatter.usage_string(subject, levels: levels, margin: margin)
|
445
|
-
end
|
446
|
-
|
447
|
-
def help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
|
448
|
-
subject = find_subject(subject)
|
449
|
-
device.puts Formatter.help_string(subject, levels: levels, margin: margin, tab: tab)
|
450
|
-
end
|
451
|
-
|
452
|
-
private
|
453
|
-
def lookup(name)
|
454
|
-
a = name.split(".")
|
455
|
-
cmd = grammar
|
456
|
-
while element = a.shift
|
457
|
-
cmd = cmd.commands[element]
|
458
|
-
end
|
459
|
-
cmd
|
460
|
-
end
|
461
|
-
|
462
|
-
def find_subject(obj)
|
463
|
-
case obj
|
464
|
-
when String; lookup(obj)
|
465
|
-
when Ast::Command; Command.grammar(obj)
|
466
|
-
when Grammar::Command; obj
|
467
|
-
when NilClass; grammar
|
468
|
-
else
|
469
|
-
raise Internal, "Illegal object: #{obj.class}"
|
470
|
-
end
|
471
|
-
end
|
472
|
-
end
|
473
|
-
|
474
|
-
def self.process(spec, argv, name: nil, exception: false)
|
475
|
-
$shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
|
476
|
-
[$shellopts.program, $shellopts.arguments]
|
477
|
-
end
|
478
|
-
|
479
|
-
def self.error(subject = nil, message)
|
480
|
-
$shellopts.error(subject, message)
|
481
|
-
end
|
482
|
-
|
483
|
-
def self.fail(message)
|
484
|
-
$shellopts.fail(message)
|
485
|
-
end
|
486
|
-
|
487
|
-
def self.help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
|
488
|
-
$shellopts.help(subject, device: device, levels: levels, margin: margin, tab: tab)
|
489
|
-
end
|
490
|
-
|
491
|
-
def self.usage(subject = nil, device: $stdout, levels: 1, margin: "")
|
492
|
-
$shellopts.usage(subject, device: device, levels: levels, margin: margin)
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
data/main
CHANGED
@@ -7,12 +7,15 @@ require 'shellopts'
|
|
7
7
|
|
8
8
|
include ShellOpts
|
9
9
|
|
10
|
+
p ShellOpts::ShellOpts.default_version
|
11
|
+
exit
|
12
|
+
|
10
13
|
VERSION = "1.2.3"
|
11
14
|
|
12
15
|
SPEC = %(
|
13
16
|
-a @ An option
|
14
17
|
)
|
15
|
-
opts, args = ShellOpts
|
18
|
+
opts, args = ShellOpts.process(SPEC, ARGV, version: VERSION)
|
16
19
|
#ShellOpts::ShellOpts.help
|
17
20
|
|
18
21
|
|
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.0.
|
4
|
+
version: 2.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: forward_to
|