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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +201 -267
  5. data/TODO +37 -5
  6. data/doc/format.rb +95 -0
  7. data/doc/grammar.txt +27 -0
  8. data/doc/syntax.rb +110 -0
  9. data/doc/syntax.txt +10 -0
  10. data/lib/ext/array.rb +62 -0
  11. data/lib/ext/forward_to.rb +15 -0
  12. data/lib/ext/lcs.rb +34 -0
  13. data/lib/shellopts/analyzer.rb +130 -0
  14. data/lib/shellopts/ansi.rb +8 -0
  15. data/lib/shellopts/args.rb +29 -21
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +292 -92
  19. data/lib/shellopts/grammar.rb +375 -0
  20. data/lib/shellopts/interpreter.rb +103 -0
  21. data/lib/shellopts/lexer.rb +175 -0
  22. data/lib/shellopts/parser.rb +293 -0
  23. data/lib/shellopts/program.rb +279 -0
  24. data/lib/shellopts/renderer.rb +227 -0
  25. data/lib/shellopts/stack.rb +7 -0
  26. data/lib/shellopts/token.rb +44 -0
  27. data/lib/shellopts/version.rb +1 -1
  28. data/lib/shellopts.rb +360 -3
  29. data/main +1180 -0
  30. data/shellopts.gemspec +8 -14
  31. metadata +86 -41
  32. data/lib/ext/algorithm.rb +0 -14
  33. data/lib/ext/ruby_env.rb +0 -8
  34. data/lib/shellopts/ast/command.rb +0 -112
  35. data/lib/shellopts/ast/dump.rb +0 -28
  36. data/lib/shellopts/ast/option.rb +0 -15
  37. data/lib/shellopts/ast/parser.rb +0 -106
  38. data/lib/shellopts/constants.rb +0 -88
  39. data/lib/shellopts/exceptions.rb +0 -21
  40. data/lib/shellopts/grammar/analyzer.rb +0 -76
  41. data/lib/shellopts/grammar/command.rb +0 -87
  42. data/lib/shellopts/grammar/dump.rb +0 -56
  43. data/lib/shellopts/grammar/lexer.rb +0 -56
  44. data/lib/shellopts/grammar/option.rb +0 -55
  45. data/lib/shellopts/grammar/parser.rb +0 -78
@@ -1,124 +1,324 @@
1
+ require 'terminfo'
1
2
 
2
- require 'ext/algorithm'
3
-
4
- require 'stringio'
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
- class Formatter
8
- # Return string describing usage of command
9
- def self.usage_string(command, levels: 1, margin: "")
10
- elements(command, levels: levels, help: false).map { |line|
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
- private
32
- def self.elements(command, levels: 1, help: false)
33
- result = []
34
- recursive_elements(result, command, levels: levels, help: help)
35
- result
36
- end
37
-
38
- def self.recursive_elements(acc, command, levels: 1, help: false)
39
- cmds = (command.virtual? ? command.cmds : [command])
40
- cmds.each { |cmd|
41
- if levels == 1 || cmd.cmds.empty?
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
- acc << [usage, cmd.text, opts]
55
- else
56
- acc << usage
29
+ elsif brief
30
+ brief.puts_descr
57
31
  end
58
- else
59
- cmd.cmds.each { |subcmd|
60
- recursive_elements(acc, subcmd, levels: levels - 1, help: help)
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
- # Return command line usage string
67
- def self.command(cmd)
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
- def self.path_elements(cmd)
72
- Algorithm.follow(cmd, :parent).map { |parent| parent.name }.reverse
168
+ class DocNode
169
+ def puts_descr() puts lines end
73
170
  end
74
171
 
75
- def self.option_elements(cmd)
76
- elements = []
77
- collapsable_opts, other_opts = cmd.opts.partition { |opt| opt.shortname && !opt.argument? }
172
+ module WrappedNode
173
+ def puts_descr(width = Formatter.rest) puts lines(width) end
174
+ end
78
175
 
79
- if !collapsable_opts.empty?
80
- elements << "-" + collapsable_opts.map(&:shortname).join
81
- end
176
+ class Code
177
+ def puts_descr() indent { super } end
178
+ end
179
+ end
82
180
 
83
- elements + other_opts.map { |opt|
84
- if opt.shortname
85
- "-#{opt.shortname} #{opt.argument_name}" # We know opt has an argument
86
- elsif opt.argument?
87
- "--#{opt.longname}=#{opt.argument_name}"
88
- else
89
- "--#{opt.longname}"
90
- end
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
- def self.option_help(opt)
95
- result = opt.names.map { |name|
96
- if name.size == 1
97
- "-#{name}"
98
- else
99
- "--#{name}"
100
- end
101
- }.join(", ")
102
- if opt.argument?
103
- if opt.longname
104
- result += "=#{opt.argument_name}"
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
- result += " #{opt.argument_name}"
280
+ puts first
107
281
  end
108
282
  end
109
- result
110
283
  end
111
284
 
112
- def self.subcommand_element(cmd)
113
- !cmd.cmds.empty? ? [cmd.cmds.map(&:name).join("|")] : []
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.argument_elements(cmd)
117
- cmd.args
306
+ def self.width()
307
+ @width ||= TermInfo.screen_width - MARGIN_RIGHT
118
308
  end
119
309
 
120
- def self.help_element(cmd)
121
- text.map { |l| l.sub(/^\s*# /, "").rstrip }.join(" ")
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