shellopts 2.0.8 → 2.0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/TODO +5 -5
- data/lib/ext/array.rb +3 -1
- data/lib/shellopts/analyzer.rb +47 -18
- data/lib/shellopts/formatter.rb +64 -72
- data/lib/shellopts/grammar.rb +16 -11
- data/lib/shellopts/parser.rb +13 -18
- data/lib/shellopts/program.rb +13 -4
- data/lib/shellopts/renderer.rb +37 -48
- data/lib/shellopts/version.rb +1 -1
- data/lib/shellopts.rb +31 -157
- data/main +4 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 485e81c58a890240d2a77e24a5f3a7a291551da968f395f3aaa4f5b2dac3cc9b
|
4
|
+
data.tar.gz: 2d6c92ca01ce1207f6311cf9b0e9bab3e555e309142ba6916d589396f215afb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31f3801b98585bed378ff77ae6632d18951ef37a49b3cfde843df9718dc5b46da40e777dff3f7b29e01bf8b32f2ebe0e1e7ab76ddb66744a461acf0061f75b1f
|
7
|
+
data.tar.gz: d72bcf1d58e9afdf4f92477af1f2824e01031a46b4cacf5283878ce1045ad91dd930c1c1a15cd588a2552a4ad90fe5ac3add6019b50e099693aa184b7747153f
|
data/TODO
CHANGED
@@ -1,18 +1,13 @@
|
|
1
1
|
|
2
|
-
o Add brackets to optional option arguments: '--all=FILE?' -> '--all[=FILE]'
|
3
2
|
o Ignore all text after ' # ' (doesn't conflict with option flag)
|
4
3
|
o Command aliases
|
5
4
|
o Add user-defined setions
|
6
5
|
o Add a SOURCE section with link to git repo
|
7
6
|
o Bullet-lists
|
8
7
|
o Allow a USAGE section (and NAME)
|
9
|
-
o Find source in code an adjust line number in error messages
|
10
|
-
o Rename line and char to lineno and charno
|
11
8
|
o Client-defined argument types
|
12
9
|
o Rename Expr -> ?
|
13
10
|
o Find clean(er) procedural object model
|
14
|
-
o Allow assignment to options (this makes practical stuff easier)
|
15
|
-
o Special handling of --help arguments so that '--help command' is possible
|
16
11
|
o Support for paging of help:
|
17
12
|
begin
|
18
13
|
file = Tempfile.new("prick")
|
@@ -23,6 +18,11 @@ o Support for paging of help:
|
|
23
18
|
file.close
|
24
19
|
end
|
25
20
|
|
21
|
+
+ Special handling of --help arguments so that '--help command' is possible
|
22
|
+
+ Allow assignment to options (this makes practical stuff easier)
|
23
|
+
+ Rename line and char to lineno and charno
|
24
|
+
+ Find source in code an adjust line number in error messages
|
25
|
+
+ Add brackets to optional option arguments: '--all=FILE?' -> '--all[=FILE]'
|
26
26
|
+ Bold text output
|
27
27
|
+ Recursive format of commands
|
28
28
|
+ Rename Compiler -> Interpreter
|
data/lib/ext/array.rb
CHANGED
@@ -41,7 +41,9 @@ module Ext
|
|
41
41
|
|
42
42
|
module Wrap
|
43
43
|
refine ::Array do
|
44
|
-
# Concatenate
|
44
|
+
# Concatenate array of words into lines that are at most +width+
|
45
|
+
# characters wide. +curr+ is the initial number of characters already
|
46
|
+
# used on the first line
|
45
47
|
def wrap(width, curr = 0)
|
46
48
|
lines = [[]]
|
47
49
|
curr -= 1 # Simplifies conditions below
|
data/lib/shellopts/analyzer.rb
CHANGED
@@ -20,10 +20,6 @@ module ShellOpts
|
|
20
20
|
end
|
21
21
|
|
22
22
|
class Command
|
23
|
-
def set_supercommand
|
24
|
-
commands.each { |child| child.instance_variable_set(:@supercommand, self) }
|
25
|
-
end
|
26
|
-
|
27
23
|
def collect_options
|
28
24
|
@options = option_groups.map(&:options).flatten
|
29
25
|
end
|
@@ -54,9 +50,9 @@ module ShellOpts
|
|
54
50
|
}
|
55
51
|
end
|
56
52
|
|
53
|
+
# TODO Check for dash-collision
|
57
54
|
def compute_command_hashes
|
58
55
|
commands.each { |command|
|
59
|
-
# TODO Check for dash-collision
|
60
56
|
!@commands_hash.key?(command.name) or
|
61
57
|
analyzer_error command.token, "Duplicate command name: #{command.name}"
|
62
58
|
@commands_hash[command.name] = command
|
@@ -76,41 +72,74 @@ module ShellOpts
|
|
76
72
|
@grammar = grammar
|
77
73
|
end
|
78
74
|
|
79
|
-
|
80
|
-
|
75
|
+
def create_implicit_commands(cmd)
|
76
|
+
path = cmd.path[0..-2]
|
77
|
+
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# Link up commands with supercommands. This is only done for commands that
|
82
|
+
# are nested within a different command than it belongs to. The
|
83
|
+
# parent/child relationship is not changed Example:
|
84
|
+
#
|
85
|
+
# cmd!
|
86
|
+
# cmd.subcmd!
|
87
|
+
#
|
88
|
+
# Here subcmd is added to cmd's list of commands. It keeps its position in
|
89
|
+
# the program's parent/child relationship so that documentation will print the
|
90
|
+
# commands in the given order and with the given indentation level
|
91
|
+
#
|
92
|
+
def link_commands
|
81
93
|
# We can't use Command#[] at this point so we collect the commands here
|
82
94
|
h = {}
|
83
95
|
@grammar.traverse(Grammar::Command) { |command|
|
84
96
|
h[command.path] = command
|
97
|
+
# TODO: Pick up parent-less commands
|
98
|
+
}
|
99
|
+
|
100
|
+
# Command to link
|
101
|
+
link = []
|
102
|
+
|
103
|
+
# Create implicit commands
|
104
|
+
h.sort { |l,r| l.size <=> r.size }.each { |path, command|
|
105
|
+
path = path[0..-2]
|
106
|
+
while !h.key?(path)
|
107
|
+
cmd = Grammar::Command.new(nil, command.token)
|
108
|
+
cmd.set_name(path.last.to_s.sub(/!/, ""), path.dup)
|
109
|
+
link << cmd
|
110
|
+
h[cmd.path] = cmd
|
111
|
+
path.pop
|
112
|
+
end
|
85
113
|
}
|
86
114
|
|
87
|
-
# Find commands to
|
115
|
+
# Find commands to link
|
88
116
|
#
|
89
|
-
# Commands are
|
90
|
-
# not defined when the data structure changes beneath it
|
91
|
-
|
117
|
+
# Commands are linked in two steps because the behaviour of #traverse is
|
118
|
+
# not defined when the data structure changes beneath it. (FIXME: Does it
|
119
|
+
# change when we don't touch the parent/child relationship?)
|
92
120
|
@grammar.traverse(Grammar::Command) { |command|
|
93
121
|
if command.path.size > 1 && command.parent && command.parent.path != command.path[0..-2]
|
94
|
-
|
122
|
+
# if command.path.size > 1 && command.parent.path != command.path[0..-2]
|
123
|
+
link << command
|
95
124
|
else
|
96
125
|
command.instance_variable_set(:@command, command.parent)
|
97
126
|
end
|
98
127
|
}
|
99
128
|
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
|
129
|
+
# Link commands but do not change parent/child relationship
|
130
|
+
link.each { |command|
|
131
|
+
path = command.path[0..-2]
|
132
|
+
path.pop while (supercommand = h[path]).nil?
|
133
|
+
command.parent.commands.delete(command) if command.parent
|
104
134
|
supercommand.commands << command
|
105
135
|
command.instance_variable_set(:@command, supercommand)
|
106
136
|
}
|
107
137
|
end
|
108
138
|
|
109
139
|
def analyze()
|
110
|
-
|
140
|
+
link_commands
|
111
141
|
|
112
142
|
@grammar.traverse(Grammar::Command) { |command|
|
113
|
-
command.set_supercommand
|
114
143
|
command.reorder_options
|
115
144
|
command.collect_options
|
116
145
|
command.compute_option_hashes
|
data/lib/shellopts/formatter.rb
CHANGED
@@ -1,12 +1,5 @@
|
|
1
1
|
require 'terminfo'
|
2
2
|
|
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
|
9
|
-
|
10
3
|
module ShellOpts
|
11
4
|
module Grammar
|
12
5
|
class Node
|
@@ -81,7 +74,15 @@ module ShellOpts
|
|
81
74
|
end
|
82
75
|
|
83
76
|
def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
|
84
|
-
|
77
|
+
# Use one-line mode if all options are declared on one line
|
78
|
+
if options.all? { |option| option.token.lineno == token.lineno }
|
79
|
+
puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
|
80
|
+
puts_options = false
|
81
|
+
else
|
82
|
+
puts Ansi.bold([prefix, render(:abbr, Formatter.rest)].flatten.compact.join(" "))
|
83
|
+
puts_options = true
|
84
|
+
end
|
85
|
+
|
85
86
|
indent {
|
86
87
|
if brief
|
87
88
|
puts self.brief.words.wrap(Formatter.rest)
|
@@ -93,7 +94,10 @@ module ShellOpts
|
|
93
94
|
|
94
95
|
if child.is_a?(Command)
|
95
96
|
child.puts_descr(prefix, name: :path)
|
96
|
-
|
97
|
+
elsif child.is_a?(OptionGroup)
|
98
|
+
child.puts_descr if puts_options
|
99
|
+
newline = false
|
100
|
+
else
|
97
101
|
child.puts_descr
|
98
102
|
end
|
99
103
|
}
|
@@ -112,37 +116,34 @@ module ShellOpts
|
|
112
116
|
|
113
117
|
section = {
|
114
118
|
Paragraph => "DESCRIPTION",
|
115
|
-
OptionGroup => "
|
116
|
-
Command => "
|
119
|
+
OptionGroup => "OPTION",
|
120
|
+
Command => "COMMAND"
|
117
121
|
}
|
118
122
|
|
123
|
+
seen_sections = {}
|
119
124
|
newline = false # True if a newline should be printed before child
|
120
125
|
indent {
|
121
126
|
children.each { |child|
|
122
|
-
|
123
|
-
#
|
124
|
-
|
125
|
-
indent(-1).puts Ansi.bold child.name
|
126
|
-
section.delete_if { |_,v| v == child.name }
|
127
|
+
klass = child.is_a?(Section) ? section.key(child.name) : child.class
|
128
|
+
if s = section[klass] # Implicit section
|
129
|
+
section.delete(klass)
|
127
130
|
section.delete(Paragraph)
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
131
|
+
if klass <= OptionGroup
|
132
|
+
s += "S" if options.size > 1
|
133
|
+
elsif klass <= Command
|
134
|
+
s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
|
135
|
+
end
|
136
|
+
puts
|
133
137
|
indent(-1).puts Ansi.bold s
|
134
|
-
section.delete(child.class)
|
135
|
-
section.delete(Paragraph)
|
136
138
|
newline = false
|
137
|
-
|
138
|
-
#
|
139
|
+
next if child.is_a?(Section)
|
140
|
+
else # Any other node adds a newline
|
139
141
|
puts if newline
|
140
142
|
newline = true
|
141
143
|
end
|
142
144
|
|
143
145
|
if child.is_a?(Command)
|
144
|
-
|
145
|
-
prefix = child.supercommand == self ? nil : child.supercommand&.name
|
146
|
+
prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
|
146
147
|
child.puts_descr(prefix, brief: false, name: :path)
|
147
148
|
newline = true
|
148
149
|
else
|
@@ -153,9 +154,10 @@ module ShellOpts
|
|
153
154
|
|
154
155
|
# Also emit commands not declared in nested scope
|
155
156
|
(commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
|
157
|
+
next if cmd.parent.nil? # Skip implicit commands
|
156
158
|
puts if newline
|
157
159
|
newline = true
|
158
|
-
prefix = cmd.
|
160
|
+
prefix = cmd.command == self ? nil : cmd.command&.name
|
159
161
|
cmd.puts_descr(prefix, brief: false, name: path)
|
160
162
|
}
|
161
163
|
}
|
@@ -207,7 +209,10 @@ module ShellOpts
|
|
207
209
|
BRIEF_COL1_MAX_WIDTH = 40
|
208
210
|
|
209
211
|
# Minimum width of second column in brief option and command lists
|
210
|
-
|
212
|
+
BRIEF_COL2_MIN_WIDTH = 30
|
213
|
+
|
214
|
+
# Maximum width of second column in brief option and command lists
|
215
|
+
BRIEF_COL2_MAX_WIDTH = 70
|
211
216
|
|
212
217
|
# Indent to use in help output
|
213
218
|
HELP_INDENT = 4
|
@@ -217,29 +222,21 @@ module ShellOpts
|
|
217
222
|
|
218
223
|
# Usage string in error messages
|
219
224
|
def self.usage(subject)
|
220
|
-
|
221
|
-
@command_prefix =
|
225
|
+
command = Grammar::Command.command(subject)
|
226
|
+
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
222
227
|
setup_indent(1) {
|
223
228
|
print lead = "#{USAGE_STRING}: "
|
224
|
-
indent(lead.size, ' ', bol: false) {
|
229
|
+
indent(lead.size, ' ', bol: false) { command.puts_usage }
|
225
230
|
}
|
226
231
|
end
|
227
232
|
|
228
|
-
# # TODO
|
229
|
-
# def self.usage=(usage_lambda)
|
230
|
-
# end
|
231
|
-
|
232
233
|
# When the user gives a -h option
|
233
|
-
def self.brief(
|
234
|
-
command = Grammar::Command.command(
|
234
|
+
def self.brief(subject)
|
235
|
+
command = Grammar::Command.command(subject)
|
235
236
|
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
236
237
|
setup_indent(BRIEF_INDENT) { command.puts_brief }
|
237
238
|
end
|
238
239
|
|
239
|
-
# # TODO
|
240
|
-
# def self.brief=(brief_lambda)
|
241
|
-
# end
|
242
|
-
|
243
240
|
# When the user gives a --help option
|
244
241
|
def self.help(subject)
|
245
242
|
subject = Grammar::Command.command(subject)
|
@@ -253,18 +250,6 @@ module ShellOpts
|
|
253
250
|
obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
|
254
251
|
end
|
255
252
|
|
256
|
-
# # TODO
|
257
|
-
# def self.help_w_lambda(program)
|
258
|
-
# if @help_lambda
|
259
|
-
# #
|
260
|
-
# else
|
261
|
-
# program = Grammar::Command.command(program)
|
262
|
-
# setup_indent(HELP_INDENT) { program.puts_descr }
|
263
|
-
# end
|
264
|
-
# end
|
265
|
-
#
|
266
|
-
# def self.help=(help_lambda) @help_lambda end
|
267
|
-
|
268
253
|
def self.puts_columns(widths, fields)
|
269
254
|
l = []
|
270
255
|
first_width, second_width = *widths
|
@@ -275,40 +260,47 @@ module ShellOpts
|
|
275
260
|
puts first
|
276
261
|
indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
|
277
262
|
elsif second
|
278
|
-
|
279
|
-
|
263
|
+
indent_size = first_width + BRIEF_COL_SEP
|
264
|
+
printf "%-#{indent_size}s", first
|
265
|
+
indent(indent_size, ' ', bol: false) { puts second.wrap(second_width) }
|
280
266
|
else
|
281
267
|
puts first
|
282
268
|
end
|
283
269
|
end
|
284
270
|
end
|
285
271
|
|
272
|
+
# Returns a tuple of [first-column-width, second-column-width]. +width+ is
|
273
|
+
# the maximum width of the colunms and the BRIEF_COL_SEP separator.
|
274
|
+
# +fields+ is an array of [subject-string, descr-text] tuples where the
|
275
|
+
# descr is an array of words
|
286
276
|
def self.compute_columns(width, fields)
|
287
|
-
first_max =
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
first_width = first_max
|
298
|
-
second_width = width - first_width - BRIEF_COL_SEP
|
277
|
+
first_max =
|
278
|
+
fields.map { |first, _| first.size }.select { |size| size <= BRIEF_COL1_MAX_WIDTH }.max ||
|
279
|
+
BRIEF_COL1_MIN_WIDTH
|
280
|
+
second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1 : 0 }.max
|
281
|
+
first_width = [[first_max, BRIEF_COL1_MIN_WIDTH].max, BRIEF_COL1_MAX_WIDTH].min
|
282
|
+
rest = width - first_width - BRIEF_COL_SEP
|
283
|
+
second_min = [BRIEF_COL2_MIN_WIDTH, second_max].min
|
284
|
+
if rest < second_min
|
285
|
+
first_width = [first_max, width - second_min - BRIEF_COL_SEP].max
|
286
|
+
second_width = [width - first_width - BRIEF_COL_SEP, BRIEF_COL2_MIN_WIDTH].max
|
299
287
|
else
|
300
|
-
|
301
|
-
second_width = BRIEF_COL2_MAX_WIDTH
|
288
|
+
second_width = [[rest, BRIEF_COL2_MIN_WIDTH].max, BRIEF_COL2_MAX_WIDTH].min
|
302
289
|
end
|
303
|
-
|
304
290
|
[first_width, second_width]
|
305
291
|
end
|
306
292
|
|
307
293
|
def self.width()
|
308
294
|
@width ||= TermInfo.screen_width - MARGIN_RIGHT
|
295
|
+
@width
|
296
|
+
end
|
297
|
+
|
298
|
+
# Used in rspec
|
299
|
+
def self.width=(width)
|
300
|
+
@width = width
|
309
301
|
end
|
310
302
|
|
311
|
-
def self.rest() width - $stdout.
|
303
|
+
def self.rest() width - $stdout.tab end
|
312
304
|
|
313
305
|
private
|
314
306
|
# TODO Get rid of?
|
data/lib/shellopts/grammar.rb
CHANGED
@@ -45,16 +45,18 @@ module ShellOpts
|
|
45
45
|
end
|
46
46
|
|
47
47
|
class IdrNode < Node
|
48
|
-
# Command of this object
|
49
|
-
#
|
50
|
-
# Initialized by the analyzer
|
48
|
+
# Command of this object (nil for the top-level Program object). This is
|
49
|
+
# different from #parent when a subcommand is nested textually on a
|
50
|
+
# higher level than its supercommand. Initialized by the analyzer
|
51
51
|
attr_reader :command
|
52
52
|
|
53
53
|
# Unique identifier of node (String) within the context of a program. nil
|
54
|
-
# for the Program object. It is the
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
54
|
+
# for the Program object. It is the dot-joined elements of path with
|
55
|
+
# internal exclamation marks removed (eg. "cmd.opt" or "cmd.cmd!").
|
56
|
+
# Initialize by the analyzer
|
57
|
+
def uid()
|
58
|
+
@uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
|
59
|
+
end
|
58
60
|
|
59
61
|
# Path from Program object and down to this node. Array of identifiers.
|
60
62
|
# Empty for the Program object. Initialized by the parser
|
@@ -84,6 +86,13 @@ module ShellOpts
|
|
84
86
|
# #ident is a reserved word. Initialized by the parser
|
85
87
|
attr_reader :attr
|
86
88
|
|
89
|
+
def set_name(name, path)
|
90
|
+
@name = name.to_s
|
91
|
+
@path = path
|
92
|
+
@ident = @path.last || :!
|
93
|
+
@attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(@ident.to_s) ? nil : @ident
|
94
|
+
end
|
95
|
+
|
87
96
|
protected
|
88
97
|
def lookup(path)
|
89
98
|
path.empty? or raise ArgumentError, "Argument should be empty"
|
@@ -189,10 +198,6 @@ module ShellOpts
|
|
189
198
|
# methods are initialized by the analyzer
|
190
199
|
#
|
191
200
|
class Command < IdrNode
|
192
|
-
# Supercommand or nil if this is the top-level Program object.
|
193
|
-
# Initialized by the analyzer
|
194
|
-
attr_reader :supercommand
|
195
|
-
|
196
201
|
# Brief description of command
|
197
202
|
attr_accessor :brief
|
198
203
|
|
data/lib/shellopts/parser.rb
CHANGED
@@ -16,9 +16,9 @@ module ShellOpts
|
|
16
16
|
class IdrNode
|
17
17
|
# Assumes that @name and @path has been defined
|
18
18
|
def parse
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
# @ident = @path.last || :!
|
20
|
+
# @attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(ident.to_s) ? nil : ident
|
21
|
+
# @uid = parent && @path.join(".").sub(/!\./, ".") # uid is nil for the Program object
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -56,8 +56,9 @@ module ShellOpts
|
|
56
56
|
@long_names = names.map { |name| "--#{name}" }
|
57
57
|
@long_idents = names.map { |name| name.tr("-", "_").to_sym }
|
58
58
|
|
59
|
-
|
60
|
-
|
59
|
+
set_name(
|
60
|
+
@long_names.first || @short_names.first,
|
61
|
+
command.path + [@long_idents.first || @short_idents.first])
|
61
62
|
|
62
63
|
@argument = !arg.nil?
|
63
64
|
|
@@ -108,11 +109,11 @@ module ShellOpts
|
|
108
109
|
def parse
|
109
110
|
if parent
|
110
111
|
path_names = token.source.sub("!", "").split(".")
|
111
|
-
|
112
|
-
|
112
|
+
set_name(
|
113
|
+
path_names.last,
|
114
|
+
path_names.map { |cmd| "#{cmd}!".to_sym })
|
113
115
|
else
|
114
|
-
|
115
|
-
@name = token.source
|
116
|
+
set_name(token.source, [])
|
116
117
|
end
|
117
118
|
super
|
118
119
|
end
|
@@ -123,13 +124,15 @@ module ShellOpts
|
|
123
124
|
super(nil, token)
|
124
125
|
end
|
125
126
|
|
126
|
-
def
|
127
|
+
def add_version_option
|
127
128
|
option_token = Token.new(:option, 1, 1, "--version")
|
128
129
|
brief_token = Token.new(:brief, 1, 1, "Write version number and exit")
|
129
130
|
group = OptionGroup.new(self, option_token)
|
130
131
|
option = Option.parse(group, option_token)
|
131
132
|
brief = Brief.parse(group, brief_token)
|
133
|
+
end
|
132
134
|
|
135
|
+
def add_help_options
|
133
136
|
option_token = Token.new(:option, 1, 1, "-h,help")
|
134
137
|
brief_token = Token.new(:brief, 1, 1, "Write help text and exit")
|
135
138
|
paragraph_token = Token.new(:text, 1, 1,
|
@@ -163,14 +166,6 @@ module ShellOpts
|
|
163
166
|
@nodes = {}
|
164
167
|
end
|
165
168
|
|
166
|
-
# def add_stdopts
|
167
|
-
# version_token = Token.new(:option, 1, 1, "--version")
|
168
|
-
# version_brief = Token.new(:brief, 1, 1, "Gryf gryf")
|
169
|
-
# group = Grammar::OptionGroup.new(@program, version_token)
|
170
|
-
# option = Grammar::Option.parse(group, version_token)
|
171
|
-
# brief = Grammr::Brief.parse(option, version_brief)
|
172
|
-
# end
|
173
|
-
|
174
169
|
def parse()
|
175
170
|
@program = Grammar::Program.parse(@tokens.shift)
|
176
171
|
oneline = @tokens.first.lineno == @tokens.last.lineno
|
data/lib/shellopts/program.rb
CHANGED
@@ -49,10 +49,10 @@ module ShellOpts
|
|
49
49
|
singleton_method_removed singleton_method_undefined
|
50
50
|
)
|
51
51
|
|
52
|
-
# These methods can be overridden by an option (the value is not used -
|
52
|
+
# These methods can be overridden by an option or a command (the value is not used -
|
53
53
|
# this is just for informational purposes)
|
54
|
-
|
55
|
-
subcommand
|
54
|
+
OVERRIDEABLE_METHOD_NAMES = %w(
|
55
|
+
subcommand subcommand! supercommand!
|
56
56
|
)
|
57
57
|
|
58
58
|
# Redefine ::new to call #__initialize__
|
@@ -107,6 +107,7 @@ module ShellOpts
|
|
107
107
|
#
|
108
108
|
# Note: Can be overridden by option, in that case use #__subcommand__ or
|
109
109
|
# ShellOpts.subcommand(object) instead
|
110
|
+
#
|
110
111
|
def subcommand() __subcommand__ end
|
111
112
|
|
112
113
|
# The subcommand object or nil if not present. Per-subcommand methods
|
@@ -120,7 +121,12 @@ module ShellOpts
|
|
120
121
|
def subcommand!() __subcommand__! end
|
121
122
|
|
122
123
|
# The parent command or nil. Initialized by #add_command
|
123
|
-
|
124
|
+
#
|
125
|
+
# Note: Can be overridden by a subcommand declaration (but not an
|
126
|
+
# option), in that case use #__supercommand__! or
|
127
|
+
# ShellOpts.supercommand!(object) instead
|
128
|
+
#
|
129
|
+
def supercommand!() __supercommand__ end
|
124
130
|
|
125
131
|
# UID of command/program
|
126
132
|
def __uid__() @__grammar__.uid end
|
@@ -149,6 +155,9 @@ module ShellOpts
|
|
149
155
|
# Map from identifier to option object or to a list of option objects if
|
150
156
|
# the option is repeatable
|
151
157
|
attr_reader :__option_hash__
|
158
|
+
|
159
|
+
# The parent command or nil. Initialized by #add_command
|
160
|
+
attr_accessor :__supercommand__
|
152
161
|
|
153
162
|
# The subcommand identifier (a Symbol incl. the exclamation mark) or nil
|
154
163
|
# if not present. Use #subcommand!, or the dynamically generated
|
data/lib/shellopts/renderer.rb
CHANGED
@@ -38,13 +38,19 @@ module ShellOpts
|
|
38
38
|
#
|
39
39
|
def render(format)
|
40
40
|
constrain format, :enum, :long, :short
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
s =
|
42
|
+
case format
|
43
|
+
when :enum; names.join(", ")
|
44
|
+
when :long; name
|
45
|
+
when :short; short_names.first || name
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Illegal format: #{format.inspect}"
|
48
|
+
end
|
49
|
+
if argument?
|
50
|
+
s + (optional? ? "[=#{argument_name}]" : "=#{argument_name}")
|
45
51
|
else
|
46
|
-
|
47
|
-
end
|
52
|
+
s
|
53
|
+
end
|
48
54
|
end
|
49
55
|
end
|
50
56
|
|
@@ -75,17 +81,19 @@ module ShellOpts
|
|
75
81
|
COMMANDS_ABBR = "[COMMANDS]"
|
76
82
|
DESCRS_ABBR = "ARGS..."
|
77
83
|
|
78
|
-
# Format can be one of :single, :enum, or :multi. :
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
84
|
+
# Format can be one of :abbr, :single, :enum, or :multi. :abbr
|
85
|
+
# lists the command on one line with options abbreviated. :single force
|
86
|
+
# one-line output and compacts options and commands if needed. :enum
|
87
|
+
# outputs a :single line for each argument specification/description,
|
88
|
+
# :multi tries one-line output but wrap options if needed. Multiple
|
89
|
+
# argument specifications/descriptions are always compacted
|
83
90
|
#
|
84
91
|
def render(format, width, root: false, **opts)
|
85
92
|
case format
|
93
|
+
when :abbr; render_abbr
|
86
94
|
when :single; render_single(width, **opts)
|
87
95
|
when :enum; render_enum(width, **opts)
|
88
|
-
when :multi;
|
96
|
+
when :multi; render_multi(width, **opts)
|
89
97
|
else
|
90
98
|
raise ArgumentError, "Illegal format: #{format.inspect}"
|
91
99
|
end
|
@@ -96,6 +104,21 @@ module ShellOpts
|
|
96
104
|
end
|
97
105
|
|
98
106
|
protected
|
107
|
+
# TODO: Refactor and implement recursive detection of any argument
|
108
|
+
def get_args(args: nil)
|
109
|
+
case descrs.size
|
110
|
+
when 0; []
|
111
|
+
when 1; [descrs.first.text]
|
112
|
+
else [DESCRS_ABBR]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Force one line and compact options to "[OPTIONS]"
|
117
|
+
def render_abbr
|
118
|
+
args = get_args
|
119
|
+
([name] + [options.empty? ? nil : "[OPTIONS]"] + args).compact.join(" ")
|
120
|
+
end
|
121
|
+
|
99
122
|
# Force one line. Compact options, commands, arguments if needed
|
100
123
|
def render_single(width, args: nil)
|
101
124
|
long_options = options.map { |option| option.render(:long) }
|
@@ -104,13 +127,7 @@ module ShellOpts
|
|
104
127
|
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
105
128
|
compact_commands = commands.empty? ? [] : [COMMANDS_ABBR]
|
106
129
|
|
107
|
-
|
108
|
-
args ||=
|
109
|
-
case descrs.size
|
110
|
-
when 0; args = []
|
111
|
-
when 1; [descrs.first.text]
|
112
|
-
else [DESCRS_ABBR]
|
113
|
-
end
|
130
|
+
args ||= get_args
|
114
131
|
|
115
132
|
begin # to be able to use 'break' below
|
116
133
|
words = [name] + long_options + short_commands + args
|
@@ -149,36 +166,8 @@ module ShellOpts
|
|
149
166
|
short_options = options.map { |option| option.render(:short) }
|
150
167
|
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
151
168
|
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
169
|
|
175
|
-
|
176
|
-
args ||=
|
177
|
-
case descrs.size
|
178
|
-
when 0; args = []
|
179
|
-
when 1; [descrs.first.text]
|
180
|
-
else [DESCRS_ABBR]
|
181
|
-
end
|
170
|
+
args ||= get_args
|
182
171
|
|
183
172
|
# On one line
|
184
173
|
words = [name] + long_options + short_commands + args
|
data/lib/shellopts/version.rb
CHANGED
data/lib/shellopts.rb
CHANGED
@@ -1,12 +1,6 @@
|
|
1
1
|
|
2
|
-
$quiet = nil
|
3
|
-
$verb = nil
|
4
|
-
$debug = nil
|
5
|
-
$shellopts = nil
|
6
|
-
|
7
2
|
require 'indented_io'
|
8
3
|
|
9
|
-
#$LOAD_PATH.unshift "../constrain/lib"
|
10
4
|
require 'constrain'
|
11
5
|
include Constrain
|
12
6
|
|
@@ -98,14 +92,22 @@ module ShellOpts
|
|
98
92
|
# Array of remaining arguments. Initialized by #interpret
|
99
93
|
attr_reader :args
|
100
94
|
|
101
|
-
#
|
102
|
-
|
103
|
-
attr_accessor :msgopts
|
95
|
+
# Automatically add a -h and a --help option if true
|
96
|
+
attr_reader :help
|
104
97
|
|
105
|
-
# Version of client program.
|
106
|
-
|
98
|
+
# Version of client program. If not nil a --version option is added to the program
|
99
|
+
def version
|
100
|
+
return @version if @version
|
101
|
+
exe = caller.find { |line| line =~ /`<top \(required\)>'$/ }&.sub(/:.*/, "")
|
102
|
+
file = Dir.glob(File.dirname(exe) + "/../lib/*/version.rb").first
|
103
|
+
@version = IO.read(file).sub(/^.*VERSION\s*=\s*"(.*?)".*$/m, '\1') or
|
104
|
+
raise ArgumentError, "ShellOpts needs an explicit version"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Add message options (TODO)
|
108
|
+
attr_accessor :msgopts
|
107
109
|
|
108
|
-
#
|
110
|
+
# Floating options
|
109
111
|
attr_accessor :float
|
110
112
|
|
111
113
|
# True if ShellOpts lets exceptions through instead of writing an error
|
@@ -117,11 +119,14 @@ module ShellOpts
|
|
117
119
|
|
118
120
|
# Debug: Internal variables made public
|
119
121
|
attr_reader :tokens
|
120
|
-
alias_method :ast, :grammar
|
122
|
+
alias_method :ast, :grammar
|
121
123
|
|
122
|
-
def initialize(name: nil,
|
124
|
+
def initialize(name: nil, help: true, version: true, msgopts: false, float: true, exception: false)
|
123
125
|
@name = name || File.basename($PROGRAM_NAME)
|
124
|
-
@
|
126
|
+
@help = help
|
127
|
+
@use_version = version ? true : false
|
128
|
+
@version = @use_version && @version != true ? @version : nil
|
129
|
+
@msgopts, @float, @exception = msgopts, float, exception
|
125
130
|
end
|
126
131
|
|
127
132
|
# Compile source and return grammar object. Also sets #spec and #grammar.
|
@@ -133,7 +138,8 @@ module ShellOpts
|
|
133
138
|
@file = find_caller_file
|
134
139
|
@tokens = Lexer.lex(name, @spec, @oneline)
|
135
140
|
ast = Parser.parse(tokens)
|
136
|
-
ast.
|
141
|
+
ast.add_version_option if @use_version
|
142
|
+
ast.add_help_options if @help
|
137
143
|
@grammar = Analyzer.analyze(ast)
|
138
144
|
}
|
139
145
|
self
|
@@ -146,19 +152,16 @@ module ShellOpts
|
|
146
152
|
handle_exceptions {
|
147
153
|
@argv = argv.dup
|
148
154
|
@program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
|
149
|
-
if
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
else
|
158
|
-
ShellOpts.help
|
159
|
-
end
|
160
|
-
exit
|
155
|
+
if @program.version?
|
156
|
+
puts version
|
157
|
+
exit
|
158
|
+
elsif @program.help?
|
159
|
+
if @program[:help].name == "-h"
|
160
|
+
ShellOpts.brief
|
161
|
+
else
|
162
|
+
ShellOpts.help
|
161
163
|
end
|
164
|
+
exit
|
162
165
|
end
|
163
166
|
}
|
164
167
|
self
|
@@ -364,132 +367,3 @@ module ShellOpts
|
|
364
367
|
end
|
365
368
|
end
|
366
369
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
__END__
|
375
|
-
|
376
|
-
require "shellopts/version"
|
377
|
-
|
378
|
-
require "ext/algorithm.rb"
|
379
|
-
require "ext/ruby_env.rb"
|
380
|
-
|
381
|
-
require "shellopts/constants.rb"
|
382
|
-
require "shellopts/exceptions.rb"
|
383
|
-
|
384
|
-
require "shellopts/grammar/analyzer.rb"
|
385
|
-
require "shellopts/grammar/lexer.rb"
|
386
|
-
require "shellopts/grammar/parser.rb"
|
387
|
-
require "shellopts/grammar/command.rb"
|
388
|
-
require "shellopts/grammar/option.rb"
|
389
|
-
|
390
|
-
require "shellopts/ast/parser.rb"
|
391
|
-
require "shellopts/ast/command.rb"
|
392
|
-
require "shellopts/ast/option.rb"
|
393
|
-
|
394
|
-
require "shellopts/args.rb"
|
395
|
-
require "shellopts/formatter.rb"
|
396
|
-
|
397
|
-
if RUBY_ENV == "development"
|
398
|
-
require "shellopts/grammar/dump.rb"
|
399
|
-
require "shellopts/ast/dump.rb"
|
400
|
-
end
|
401
|
-
|
402
|
-
$verb = nil
|
403
|
-
$quiet = nil
|
404
|
-
$shellopts = nil
|
405
|
-
|
406
|
-
module ShellOpts
|
407
|
-
class ShellOpts
|
408
|
-
attr_reader :name # Name of program. Defaults to the name of the executable
|
409
|
-
attr_reader :spec
|
410
|
-
attr_reader :argv
|
411
|
-
|
412
|
-
attr_reader :grammar
|
413
|
-
attr_reader :program
|
414
|
-
attr_reader :arguments
|
415
|
-
|
416
|
-
def initialize(spec, argv, name: nil, exception: false)
|
417
|
-
@name = name || File.basename($PROGRAM_NAME)
|
418
|
-
@spec, @argv = spec, argv.dup
|
419
|
-
exprs = Grammar::Lexer.lex(@spec)
|
420
|
-
commands = Grammar::Parser.parse(@name, exprs)
|
421
|
-
@grammar = Grammar::Analyzer.analyze(commands)
|
422
|
-
|
423
|
-
begin
|
424
|
-
@program, @arguments = Ast::Parser.parse(@grammar, @argv)
|
425
|
-
rescue Error => ex
|
426
|
-
raise if exception
|
427
|
-
error(ex.subject, ex.message)
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
def error(subject = nil, message)
|
432
|
-
$stderr.puts "#{name}: #{message}"
|
433
|
-
usage(subject, device: $stderr)
|
434
|
-
exit 1
|
435
|
-
end
|
436
|
-
|
437
|
-
def fail(message)
|
438
|
-
$stderr.puts "#{name}: #{message}"
|
439
|
-
exit 1
|
440
|
-
end
|
441
|
-
|
442
|
-
def usage(subject = nil, device: $stdout, levels: 1, margin: "")
|
443
|
-
subject = find_subject(subject)
|
444
|
-
device.puts Formatter.usage_string(subject, levels: levels, margin: margin)
|
445
|
-
end
|
446
|
-
|
447
|
-
def help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
|
448
|
-
subject = find_subject(subject)
|
449
|
-
device.puts Formatter.help_string(subject, levels: levels, margin: margin, tab: tab)
|
450
|
-
end
|
451
|
-
|
452
|
-
private
|
453
|
-
def lookup(name)
|
454
|
-
a = name.split(".")
|
455
|
-
cmd = grammar
|
456
|
-
while element = a.shift
|
457
|
-
cmd = cmd.commands[element]
|
458
|
-
end
|
459
|
-
cmd
|
460
|
-
end
|
461
|
-
|
462
|
-
def find_subject(obj)
|
463
|
-
case obj
|
464
|
-
when String; lookup(obj)
|
465
|
-
when Ast::Command; Command.grammar(obj)
|
466
|
-
when Grammar::Command; obj
|
467
|
-
when NilClass; grammar
|
468
|
-
else
|
469
|
-
raise Internal, "Illegal object: #{obj.class}"
|
470
|
-
end
|
471
|
-
end
|
472
|
-
end
|
473
|
-
|
474
|
-
def self.process(spec, argv, name: nil, exception: false)
|
475
|
-
$shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
|
476
|
-
[$shellopts.program, $shellopts.arguments]
|
477
|
-
end
|
478
|
-
|
479
|
-
def self.error(subject = nil, message)
|
480
|
-
$shellopts.error(subject, message)
|
481
|
-
end
|
482
|
-
|
483
|
-
def self.fail(message)
|
484
|
-
$shellopts.fail(message)
|
485
|
-
end
|
486
|
-
|
487
|
-
def self.help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
|
488
|
-
$shellopts.help(subject, device: device, levels: levels, margin: margin, tab: tab)
|
489
|
-
end
|
490
|
-
|
491
|
-
def self.usage(subject = nil, device: $stdout, levels: 1, margin: "")
|
492
|
-
$shellopts.usage(subject, device: device, levels: levels, margin: margin)
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
data/main
CHANGED
@@ -7,12 +7,15 @@ require 'shellopts'
|
|
7
7
|
|
8
8
|
include ShellOpts
|
9
9
|
|
10
|
+
p ShellOpts::ShellOpts.default_version
|
11
|
+
exit
|
12
|
+
|
10
13
|
VERSION = "1.2.3"
|
11
14
|
|
12
15
|
SPEC = %(
|
13
16
|
-a @ An option
|
14
17
|
)
|
15
|
-
opts, args = ShellOpts
|
18
|
+
opts, args = ShellOpts.process(SPEC, ARGV, version: VERSION)
|
16
19
|
#ShellOpts::ShellOpts.help
|
17
20
|
|
18
21
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shellopts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: forward_to
|