shellopts 2.0.8 → 2.0.11
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/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
|