shellopts 2.0.0.pre.13 → 2.0.1
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 +46 -134
- 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 +58 -5
- 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 +325 -0
- 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 +269 -82
- 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 +2 -2
- data/lib/shellopts.rb +439 -220
- data/main +1180 -0
- data/shellopts.gemspec +9 -15
- metadata +85 -42
- data/lib/main.rb +0 -1
- data/lib/shellopts/ast/command.rb +0 -41
- data/lib/shellopts/ast/node.rb +0 -37
- data/lib/shellopts/ast/option.rb +0 -21
- data/lib/shellopts/ast/program.rb +0 -14
- data/lib/shellopts/compiler.rb +0 -128
- data/lib/shellopts/generator.rb +0 -15
- data/lib/shellopts/grammar/command.rb +0 -80
- data/lib/shellopts/grammar/node.rb +0 -33
- data/lib/shellopts/grammar/option.rb +0 -66
- data/lib/shellopts/grammar/program.rb +0 -65
- data/lib/shellopts/idr.rb +0 -236
- data/lib/shellopts/main.rb +0 -10
- data/lib/shellopts/option_struct.rb +0 -148
- 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,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
|
data/lib/shellopts/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module
|
2
|
-
VERSION = "2.0.
|
1
|
+
module ShellOpts
|
2
|
+
VERSION = "2.0.1"
|
3
3
|
end
|