shellopts 2.0.0.pre.13 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) 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 +46 -134
  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 +58 -5
  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 +325 -0
  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 +269 -82
  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 +2 -2
  28. data/lib/shellopts.rb +439 -220
  29. data/main +1180 -0
  30. data/shellopts.gemspec +9 -15
  31. metadata +85 -42
  32. data/lib/main.rb +0 -1
  33. data/lib/shellopts/ast/command.rb +0 -41
  34. data/lib/shellopts/ast/node.rb +0 -37
  35. data/lib/shellopts/ast/option.rb +0 -21
  36. data/lib/shellopts/ast/program.rb +0 -14
  37. data/lib/shellopts/compiler.rb +0 -128
  38. data/lib/shellopts/generator.rb +0 -15
  39. data/lib/shellopts/grammar/command.rb +0 -80
  40. data/lib/shellopts/grammar/node.rb +0 -33
  41. data/lib/shellopts/grammar/option.rb +0 -66
  42. data/lib/shellopts/grammar/program.rb +0 -65
  43. data/lib/shellopts/idr.rb +0 -236
  44. data/lib/shellopts/main.rb +0 -10
  45. data/lib/shellopts/option_struct.rb +0 -148
  46. data/lib/shellopts/shellopts.rb +0 -123
@@ -0,0 +1,227 @@
1
+ require 'terminfo'
2
+
3
+ # Option rendering
4
+ # -a, --all # Only used in brief and doc formats (enum)
5
+ # --all # Only used in usage (long)
6
+ # -a # Only used in usage (short)
7
+ #
8
+ # Option group rendering
9
+ # -a, --all -b, --beta # Only used in brief formats (enum)
10
+ # --all --beta # Used in usage (long)
11
+ # -a -b # Used in usage (short)
12
+ #
13
+ # -a, --all # Only used in doc format (:multi)
14
+ # -b, --beta
15
+ #
16
+ # Command rendering
17
+ # cmd --all --beta [cmd1|cmd2] ARG1 ARG2 # Single-line formats (:single)
18
+ # cmd --all --beta [cmd1|cmd2] ARGS...
19
+ # cmd -a -b [cmd1|cmd2] ARG1 ARG2
20
+ # cmd -a -b [cmd1|cmd2] ARGS...
21
+ #
22
+ # cmd -a -b [cmd1|cmd2] ARG1 ARG2 # One line for each argument description (:enum)
23
+ # cmd -a -b [cmd1|cmd2] ARG3 ARG4 # (used in the USAGE section)
24
+ #
25
+ # cmd --all --beta # Multi-line formats (:multi)
26
+ # [cmd1|cmd2] ARG1 ARG2
27
+ # cmd --all --beta
28
+ # <commands> ARGS
29
+ #
30
+ module ShellOpts
31
+ module Grammar
32
+ class Option
33
+ # Formats:
34
+ #
35
+ # :enum -a, --all
36
+ # :long --all
37
+ # :short -a
38
+ #
39
+ def render(format)
40
+ constrain format, :enum, :long, :short
41
+ case format
42
+ when :enum; names.join(", ")
43
+ when :long; name
44
+ when :short; short_names.first || name
45
+ else
46
+ raise ArgumentError, "Illegal format: #{format.inspect}"
47
+ end + (argument? ? "=#{argument_name}" : "")
48
+ end
49
+ end
50
+
51
+ class OptionGroup
52
+ # Formats:
53
+ #
54
+ # :enum -a, --all -r, --recursive
55
+ # :long --all --recursive
56
+ # :short -a -r
57
+ # :multi -a, --all
58
+ # -r, --recursive
59
+ #
60
+ def render(format)
61
+ constrain format, :enum, :long, :short, :multi
62
+ if format == :multi
63
+ options.map { |option| option.render(:enum) }.join("\n")
64
+ else
65
+ options.map { |option| option.render(format) }.join(" ")
66
+ end
67
+ end
68
+ end
69
+
70
+ # brief one-line commands should optionally use compact options
71
+ class Command
72
+ using Ext::Array::Wrap
73
+
74
+ OPTIONS_ABBR = "[OPTIONS]"
75
+ COMMANDS_ABBR = "[COMMANDS]"
76
+ DESCRS_ABBR = "ARGS..."
77
+
78
+ # Format can be one of :single, :enum, or :multi. :single force one-line
79
+ # output and compacts options and commands if needed. :enum outputs a
80
+ # :single line for each argument specification/description, :multi tries
81
+ # one-line output but wrap options if needed. Multiple argument
82
+ # specifications/descriptions are always compacted
83
+ #
84
+ def render(format, width, root: false, **opts)
85
+ case format
86
+ when :single; render_single(width, **opts)
87
+ when :enum; render_enum(width, **opts)
88
+ when :multi; render_multi2(width, **opts)
89
+ else
90
+ raise ArgumentError, "Illegal format: #{format.inspect}"
91
+ end
92
+ end
93
+
94
+ def names(root: false)
95
+ (root ? ancestors : []) + [self]
96
+ end
97
+
98
+ protected
99
+ # Force one line. Compact options, commands, arguments if needed
100
+ def render_single(width, args: nil)
101
+ long_options = options.map { |option| option.render(:long) }
102
+ short_options = options.map { |option| option.render(:short) }
103
+ compact_options = options.empty? ? [] : [OPTIONS_ABBR]
104
+ short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
105
+ compact_commands = commands.empty? ? [] : [COMMANDS_ABBR]
106
+
107
+ # TODO: Refactor and implement recursive detection of any argument
108
+ args ||=
109
+ case descrs.size
110
+ when 0; args = []
111
+ when 1; [descrs.first.text]
112
+ else [DESCRS_ABBR]
113
+ end
114
+
115
+ begin # to be able to use 'break' below
116
+ words = [name] + long_options + short_commands + args
117
+ break if pass?(words, width)
118
+ words = [name] + short_options + short_commands + args
119
+ break if pass?(words, width)
120
+ words = [name] + long_options + compact_commands + args
121
+ break if pass?(words, width)
122
+ words = [name] + short_options + compact_commands + args
123
+ break if pass?(words, width)
124
+ words = [name] + compact_options + short_commands + args
125
+ break if pass?(words, width)
126
+ words = [name] + compact_options + compact_commands + args
127
+ break if pass?(words, width)
128
+ words = [name] + compact_options + compact_commands + [DESCRS_ABBR]
129
+ end while false
130
+ words.join(" ")
131
+ end
132
+
133
+ # Render one line for each argument specification/description
134
+ def render_enum(width)
135
+ # TODO: Also refactor args here
136
+ args_texts = self.descrs.empty? ? [""] : descrs.map(&:text)
137
+ args_texts.map { |args_text| render_single(width, args: [args_text]) }
138
+ end
139
+
140
+ # Render the description using the given method (:single, :multi)
141
+ def render_descr(method, width, descr)
142
+ send.send method, width, args: descr
143
+ end
144
+
145
+ # Try to keep on one line but wrap options if needed. Multiple argument
146
+ # specifications/descriptions are always compacted
147
+ def render_multi(width, args: nil)
148
+ long_options = options.map { |option| option.render(:long) }
149
+ short_options = options.map { |option| option.render(:short) }
150
+ short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
151
+ 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
+
175
+ # TODO: Refactor and implement recursive detection of any argument
176
+ args ||=
177
+ case descrs.size
178
+ when 0; args = []
179
+ when 1; [descrs.first.text]
180
+ else [DESCRS_ABBR]
181
+ end
182
+
183
+ # On one line
184
+ words = [name] + long_options + short_commands + args
185
+ return [words.join(" ")] if pass?(words, width)
186
+ words = [name] + short_options + short_commands + args
187
+ return [words.join(" ")] if pass?(words, width)
188
+
189
+ # On multiple lines
190
+ lead = name + " "
191
+ options = long_options.wrap(width - lead.size)
192
+ options = [lead + options[0]] + indent_lines(lead.size, options[1..-1])
193
+
194
+ begin
195
+ words = short_commands + args
196
+ break if pass?(words, width)
197
+ words = compact_commands + args
198
+ break if pass?(words, width)
199
+ words = compact_commands + [DESCRS_ABBR]
200
+ end while false
201
+
202
+ cmdargs = words.empty? ? [] : [words.join(" ")]
203
+ options + indent_lines(lead.size, cmdargs)
204
+ end
205
+
206
+ protected
207
+ # Helper method that returns true if words can fit in width characters
208
+ def pass?(words, width)
209
+ words.sum(&:size) + words.size - 1 <= width
210
+ end
211
+
212
+ # Indent array of lines
213
+ def indent_lines(indent, lines)
214
+ indent = [indent, 0].max
215
+ lines.map { |line| ' ' * indent + line }
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+
@@ -0,0 +1,7 @@
1
+ module ShellOpts
2
+ module Stack
3
+ refine Array do
4
+ def top() last end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+
2
+ module ShellOpts
3
+ class Token
4
+ # Each kind should have a corresponding Grammar class with the same name
5
+ KINDS = [
6
+ :program, :section, :option, :command, :spec, :argument, :usage,
7
+ :usage_string, :brief, :text, :blank
8
+ ]
9
+
10
+ # Kind of token
11
+ attr_reader :kind
12
+
13
+ # Line number (one-based)
14
+ attr_reader :lineno
15
+
16
+ # Char number (one-based). The lexer may adjust the char number (eg. for
17
+ # blank lines)
18
+ attr_accessor :charno
19
+
20
+ # Source of the token
21
+ attr_reader :source
22
+
23
+ def initialize(kind, lineno, charno, source)
24
+ constrain kind, :program, *KINDS
25
+ @kind, @lineno, @charno, @source = kind, lineno, charno, source
26
+ end
27
+
28
+ forward_to :source, :to_s, :empty?
29
+
30
+ def pos(start_lineno = 1, start_charno = 1)
31
+ "#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
32
+ end
33
+
34
+ def to_s() source end
35
+
36
+ def inspect()
37
+ "<#{self.class.to_s.sub(/.*::/, "")} #{pos} #{kind.inspect} #{source.inspect}>"
38
+ end
39
+
40
+ def dump
41
+ puts "#{kind}@#{lineno}:#{charno} #{source.inspect}"
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
- module Shellopts
2
- VERSION = "2.0.0.pre.13"
1
+ module ShellOpts
2
+ VERSION = "2.0.1"
3
3
  end