shellopts 2.0.0.pre.14 → 2.0.2
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/.gitignore +2 -1
- data/.ruby-version +1 -1
- data/README.md +201 -267
- data/TODO +37 -5
- data/doc/format.rb +95 -0
- data/doc/grammar.txt +27 -0
- data/doc/syntax.rb +110 -0
- data/doc/syntax.txt +10 -0
- data/lib/ext/array.rb +62 -0
- data/lib/ext/forward_to.rb +15 -0
- data/lib/ext/lcs.rb +34 -0
- data/lib/shellopts/analyzer.rb +130 -0
- data/lib/shellopts/ansi.rb +8 -0
- data/lib/shellopts/args.rb +29 -21
- data/lib/shellopts/argument_type.rb +139 -0
- data/lib/shellopts/dump.rb +158 -0
- data/lib/shellopts/formatter.rb +292 -92
- data/lib/shellopts/grammar.rb +375 -0
- data/lib/shellopts/interpreter.rb +103 -0
- data/lib/shellopts/lexer.rb +175 -0
- data/lib/shellopts/parser.rb +293 -0
- data/lib/shellopts/program.rb +279 -0
- data/lib/shellopts/renderer.rb +227 -0
- data/lib/shellopts/stack.rb +7 -0
- data/lib/shellopts/token.rb +44 -0
- data/lib/shellopts/version.rb +1 -1
- data/lib/shellopts.rb +360 -3
- data/main +1180 -0
- data/shellopts.gemspec +8 -14
- metadata +86 -41
- data/lib/ext/algorithm.rb +0 -14
- data/lib/ext/ruby_env.rb +0 -8
- data/lib/shellopts/ast/command.rb +0 -112
- data/lib/shellopts/ast/dump.rb +0 -28
- data/lib/shellopts/ast/option.rb +0 -15
- data/lib/shellopts/ast/parser.rb +0 -106
- data/lib/shellopts/constants.rb +0 -88
- data/lib/shellopts/exceptions.rb +0 -21
- data/lib/shellopts/grammar/analyzer.rb +0 -76
- data/lib/shellopts/grammar/command.rb +0 -87
- data/lib/shellopts/grammar/dump.rb +0 -56
- data/lib/shellopts/grammar/lexer.rb +0 -56
- data/lib/shellopts/grammar/option.rb +0 -55
- data/lib/shellopts/grammar/parser.rb +0 -78
data/lib/shellopts/formatter.rb
CHANGED
@@ -1,124 +1,324 @@
|
|
1
|
+
require 'terminfo'
|
1
2
|
|
2
|
-
|
3
|
-
|
4
|
-
|
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
|
5
9
|
|
6
10
|
module ShellOpts
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
"#{margin}#{line}"
|
12
|
-
}.join("\n")
|
13
|
-
end
|
14
|
-
|
15
|
-
# Return string with help for the given command
|
16
|
-
def self.help_string(command, levels: 10, margin: "", tab: " ")
|
17
|
-
io = StringIO.new
|
18
|
-
elements(command, levels: levels, help: true).each { |head, texts, options|
|
19
|
-
io.puts "#{margin}#{head}"
|
20
|
-
texts.each { |text| io.puts "#{margin}#{tab}#{text}" }
|
21
|
-
options.each { |opt_head, opt_texts|
|
22
|
-
io.puts
|
23
|
-
io.puts "#{margin}#{tab}#{opt_head}"
|
24
|
-
opt_texts.each { |text| io.puts "#{margin}#{tab*2}#{text}" }
|
25
|
-
}
|
26
|
-
io.puts
|
27
|
-
}
|
28
|
-
io.string[0..-2]
|
11
|
+
module Grammar
|
12
|
+
class Node
|
13
|
+
def puts_help() end
|
14
|
+
def puts_usage() end
|
29
15
|
end
|
30
16
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
usage = (
|
43
|
-
path_elements(cmd) +
|
44
|
-
option_elements(cmd) +
|
45
|
-
subcommand_element(cmd) +
|
46
|
-
argument_elements(cmd)
|
47
|
-
).compact.join(" ")
|
48
|
-
if help
|
49
|
-
opts = []
|
50
|
-
cmd.opts.each { |opt|
|
51
|
-
next if opt.text.empty?
|
52
|
-
opts << [option_help(opt), opt.text]
|
17
|
+
class Option
|
18
|
+
end
|
19
|
+
|
20
|
+
class OptionGroup
|
21
|
+
def puts_descr
|
22
|
+
puts Ansi.bold(render(:multi))
|
23
|
+
indent {
|
24
|
+
if description.any?
|
25
|
+
description.each { |descr|
|
26
|
+
descr.puts_descr
|
27
|
+
puts if descr != description.last
|
53
28
|
}
|
54
|
-
|
55
|
-
|
56
|
-
acc << usage
|
29
|
+
elsif brief
|
30
|
+
brief.puts_descr
|
57
31
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# brief one-line commands should optionally use compact options
|
37
|
+
class Command
|
38
|
+
using Ext::Array::Wrap
|
39
|
+
|
40
|
+
def puts_usage(bol: false)
|
41
|
+
if descrs.size == 0
|
42
|
+
print (lead = Formatter.command_prefix || "")
|
43
|
+
indent(lead.size, ' ', bol: bol && lead == "") {
|
44
|
+
puts render(:multi, Formatter::USAGE_MAX_WIDTH)
|
61
45
|
}
|
46
|
+
else
|
47
|
+
lead = Formatter.command_prefix || ""
|
48
|
+
descrs.each { |descr|
|
49
|
+
print lead
|
50
|
+
puts render(:single, Formatter::USAGE_MAX_WIDTH, args: [descr.text])
|
51
|
+
}
|
62
52
|
end
|
63
|
-
|
53
|
+
end
|
54
|
+
|
55
|
+
def puts_brief
|
56
|
+
width = Formatter.rest
|
57
|
+
option_briefs = option_groups.map { |group| [group.render(:enum), group.brief&.words] }
|
58
|
+
command_briefs = commands.map { |command| [command.render(:single, width), command.brief&.words] }
|
59
|
+
widths = Formatter::compute_columns(width, option_briefs + command_briefs)
|
60
|
+
|
61
|
+
if brief
|
62
|
+
puts brief
|
63
|
+
puts
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "Usage"
|
67
|
+
indent { puts_usage(bol: true) }
|
68
|
+
|
69
|
+
if options.any?
|
70
|
+
puts
|
71
|
+
puts "Options"
|
72
|
+
indent { Formatter::puts_columns(widths, option_briefs) }
|
73
|
+
end
|
74
|
+
|
75
|
+
if commands.any?
|
76
|
+
puts
|
77
|
+
puts "Commands"
|
78
|
+
indent { Formatter::puts_columns(widths, command_briefs) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
|
83
|
+
puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
|
84
|
+
indent {
|
85
|
+
if brief
|
86
|
+
puts self.brief.words.wrap(Formatter.rest)
|
87
|
+
else
|
88
|
+
newline = false
|
89
|
+
children.each { |child|
|
90
|
+
puts if newline
|
91
|
+
newline = true
|
92
|
+
|
93
|
+
if child.is_a?(Command)
|
94
|
+
child.puts_descr(prefix, name: :path)
|
95
|
+
else
|
96
|
+
child.puts_descr
|
97
|
+
end
|
98
|
+
}
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def puts_help
|
104
|
+
puts Ansi.bold "NAME"
|
105
|
+
full_name = [Formatter::command_prefix, name].join
|
106
|
+
indent { puts brief ? "#{full_name} - #{brief}" : full_name }
|
107
|
+
puts
|
108
|
+
|
109
|
+
puts Ansi.bold "USAGE"
|
110
|
+
indent { puts_usage(bol: true) }
|
111
|
+
|
112
|
+
section = {
|
113
|
+
Paragraph => "DESCRIPTION",
|
114
|
+
OptionGroup => "OPTIONS",
|
115
|
+
Command => "COMMANDS"
|
116
|
+
}
|
117
|
+
|
118
|
+
newline = false # True if a newline should be printed before child
|
119
|
+
indent {
|
120
|
+
children.each { |child|
|
121
|
+
if child.is_a?(Section) # Explicit section
|
122
|
+
# p :A
|
123
|
+
puts
|
124
|
+
indent(-1).puts Ansi.bold child.name
|
125
|
+
section.delete_if { |_,v| v == child.name }
|
126
|
+
section.delete(Paragraph)
|
127
|
+
newline = false
|
128
|
+
next
|
129
|
+
elsif s = section[child.class] # Implicit section
|
130
|
+
# p :B
|
131
|
+
puts
|
132
|
+
indent(-1).puts Ansi.bold s
|
133
|
+
section.delete(child.class)
|
134
|
+
section.delete(Paragraph)
|
135
|
+
newline = false
|
136
|
+
else # Any other node add a newline
|
137
|
+
# p :C
|
138
|
+
puts if newline
|
139
|
+
newline = true
|
140
|
+
end
|
141
|
+
|
142
|
+
if child.is_a?(Command)
|
143
|
+
# prefix = child.parent != self ? nil : child.supercommand&.name
|
144
|
+
prefix = child.supercommand == self ? nil : child.supercommand&.name
|
145
|
+
child.puts_descr(prefix, brief: false, name: :path)
|
146
|
+
newline = true
|
147
|
+
else
|
148
|
+
child.puts_descr
|
149
|
+
newline = true
|
150
|
+
end
|
151
|
+
}
|
152
|
+
|
153
|
+
# Also emit commands not declared in nested scope
|
154
|
+
(commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
|
155
|
+
puts if newline
|
156
|
+
newline = true
|
157
|
+
prefix = cmd.supercommand == self ? nil : cmd.supercommand&.name
|
158
|
+
cmd.puts_descr(prefix, brief: false, name: path)
|
159
|
+
}
|
160
|
+
}
|
161
|
+
end
|
64
162
|
end
|
65
163
|
|
66
|
-
|
67
|
-
|
68
|
-
(path_elements(cmd) + option_elements(cmd) + argument_elements(cmd)).compact.join(" ")
|
164
|
+
class Program
|
165
|
+
using Ext::Array::Wrap
|
69
166
|
end
|
70
167
|
|
71
|
-
|
72
|
-
|
168
|
+
class DocNode
|
169
|
+
def puts_descr() puts lines end
|
73
170
|
end
|
74
171
|
|
75
|
-
|
76
|
-
|
77
|
-
|
172
|
+
module WrappedNode
|
173
|
+
def puts_descr(width = Formatter.rest) puts lines(width) end
|
174
|
+
end
|
78
175
|
|
79
|
-
|
80
|
-
|
81
|
-
|
176
|
+
class Code
|
177
|
+
def puts_descr() indent { super } end
|
178
|
+
end
|
179
|
+
end
|
82
180
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
181
|
+
class Formatter
|
182
|
+
using Ext::Array::Wrap
|
183
|
+
|
184
|
+
# Right margin
|
185
|
+
MARGIN_RIGHT = 3
|
186
|
+
|
187
|
+
# String for 'Usage' in error messages
|
188
|
+
USAGE_STRING = "Usage"
|
189
|
+
|
190
|
+
# Indent to use in usage output
|
191
|
+
USAGE_INDENT = USAGE_STRING.size
|
192
|
+
|
193
|
+
# Width of usage (after usage string)
|
194
|
+
USAGE_MAX_WIDTH = 70
|
195
|
+
|
196
|
+
# Indent to use in brief output
|
197
|
+
BRIEF_INDENT = 2
|
198
|
+
|
199
|
+
# Number of characters between columns in brief output
|
200
|
+
BRIEF_COL_SEP = 2
|
201
|
+
|
202
|
+
# Maximum width of first column in brief option and command lists
|
203
|
+
BRIEF_COL1_MIN_WIDTH = 6
|
204
|
+
|
205
|
+
# Maximum width of first column in brief option and command lists
|
206
|
+
BRIEF_COL1_MAX_WIDTH = 40
|
207
|
+
|
208
|
+
# Minimum width of second column in brief option and command lists
|
209
|
+
BRIEF_COL2_MAX_WIDTH = 50
|
210
|
+
|
211
|
+
# Indent to use in help output
|
212
|
+
HELP_INDENT = 4
|
213
|
+
|
214
|
+
# Command prefix when subject is a sub-command
|
215
|
+
def self.command_prefix() @command_prefix end
|
216
|
+
|
217
|
+
# Usage string in error messages
|
218
|
+
def self.usage(subject)
|
219
|
+
subject = Grammar::Command.command(subject)
|
220
|
+
@command_prefix = subject.ancestors.map { |node| node.name + " " }.join
|
221
|
+
setup_indent(1) {
|
222
|
+
print lead = "#{USAGE_STRING}: "
|
223
|
+
indent(lead.size, ' ', bol: false) { subject.puts_usage }
|
91
224
|
}
|
92
225
|
end
|
93
226
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
227
|
+
# # TODO
|
228
|
+
# def self.usage=(usage_lambda)
|
229
|
+
# end
|
230
|
+
|
231
|
+
# When the user gives a -h option
|
232
|
+
def self.brief(command)
|
233
|
+
command = Grammar::Command.command(command)
|
234
|
+
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
235
|
+
setup_indent(BRIEF_INDENT) { command.puts_brief }
|
236
|
+
end
|
237
|
+
|
238
|
+
# # TODO
|
239
|
+
# def self.brief=(brief_lambda)
|
240
|
+
# end
|
241
|
+
|
242
|
+
# When the user gives a --help option
|
243
|
+
def self.help(subject)
|
244
|
+
subject = Grammar::Command.command(subject)
|
245
|
+
@command_prefix = subject.ancestors.map { |node| node.name + " " }.join
|
246
|
+
setup_indent(HELP_INDENT) { subject.puts_help }
|
247
|
+
end
|
248
|
+
|
249
|
+
# Short-hand to get the Grammar::Command object
|
250
|
+
def self.command_of(obj)
|
251
|
+
constrain obj, Grammar::Command, ::ShellOpts::Program
|
252
|
+
obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
|
253
|
+
end
|
254
|
+
|
255
|
+
# # TODO
|
256
|
+
# def self.help_w_lambda(program)
|
257
|
+
# if @help_lambda
|
258
|
+
# #
|
259
|
+
# else
|
260
|
+
# program = Grammar::Command.command(program)
|
261
|
+
# setup_indent(HELP_INDENT) { program.puts_descr }
|
262
|
+
# end
|
263
|
+
# end
|
264
|
+
#
|
265
|
+
# def self.help=(help_lambda) @help_lambda end
|
266
|
+
|
267
|
+
def self.puts_columns(widths, fields)
|
268
|
+
l = []
|
269
|
+
first_width, second_width = *widths
|
270
|
+
second_col = first_width + 2
|
271
|
+
|
272
|
+
for (first, second) in fields
|
273
|
+
if first.size > first_width
|
274
|
+
puts first
|
275
|
+
indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
|
276
|
+
elsif second
|
277
|
+
printf "%-#{first_width + BRIEF_COL_SEP}s", first
|
278
|
+
indent(first_width, bol: false) { puts second.wrap(second_width) }
|
105
279
|
else
|
106
|
-
|
280
|
+
puts first
|
107
281
|
end
|
108
282
|
end
|
109
|
-
result
|
110
283
|
end
|
111
284
|
|
112
|
-
def self.
|
113
|
-
|
285
|
+
def self.compute_columns(width, fields)
|
286
|
+
first_max = [
|
287
|
+
(fields.map { |first, _| first.size } + [BRIEF_COL1_MIN_WIDTH]).max,
|
288
|
+
BRIEF_COL1_MAX_WIDTH
|
289
|
+
].min
|
290
|
+
second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1: 0 }.max
|
291
|
+
|
292
|
+
if first_max + BRIEF_COL_SEP + second_max <= width
|
293
|
+
first_width = first_max
|
294
|
+
second_width = second_max
|
295
|
+
elsif first_max + BRIEF_COL_SEP + BRIEF_COL2_MAX_WIDTH <= width
|
296
|
+
first_width = first_max
|
297
|
+
second_width = width - first_width - BRIEF_COL_SEP
|
298
|
+
else
|
299
|
+
first_width = [width - BRIEF_COL_SEP - BRIEF_COL2_MAX_WIDTH, BRIEF_COL1_MAX_WIDTH].min
|
300
|
+
second_width = BRIEF_COL2_MAX_WIDTH
|
301
|
+
end
|
302
|
+
|
303
|
+
[first_width, second_width]
|
114
304
|
end
|
115
305
|
|
116
|
-
def self.
|
117
|
-
|
306
|
+
def self.width()
|
307
|
+
@width ||= TermInfo.screen_width - MARGIN_RIGHT
|
118
308
|
end
|
119
309
|
|
120
|
-
def self.
|
121
|
-
|
310
|
+
def self.rest() width - $stdout.margin end
|
311
|
+
|
312
|
+
private
|
313
|
+
# TODO Get rid of?
|
314
|
+
def self.setup_indent(indent, &block)
|
315
|
+
default_indent = IndentedIO.default_indent
|
316
|
+
begin
|
317
|
+
IndentedIO.default_indent = " " * indent
|
318
|
+
indent(0) { yield } # Ensure IndentedIO is on the top of the stack so we can use $stdout.levels
|
319
|
+
ensure
|
320
|
+
IndentedIO.default_indent = default_indent
|
321
|
+
end
|
122
322
|
end
|
123
323
|
end
|
124
324
|
end
|