shellopts 2.0.9 → 2.0.12
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 +52 -57
- data/lib/shellopts/grammar.rb +16 -11
- data/lib/shellopts/parser.rb +13 -10
- 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 +53 -170
- 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: b2f378c7e7c04297221bfae81065ef06699e93b2dc4fda892b210ef4702c5836
|
4
|
+
data.tar.gz: f5d83b09b371aebd7cc07e73905edffb2f9f834ba5e382894277310e984e7ecc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6255a64a99ceefe0dc0a7a3195976d42b2db6b3a28d779cbeefbc9f36380e46a762939e2062ff9f32f4f78bd5bff8122606b5102d9d8473e86c0a742cd7db479
|
7
|
+
data.tar.gz: b54f74a5d8fd251d1492667bd8e1340c1f17a3e96936f423efd61e82fe665294936c1edd1ac9ac4ab9df118450ccefafcda117f84ef4e0726d78159196e075cf
|
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
|
}
|
@@ -125,9 +129,9 @@ module ShellOpts
|
|
125
129
|
section.delete(klass)
|
126
130
|
section.delete(Paragraph)
|
127
131
|
if klass <= OptionGroup
|
128
|
-
s
|
132
|
+
s += "S" if options.size > 1
|
129
133
|
elsif klass <= Command
|
130
|
-
s
|
134
|
+
s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
|
131
135
|
end
|
132
136
|
puts
|
133
137
|
indent(-1).puts Ansi.bold s
|
@@ -139,7 +143,7 @@ module ShellOpts
|
|
139
143
|
end
|
140
144
|
|
141
145
|
if child.is_a?(Command)
|
142
|
-
prefix = child.
|
146
|
+
prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
|
143
147
|
child.puts_descr(prefix, brief: false, name: :path)
|
144
148
|
newline = true
|
145
149
|
else
|
@@ -150,9 +154,10 @@ module ShellOpts
|
|
150
154
|
|
151
155
|
# Also emit commands not declared in nested scope
|
152
156
|
(commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
|
157
|
+
next if cmd.parent.nil? # Skip implicit commands
|
153
158
|
puts if newline
|
154
159
|
newline = true
|
155
|
-
prefix = cmd.
|
160
|
+
prefix = cmd.command == self ? nil : cmd.command&.name
|
156
161
|
cmd.puts_descr(prefix, brief: false, name: path)
|
157
162
|
}
|
158
163
|
}
|
@@ -204,7 +209,10 @@ module ShellOpts
|
|
204
209
|
BRIEF_COL1_MAX_WIDTH = 40
|
205
210
|
|
206
211
|
# Minimum width of second column in brief option and command lists
|
207
|
-
|
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
|
208
216
|
|
209
217
|
# Indent to use in help output
|
210
218
|
HELP_INDENT = 4
|
@@ -214,29 +222,21 @@ module ShellOpts
|
|
214
222
|
|
215
223
|
# Usage string in error messages
|
216
224
|
def self.usage(subject)
|
217
|
-
|
218
|
-
@command_prefix =
|
225
|
+
command = Grammar::Command.command(subject)
|
226
|
+
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
219
227
|
setup_indent(1) {
|
220
228
|
print lead = "#{USAGE_STRING}: "
|
221
|
-
indent(lead.size, ' ', bol: false) {
|
229
|
+
indent(lead.size, ' ', bol: false) { command.puts_usage }
|
222
230
|
}
|
223
231
|
end
|
224
232
|
|
225
|
-
# # TODO
|
226
|
-
# def self.usage=(usage_lambda)
|
227
|
-
# end
|
228
|
-
|
229
233
|
# When the user gives a -h option
|
230
|
-
def self.brief(
|
231
|
-
command = Grammar::Command.command(
|
234
|
+
def self.brief(subject)
|
235
|
+
command = Grammar::Command.command(subject)
|
232
236
|
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
233
237
|
setup_indent(BRIEF_INDENT) { command.puts_brief }
|
234
238
|
end
|
235
239
|
|
236
|
-
# # TODO
|
237
|
-
# def self.brief=(brief_lambda)
|
238
|
-
# end
|
239
|
-
|
240
240
|
# When the user gives a --help option
|
241
241
|
def self.help(subject)
|
242
242
|
subject = Grammar::Command.command(subject)
|
@@ -250,18 +250,6 @@ module ShellOpts
|
|
250
250
|
obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
|
251
251
|
end
|
252
252
|
|
253
|
-
# # TODO
|
254
|
-
# def self.help_w_lambda(program)
|
255
|
-
# if @help_lambda
|
256
|
-
# #
|
257
|
-
# else
|
258
|
-
# program = Grammar::Command.command(program)
|
259
|
-
# setup_indent(HELP_INDENT) { program.puts_descr }
|
260
|
-
# end
|
261
|
-
# end
|
262
|
-
#
|
263
|
-
# def self.help=(help_lambda) @help_lambda end
|
264
|
-
|
265
253
|
def self.puts_columns(widths, fields)
|
266
254
|
l = []
|
267
255
|
first_width, second_width = *widths
|
@@ -272,40 +260,47 @@ module ShellOpts
|
|
272
260
|
puts first
|
273
261
|
indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
|
274
262
|
elsif second
|
275
|
-
|
276
|
-
|
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) }
|
277
266
|
else
|
278
267
|
puts first
|
279
268
|
end
|
280
269
|
end
|
281
270
|
end
|
282
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
|
283
276
|
def self.compute_columns(width, fields)
|
284
|
-
first_max =
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
first_width = first_max
|
295
|
-
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
|
296
287
|
else
|
297
|
-
|
298
|
-
second_width = BRIEF_COL2_MAX_WIDTH
|
288
|
+
second_width = [[rest, BRIEF_COL2_MIN_WIDTH].max, BRIEF_COL2_MAX_WIDTH].min
|
299
289
|
end
|
300
|
-
|
301
290
|
[first_width, second_width]
|
302
291
|
end
|
303
292
|
|
304
293
|
def self.width()
|
305
294
|
@width ||= TermInfo.screen_width - MARGIN_RIGHT
|
295
|
+
@width
|
296
|
+
end
|
297
|
+
|
298
|
+
# Used in rspec
|
299
|
+
def self.width=(width)
|
300
|
+
@width = width
|
306
301
|
end
|
307
302
|
|
308
|
-
def self.rest() width - $stdout.
|
303
|
+
def self.rest() width - $stdout.tab end
|
309
304
|
|
310
305
|
private
|
311
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,
|
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
|
-
|
95
|
+
# Automatically add a -h and a --help option if true
|
96
|
+
attr_reader :help
|
97
|
+
|
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
|
104
106
|
|
105
|
-
#
|
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
|
@@ -189,23 +192,23 @@ module ShellOpts
|
|
189
192
|
#
|
190
193
|
# #error is supposed to be used when the user made an error and the usage
|
191
194
|
# is written to help correcting the error
|
192
|
-
#
|
193
195
|
def error(subject = nil, message)
|
194
|
-
saved = $stdout
|
195
|
-
$stdout = $stderr
|
196
196
|
$stderr.puts "#{name}: #{message}"
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
197
|
+
saved = $stdout
|
198
|
+
begin
|
199
|
+
$stdout = $stderr
|
200
|
+
Formatter.usage(grammar)
|
201
|
+
exit 1
|
202
|
+
ensure
|
203
|
+
$stdout = saved
|
204
|
+
end
|
201
205
|
end
|
202
206
|
|
203
207
|
# Write error message to standard error and terminate program with status 1
|
204
208
|
#
|
205
|
-
# #failure
|
206
|
-
#
|
207
|
-
#
|
208
|
-
#
|
209
|
+
# #failure doesn't print the program usage because is supposed to be used
|
210
|
+
# when the user specified the correct arguments but something else went
|
211
|
+
# wrong during processing
|
209
212
|
def failure(message)
|
210
213
|
$stderr.puts "#{name}: #{message}"
|
211
214
|
exit 1
|
@@ -219,7 +222,6 @@ module ShellOpts
|
|
219
222
|
|
220
223
|
# Print help for the given subject or the full documentation if +subject+
|
221
224
|
# is nil. Clears the screen beforehand if :clear is true
|
222
|
-
#
|
223
225
|
def help(subject = nil, clear: true)
|
224
226
|
node = (subject ? @grammar[subject] : @grammar) or
|
225
227
|
raise ArgumentError, "No such command: '#{subject&.sub(".", " ")}'"
|
@@ -330,7 +332,17 @@ module ShellOpts
|
|
330
332
|
def self.instance=(instance) @instance = instance end
|
331
333
|
def self.shellopts() instance end
|
332
334
|
|
333
|
-
|
335
|
+
def self.error(subject = nil, message)
|
336
|
+
instance.error(subject, message) if instance? # Never returns
|
337
|
+
$stderr.puts "#{File.basename($PROGRAM_NAME)}: #{message}"
|
338
|
+
exit 1
|
339
|
+
end
|
340
|
+
|
341
|
+
def self.failure(message)
|
342
|
+
instance.failure(message) if instance?
|
343
|
+
$stderr.puts "#{File.basename($PROGRAM_NAME)}: #{message}"
|
344
|
+
exit 1
|
345
|
+
end
|
334
346
|
|
335
347
|
# The Include module brings the reporting methods into the namespace when
|
336
348
|
# included
|
@@ -364,132 +376,3 @@ module ShellOpts
|
|
364
376
|
end
|
365
377
|
end
|
366
378
|
|
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.12
|
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-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: forward_to
|