shellopts 2.0.0.pre.14 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 +25 -15
  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 +359 -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
@@ -0,0 +1,158 @@
1
+ module ShellOpts
2
+ module Grammar
3
+ class IdrNode
4
+ def dump_doc
5
+ puts "#{self.class} #{ident}"
6
+ indent {
7
+ children.each(&:dump_doc)
8
+ }
9
+ end
10
+ end
11
+ end
12
+
13
+ module Grammar
14
+ class Command
15
+ # Usable after parsing
16
+ def render_structure
17
+ io = StringIO.new
18
+ dump_structure(io)
19
+ io.string
20
+ end
21
+
22
+ def dump_structure(device = $stdout)
23
+ device.puts ident
24
+ device.indent { |dev|
25
+ option_groups.each { |group| dev.puts group.options.map(&:name).join(" ") }
26
+ commands.each { |command| command.dump_structure(dev) }
27
+ descrs.each { |descr| dev.puts descr.text }
28
+ }
29
+ end
30
+ end
31
+ end
32
+
33
+ module Grammar
34
+ class Node
35
+ def dump_ast
36
+ puts "#{classname} @ #{token.pos} #{token.source}"
37
+ indent { children.each(&:dump_ast) }
38
+ end
39
+
40
+ def dump_idr(short = false)
41
+ puts "#{classname}" if !short
42
+ end
43
+
44
+ def dump_attrs(*attrs)
45
+ indent {
46
+ Array(attrs).flatten.select { |attr| attr.is_a?(Symbol) }.each { |attr|
47
+ value = self.send(attr)
48
+ case value
49
+ when Brief
50
+ puts "#{attr}: #{value.text}"
51
+ when Node
52
+ puts "#{attr}:"
53
+ indent { value.dump_idr }
54
+ when Array
55
+ case value.first
56
+ when nil
57
+ puts "#{attr}: []"
58
+ when Node
59
+ puts "#{attr}:"
60
+ indent { value.each(&:dump_idr) }
61
+ else
62
+ puts "#{attr}: #{value.inspect}"
63
+ end
64
+ when ArgumentType
65
+ puts "#{attr}: #{value}"
66
+ else
67
+ # value = value.inspect if value.nil? || !value.respond_to?(:to_s)
68
+ puts "#{attr}: #{value.inspect}"
69
+ end
70
+ }
71
+ }
72
+ end
73
+
74
+ protected
75
+ def classname() self.class.to_s.sub(/.*::/, "") end
76
+ end
77
+
78
+ class Option
79
+ def dump_idr(short = false)
80
+ if short
81
+ s = [
82
+ name,
83
+ argument? ? argument_type.name : nil,
84
+ optional? ? "?" : nil,
85
+ repeatable? ? "*" : nil
86
+ ].compact.join(" ")
87
+ puts s
88
+ else
89
+ puts "#{name}: #{classname}"
90
+ dump_attrs(
91
+ :uid, :path, :attr, :ident, :name, :idents, :names,
92
+ :repeatable?,
93
+ :argument?, argument? && :argument_name, argument? && :argument_type,
94
+ :enum?, enum? && :argument_enum,
95
+ :optional?)
96
+ indent { puts "brief: #{group.brief}" }
97
+ end
98
+ end
99
+ end
100
+
101
+ class Command
102
+ def dump_idr(short = false)
103
+ if short
104
+ puts name
105
+ indent {
106
+ options.each { |option| option.dump_idr(short) }
107
+ commands.each { |command| command.dump_idr(short) }
108
+ descrs.each { |descr| descr.dump_idr(short) }
109
+ }
110
+ else
111
+ puts "#{name}: #{classname}"
112
+ dump_attrs :uid, :path, :ident, :name, :options, :commands, :specs, :descrs, :brief
113
+ end
114
+ end
115
+ end
116
+
117
+ class ArgDescr
118
+ def dump_idr(short = false)
119
+ super
120
+ indent { puts token.to_s }
121
+ end
122
+ end
123
+
124
+ class ArgSpec < Node
125
+ def dump_idr(short = false)
126
+ super
127
+ dump_attrs :arguments
128
+ end
129
+ end
130
+
131
+ class Arg < Node
132
+ def dump_idr(short = false)
133
+ puts "<type>"
134
+ end
135
+ end
136
+ end
137
+
138
+ class Command
139
+ def __dump__(argv = [])
140
+ ::Kernel.puts __name__
141
+ ::Kernel.indent {
142
+ __options__.each { |ident, value| ::Kernel.puts "#{ident}: #{value.inspect}" }
143
+ __subcommand__!&.__dump__
144
+ ::Kernel.puts argv.map(&:inspect).join(" ") if !argv.empty?
145
+ }
146
+ end
147
+
148
+ # Class-level accessor methods
149
+ def self.dump(expr, argv = []) expr.__dump__(argv) end
150
+ end
151
+
152
+ class Option
153
+ def dump
154
+ ::Kernel.puts [name, argument].compact.join(" ")
155
+ end
156
+ end
157
+ end
158
+
@@ -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