shellopts 2.0.0.pre.13 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.ruby-version +1 -1
- data/README.md +201 -267
- data/TODO +46 -134
- data/doc/format.rb +95 -0
- data/doc/grammar.txt +27 -0
- data/doc/syntax.rb +110 -0
- data/doc/syntax.txt +10 -0
- data/lib/ext/array.rb +58 -5
- data/lib/ext/forward_to.rb +15 -0
- data/lib/ext/lcs.rb +34 -0
- data/lib/shellopts/analyzer.rb +130 -0
- data/lib/shellopts/ansi.rb +8 -0
- data/lib/shellopts/args.rb +29 -21
- data/lib/shellopts/argument_type.rb +139 -0
- data/lib/shellopts/dump.rb +158 -0
- data/lib/shellopts/formatter.rb +325 -0
- data/lib/shellopts/grammar.rb +375 -0
- data/lib/shellopts/interpreter.rb +103 -0
- data/lib/shellopts/lexer.rb +175 -0
- data/lib/shellopts/parser.rb +269 -82
- data/lib/shellopts/program.rb +279 -0
- data/lib/shellopts/renderer.rb +227 -0
- data/lib/shellopts/stack.rb +7 -0
- data/lib/shellopts/token.rb +44 -0
- data/lib/shellopts/version.rb +2 -2
- data/lib/shellopts.rb +439 -220
- data/main +1180 -0
- data/shellopts.gemspec +9 -15
- metadata +85 -42
- data/lib/main.rb +0 -1
- data/lib/shellopts/ast/command.rb +0 -41
- data/lib/shellopts/ast/node.rb +0 -37
- data/lib/shellopts/ast/option.rb +0 -21
- data/lib/shellopts/ast/program.rb +0 -14
- data/lib/shellopts/compiler.rb +0 -128
- data/lib/shellopts/generator.rb +0 -15
- data/lib/shellopts/grammar/command.rb +0 -80
- data/lib/shellopts/grammar/node.rb +0 -33
- data/lib/shellopts/grammar/option.rb +0 -66
- data/lib/shellopts/grammar/program.rb +0 -65
- data/lib/shellopts/idr.rb +0 -236
- data/lib/shellopts/main.rb +0 -10
- data/lib/shellopts/option_struct.rb +0 -148
- data/lib/shellopts/shellopts.rb +0 -123
data/lib/shellopts/args.rb
CHANGED
@@ -3,36 +3,36 @@ module ShellOpts
|
|
3
3
|
# Specialization of Array for arguments lists. Args extends Array with a
|
4
4
|
# #extract and an #expect method to extract elements from the array. The
|
5
5
|
# methods raise a ShellOpts::UserError exception in case of errors
|
6
|
+
#
|
6
7
|
class Args < Array
|
7
|
-
def initialize(shellopts, *args)
|
8
|
-
@shellopts = shellopts
|
9
|
-
super(*args)
|
10
|
-
end
|
11
|
-
|
12
8
|
# Remove and return elements from beginning of the array
|
13
9
|
#
|
14
10
|
# If +count_or_range+ is a number, that number of elements will be
|
15
11
|
# returned. If the count is one, a simple value is returned instead of an
|
16
|
-
# array.
|
12
|
+
# array. If the count is negative, the elements will be removed from the
|
17
13
|
# end of the array. If +count_or_range+ is a range, the number of elements
|
18
14
|
# returned will be in that range. The range can't contain negative numbers
|
19
15
|
#
|
20
16
|
# #extract raise a ShellOpts::UserError exception if there's is not enough
|
21
17
|
# elements in the array to satisfy the request
|
18
|
+
#
|
22
19
|
def extract(count_or_range, message = nil)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
20
|
+
case count_or_range
|
21
|
+
when Range
|
22
|
+
range = count_or_range
|
23
|
+
range.min <= self.size or inoa(message)
|
24
|
+
n_extract = [self.size, range.max].min
|
25
|
+
n_extend = range.max > self.size ? range.max - self.size : 0
|
26
|
+
r = self.shift(n_extract) + Array.new(n_extend)
|
27
|
+
range.max <= 1 ? r.first : r
|
28
|
+
when Integer
|
29
|
+
count = count_or_range
|
30
|
+
count.abs <= self.size or inoa(message)
|
31
|
+
start = count >= 0 ? 0 : size + count
|
32
|
+
r = slice!(start, count.abs)
|
33
|
+
r.size <= 0 ? nil : (r.size == 1 ? r.first : r)
|
34
|
+
else
|
35
|
+
raise ArgumentError
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -41,14 +41,22 @@ module ShellOpts
|
|
41
41
|
#
|
42
42
|
# #expect raise a ShellOpts::UserError exception if the array is not emptied
|
43
43
|
# by the operation
|
44
|
+
#
|
44
45
|
def expect(count_or_range, message = nil)
|
45
|
-
count_or_range
|
46
|
+
case count_or_range
|
47
|
+
when Range
|
48
|
+
count_or_range === self.size or inoa(message)
|
49
|
+
when Integer
|
50
|
+
count_or_range >= 0 or raise ArgumentError, "Count can't be negative"
|
51
|
+
count_or_range.abs == self.size or inoa(message)
|
52
|
+
end
|
46
53
|
extract(count_or_range) # Can't fail
|
47
54
|
end
|
48
55
|
|
49
56
|
private
|
50
57
|
def inoa(message = nil)
|
51
|
-
raise
|
58
|
+
raise ArgumentError, message || "Illegal number of arguments"
|
52
59
|
end
|
53
60
|
end
|
54
61
|
end
|
62
|
+
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
class ArgumentType
|
4
|
+
# Name of type
|
5
|
+
def name() self.class.to_s.sub(/.*::/, "").sub(/Argument/, "") end
|
6
|
+
|
7
|
+
# Return truish if value literal (String) match the type. Returns false
|
8
|
+
# and set #message if the value doesn't match. <name> is used to
|
9
|
+
# construct the error message and is the name/alias the user specified on
|
10
|
+
# the command line
|
11
|
+
def match?(name, literal) true end
|
12
|
+
|
13
|
+
# Error message if match? returned false. Note that this method is not
|
14
|
+
# safe for concurrent processing
|
15
|
+
attr_reader :message
|
16
|
+
|
17
|
+
# Return true if .value is an "instance" of self (not used atm. See
|
18
|
+
# Command#[] and Grammar::Option#value?)
|
19
|
+
def value?(value) true end
|
20
|
+
|
21
|
+
# Convert value to Ruby type
|
22
|
+
def convert(value) value end
|
23
|
+
|
24
|
+
# String representation. Equal to #name
|
25
|
+
def to_s() name end
|
26
|
+
|
27
|
+
protected
|
28
|
+
# it is important that #set_message return false
|
29
|
+
def set_message(msg)
|
30
|
+
@message = msg
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class StringType < ArgumentType
|
36
|
+
end
|
37
|
+
|
38
|
+
class IntegerArgument < ArgumentType
|
39
|
+
def match?(name, literal)
|
40
|
+
literal =~ /^-?\d+$/ or
|
41
|
+
set_message "Illegal integer value in #{name}: #{literal}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def value?(value) value.is_a?(Integer) end
|
45
|
+
def convert(value) value.to_i end
|
46
|
+
end
|
47
|
+
|
48
|
+
class FloatArgument < ArgumentType
|
49
|
+
def match?(name, literal)
|
50
|
+
# https://stackoverflow.com/a/21891705/2130986
|
51
|
+
literal =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
|
52
|
+
set_message "Illegal decimal value in #{name}: #{literal}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def value?(value) value.is_a?(Numeric) end
|
56
|
+
def convert(value) value.to_f end
|
57
|
+
end
|
58
|
+
|
59
|
+
class FileArgument < ArgumentType
|
60
|
+
attr_reader :kind
|
61
|
+
|
62
|
+
def initialize(kind)
|
63
|
+
constrain kind, :file, :dir, :path, :efile, :edir, :epath, :nfile, :ndir, :npath
|
64
|
+
@kind = kind
|
65
|
+
end
|
66
|
+
|
67
|
+
def match?(name, literal)
|
68
|
+
case kind
|
69
|
+
when :file; match_path(name, literal, kind, :file?, :default)
|
70
|
+
when :dir; match_path(name, literal, kind, :directory?, :default)
|
71
|
+
when :path; match_path(name, literal, kind, :exist?, :default)
|
72
|
+
|
73
|
+
when :efile; match_path(name, literal, kind, :file?, :exist)
|
74
|
+
when :edir; match_path(name, literal, kind, :directory?, :exist)
|
75
|
+
when :epath; match_path(name, literal, kind, :exist?, :exist)
|
76
|
+
|
77
|
+
when :nfile; match_path(name, literal, kind, :file?, :new)
|
78
|
+
when :ndir; match_path(name, literal, kind, :directory?, :new)
|
79
|
+
when :npath; match_path(name, literal, kind, :exist?, :new)
|
80
|
+
else
|
81
|
+
raise InternalError, "Illegal kind: #{kind.inspect}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Note: No checks done, not sure if it is a feature or a bug
|
86
|
+
def value?(value) value.is_a?(String) end
|
87
|
+
|
88
|
+
protected
|
89
|
+
def match_path(name, literal, kind, method, mode)
|
90
|
+
subject =
|
91
|
+
case kind
|
92
|
+
when :file, :efile, :nfile; "regular file"
|
93
|
+
when :dir, :edir, :ndir; "directory"
|
94
|
+
when :path, :epath, :npath; "path"
|
95
|
+
else
|
96
|
+
raise ArgumentError
|
97
|
+
end
|
98
|
+
|
99
|
+
if File.send(method, literal) # exists?
|
100
|
+
if mode == :new
|
101
|
+
set_message "#{subject.capitalize} already exists in #{name}: #{literal}"
|
102
|
+
elsif kind == :path || kind == :epath
|
103
|
+
if File.file?(literal) || File.directory?(literal)
|
104
|
+
true
|
105
|
+
else
|
106
|
+
set_message "Expected regular file or directory as #{name} argument: #{literal}"
|
107
|
+
end
|
108
|
+
else
|
109
|
+
true
|
110
|
+
end
|
111
|
+
elsif File.exist?(literal) # exists but not the right type
|
112
|
+
if mode == :new
|
113
|
+
set_message "#{subject.capitalize} already exists"
|
114
|
+
else
|
115
|
+
set_message "Expected #{subject} as #{name} argument: #{literal}"
|
116
|
+
end
|
117
|
+
else # does not exist
|
118
|
+
if [:default, :new].include? mode
|
119
|
+
if File.exist?(File.dirname(literal))
|
120
|
+
true
|
121
|
+
else
|
122
|
+
set_message "Illegal path in #{name}: #{literal}"
|
123
|
+
end
|
124
|
+
else
|
125
|
+
set_message "Error in #{name} argument: Can't find #{literal}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class EnumArgument < ArgumentType
|
132
|
+
attr_reader :values
|
133
|
+
def initialize(values) @values = values.dup end
|
134
|
+
def match?(name, literal) literal?(literal) or set_message "Illegal value in #{name}: '#{literal}'" end
|
135
|
+
def value?(value) @values.include?(value) end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
class IdrNode
|
4
|
+
def dump_doc
|
5
|
+
puts "#{self.class} #{ident}"
|
6
|
+
indent {
|
7
|
+
children.each(&:dump_doc)
|
8
|
+
}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Grammar
|
14
|
+
class Command
|
15
|
+
# Usable after parsing
|
16
|
+
def render_structure
|
17
|
+
io = StringIO.new
|
18
|
+
dump_structure(io)
|
19
|
+
io.string
|
20
|
+
end
|
21
|
+
|
22
|
+
def dump_structure(device = $stdout)
|
23
|
+
device.puts ident
|
24
|
+
device.indent { |dev|
|
25
|
+
option_groups.each { |group| dev.puts group.options.map(&:name).join(" ") }
|
26
|
+
commands.each { |command| command.dump_structure(dev) }
|
27
|
+
descrs.each { |descr| dev.puts descr.text }
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Grammar
|
34
|
+
class Node
|
35
|
+
def dump_ast
|
36
|
+
puts "#{classname} @ #{token.pos} #{token.source}"
|
37
|
+
indent { children.each(&:dump_ast) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def dump_idr(short = false)
|
41
|
+
puts "#{classname}" if !short
|
42
|
+
end
|
43
|
+
|
44
|
+
def dump_attrs(*attrs)
|
45
|
+
indent {
|
46
|
+
Array(attrs).flatten.select { |attr| attr.is_a?(Symbol) }.each { |attr|
|
47
|
+
value = self.send(attr)
|
48
|
+
case value
|
49
|
+
when Brief
|
50
|
+
puts "#{attr}: #{value.text}"
|
51
|
+
when Node
|
52
|
+
puts "#{attr}:"
|
53
|
+
indent { value.dump_idr }
|
54
|
+
when Array
|
55
|
+
case value.first
|
56
|
+
when nil
|
57
|
+
puts "#{attr}: []"
|
58
|
+
when Node
|
59
|
+
puts "#{attr}:"
|
60
|
+
indent { value.each(&:dump_idr) }
|
61
|
+
else
|
62
|
+
puts "#{attr}: #{value.inspect}"
|
63
|
+
end
|
64
|
+
when ArgumentType
|
65
|
+
puts "#{attr}: #{value}"
|
66
|
+
else
|
67
|
+
# value = value.inspect if value.nil? || !value.respond_to?(:to_s)
|
68
|
+
puts "#{attr}: #{value.inspect}"
|
69
|
+
end
|
70
|
+
}
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
def classname() self.class.to_s.sub(/.*::/, "") end
|
76
|
+
end
|
77
|
+
|
78
|
+
class Option
|
79
|
+
def dump_idr(short = false)
|
80
|
+
if short
|
81
|
+
s = [
|
82
|
+
name,
|
83
|
+
argument? ? argument_type.name : nil,
|
84
|
+
optional? ? "?" : nil,
|
85
|
+
repeatable? ? "*" : nil
|
86
|
+
].compact.join(" ")
|
87
|
+
puts s
|
88
|
+
else
|
89
|
+
puts "#{name}: #{classname}"
|
90
|
+
dump_attrs(
|
91
|
+
:uid, :path, :attr, :ident, :name, :idents, :names,
|
92
|
+
:repeatable?,
|
93
|
+
:argument?, argument? && :argument_name, argument? && :argument_type,
|
94
|
+
:enum?, enum? && :argument_enum,
|
95
|
+
:optional?)
|
96
|
+
indent { puts "brief: #{group.brief}" }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Command
|
102
|
+
def dump_idr(short = false)
|
103
|
+
if short
|
104
|
+
puts name
|
105
|
+
indent {
|
106
|
+
options.each { |option| option.dump_idr(short) }
|
107
|
+
commands.each { |command| command.dump_idr(short) }
|
108
|
+
descrs.each { |descr| descr.dump_idr(short) }
|
109
|
+
}
|
110
|
+
else
|
111
|
+
puts "#{name}: #{classname}"
|
112
|
+
dump_attrs :uid, :path, :ident, :name, :options, :commands, :specs, :descrs, :brief
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class ArgDescr
|
118
|
+
def dump_idr(short = false)
|
119
|
+
super
|
120
|
+
indent { puts token.to_s }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class ArgSpec < Node
|
125
|
+
def dump_idr(short = false)
|
126
|
+
super
|
127
|
+
dump_attrs :arguments
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class Arg < Node
|
132
|
+
def dump_idr(short = false)
|
133
|
+
puts "<type>"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Command
|
139
|
+
def __dump__(argv = [])
|
140
|
+
::Kernel.puts __name__
|
141
|
+
::Kernel.indent {
|
142
|
+
__options__.each { |ident, value| ::Kernel.puts "#{ident}: #{value.inspect}" }
|
143
|
+
__subcommand__!&.__dump__
|
144
|
+
::Kernel.puts argv.map(&:inspect).join(" ") if !argv.empty?
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
# Class-level accessor methods
|
149
|
+
def self.dump(expr, argv = []) expr.__dump__(argv) end
|
150
|
+
end
|
151
|
+
|
152
|
+
class Option
|
153
|
+
def dump
|
154
|
+
::Kernel.puts [name, argument].compact.join(" ")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'terminfo'
|
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
|
+
module ShellOpts
|
11
|
+
module Grammar
|
12
|
+
class Node
|
13
|
+
def puts_help() end
|
14
|
+
def puts_usage() end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Option
|
18
|
+
end
|
19
|
+
|
20
|
+
class OptionGroup
|
21
|
+
def puts_descr
|
22
|
+
puts Ansi.bold(render(:multi))
|
23
|
+
indent {
|
24
|
+
if description.any?
|
25
|
+
description.each { |descr|
|
26
|
+
descr.puts_descr
|
27
|
+
puts if descr != description.last
|
28
|
+
}
|
29
|
+
elsif brief
|
30
|
+
brief.puts_descr
|
31
|
+
end
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# brief one-line commands should optionally use compact options
|
37
|
+
class Command
|
38
|
+
using Ext::Array::Wrap
|
39
|
+
|
40
|
+
def puts_usage(bol: false)
|
41
|
+
if descrs.size == 0
|
42
|
+
print (lead = Formatter.command_prefix || "")
|
43
|
+
indent(lead.size, ' ', bol: bol && lead == "") {
|
44
|
+
puts render(:multi, Formatter::USAGE_MAX_WIDTH)
|
45
|
+
}
|
46
|
+
else
|
47
|
+
lead = Formatter.command_prefix || ""
|
48
|
+
descrs.each { |descr|
|
49
|
+
print lead
|
50
|
+
puts render(:single, Formatter::USAGE_MAX_WIDTH, args: [descr.text])
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def puts_brief
|
56
|
+
width = Formatter.rest
|
57
|
+
option_briefs = option_groups.map { |group| [group.render(:enum), group.brief&.words] }
|
58
|
+
command_briefs = commands.map { |command| [command.render(:single, width), command.brief&.words] }
|
59
|
+
widths = Formatter::compute_columns(width, option_briefs + command_briefs)
|
60
|
+
|
61
|
+
if brief
|
62
|
+
puts brief
|
63
|
+
puts
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "Usage"
|
67
|
+
indent { puts_usage(bol: true) }
|
68
|
+
|
69
|
+
if options.any?
|
70
|
+
puts
|
71
|
+
puts "Options"
|
72
|
+
indent { Formatter::puts_columns(widths, option_briefs) }
|
73
|
+
end
|
74
|
+
|
75
|
+
if commands.any?
|
76
|
+
puts
|
77
|
+
puts "Commands"
|
78
|
+
indent { Formatter::puts_columns(widths, command_briefs) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
|
83
|
+
puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
|
84
|
+
indent {
|
85
|
+
if brief
|
86
|
+
puts self.brief.words.wrap(Formatter.rest)
|
87
|
+
else
|
88
|
+
newline = false
|
89
|
+
children.each { |child|
|
90
|
+
puts if newline
|
91
|
+
newline = true
|
92
|
+
|
93
|
+
if child.is_a?(Command)
|
94
|
+
child.puts_descr(prefix, name: :path)
|
95
|
+
else
|
96
|
+
child.puts_descr
|
97
|
+
end
|
98
|
+
}
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def puts_help
|
104
|
+
puts Ansi.bold "NAME"
|
105
|
+
full_name = [Formatter::command_prefix, name].join
|
106
|
+
indent { puts brief ? "#{full_name} - #{brief}" : full_name }
|
107
|
+
puts
|
108
|
+
|
109
|
+
puts Ansi.bold "USAGE"
|
110
|
+
indent { puts_usage(bol: true) }
|
111
|
+
|
112
|
+
section = {
|
113
|
+
Paragraph => "DESCRIPTION",
|
114
|
+
OptionGroup => "OPTIONS",
|
115
|
+
Command => "COMMANDS"
|
116
|
+
}
|
117
|
+
|
118
|
+
newline = false # True if a newline should be printed before child
|
119
|
+
indent {
|
120
|
+
children.each { |child|
|
121
|
+
if child.is_a?(Section) # Explicit section
|
122
|
+
# p :A
|
123
|
+
puts
|
124
|
+
indent(-1).puts Ansi.bold child.name
|
125
|
+
section.delete_if { |_,v| v == child.name }
|
126
|
+
section.delete(Paragraph)
|
127
|
+
newline = false
|
128
|
+
next
|
129
|
+
elsif s = section[child.class] # Implicit section
|
130
|
+
# p :B
|
131
|
+
puts
|
132
|
+
indent(-1).puts Ansi.bold s
|
133
|
+
section.delete(child.class)
|
134
|
+
section.delete(Paragraph)
|
135
|
+
newline = false
|
136
|
+
else # Any other node add a newline
|
137
|
+
# p :C
|
138
|
+
puts if newline
|
139
|
+
newline = true
|
140
|
+
end
|
141
|
+
|
142
|
+
if child.is_a?(Command)
|
143
|
+
# prefix = child.parent != self ? nil : child.supercommand&.name
|
144
|
+
prefix = child.supercommand == self ? nil : child.supercommand&.name
|
145
|
+
child.puts_descr(prefix, brief: false, name: :path)
|
146
|
+
newline = true
|
147
|
+
else
|
148
|
+
child.puts_descr
|
149
|
+
newline = true
|
150
|
+
end
|
151
|
+
}
|
152
|
+
|
153
|
+
# Also emit commands not declared in nested scope
|
154
|
+
(commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
|
155
|
+
puts if newline
|
156
|
+
newline = true
|
157
|
+
prefix = cmd.supercommand == self ? nil : cmd.supercommand&.name
|
158
|
+
cmd.puts_descr(prefix, brief: false, name: path)
|
159
|
+
}
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class Program
|
165
|
+
using Ext::Array::Wrap
|
166
|
+
end
|
167
|
+
|
168
|
+
class DocNode
|
169
|
+
def puts_descr() puts lines end
|
170
|
+
end
|
171
|
+
|
172
|
+
module WrappedNode
|
173
|
+
def puts_descr(width = Formatter.rest) puts lines(width) end
|
174
|
+
end
|
175
|
+
|
176
|
+
class Code
|
177
|
+
def puts_descr() indent { super } end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class Formatter
|
182
|
+
using Ext::Array::Wrap
|
183
|
+
|
184
|
+
# Right margin
|
185
|
+
MARGIN_RIGHT = 3
|
186
|
+
|
187
|
+
# String for 'Usage' in error messages
|
188
|
+
USAGE_STRING = "Usage"
|
189
|
+
|
190
|
+
# Indent to use in usage output
|
191
|
+
USAGE_INDENT = USAGE_STRING.size
|
192
|
+
|
193
|
+
# Width of usage (after usage string)
|
194
|
+
USAGE_MAX_WIDTH = 70
|
195
|
+
|
196
|
+
# Indent to use in brief output
|
197
|
+
BRIEF_INDENT = 2
|
198
|
+
|
199
|
+
# Number of characters between columns in brief output
|
200
|
+
BRIEF_COL_SEP = 2
|
201
|
+
|
202
|
+
# Maximum width of first column in brief option and command lists
|
203
|
+
BRIEF_COL1_MIN_WIDTH = 6
|
204
|
+
|
205
|
+
# Maximum width of first column in brief option and command lists
|
206
|
+
BRIEF_COL1_MAX_WIDTH = 40
|
207
|
+
|
208
|
+
# Minimum width of second column in brief option and command lists
|
209
|
+
BRIEF_COL2_MAX_WIDTH = 50
|
210
|
+
|
211
|
+
# Indent to use in help output
|
212
|
+
HELP_INDENT = 4
|
213
|
+
|
214
|
+
# Command prefix when subject is a sub-command
|
215
|
+
def self.command_prefix() @command_prefix end
|
216
|
+
|
217
|
+
# Usage string in error messages
|
218
|
+
def self.usage(subject)
|
219
|
+
subject = Grammar::Command.command(subject)
|
220
|
+
@command_prefix = subject.ancestors.map { |node| node.name + " " }.join
|
221
|
+
setup_indent(1) {
|
222
|
+
print lead = "#{USAGE_STRING}: "
|
223
|
+
indent(lead.size, ' ', bol: false) { subject.puts_usage }
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
# # TODO
|
228
|
+
# def self.usage=(usage_lambda)
|
229
|
+
# end
|
230
|
+
|
231
|
+
# When the user gives a -h option
|
232
|
+
def self.brief(command)
|
233
|
+
command = Grammar::Command.command(command)
|
234
|
+
@command_prefix = command.ancestors.map { |node| node.name + " " }.join
|
235
|
+
setup_indent(BRIEF_INDENT) { command.puts_brief }
|
236
|
+
end
|
237
|
+
|
238
|
+
# # TODO
|
239
|
+
# def self.brief=(brief_lambda)
|
240
|
+
# end
|
241
|
+
|
242
|
+
# When the user gives a --help option
|
243
|
+
def self.help(subject)
|
244
|
+
subject = Grammar::Command.command(subject)
|
245
|
+
@command_prefix = subject.ancestors.map { |node| node.name + " " }.join
|
246
|
+
setup_indent(HELP_INDENT) { subject.puts_help }
|
247
|
+
end
|
248
|
+
|
249
|
+
# Short-hand to get the Grammar::Command object
|
250
|
+
def self.command_of(obj)
|
251
|
+
constrain obj, Grammar::Command, ::ShellOpts::Program
|
252
|
+
obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
|
253
|
+
end
|
254
|
+
|
255
|
+
# # TODO
|
256
|
+
# def self.help_w_lambda(program)
|
257
|
+
# if @help_lambda
|
258
|
+
# #
|
259
|
+
# else
|
260
|
+
# program = Grammar::Command.command(program)
|
261
|
+
# setup_indent(HELP_INDENT) { program.puts_descr }
|
262
|
+
# end
|
263
|
+
# end
|
264
|
+
#
|
265
|
+
# def self.help=(help_lambda) @help_lambda end
|
266
|
+
|
267
|
+
def self.puts_columns(widths, fields)
|
268
|
+
l = []
|
269
|
+
first_width, second_width = *widths
|
270
|
+
second_col = first_width + 2
|
271
|
+
|
272
|
+
for (first, second) in fields
|
273
|
+
if first.size > first_width
|
274
|
+
puts first
|
275
|
+
indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
|
276
|
+
elsif second
|
277
|
+
printf "%-#{first_width + BRIEF_COL_SEP}s", first
|
278
|
+
indent(first_width, bol: false) { puts second.wrap(second_width) }
|
279
|
+
else
|
280
|
+
puts first
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.compute_columns(width, fields)
|
286
|
+
first_max = [
|
287
|
+
(fields.map { |first, _| first.size } + [BRIEF_COL1_MIN_WIDTH]).max,
|
288
|
+
BRIEF_COL1_MAX_WIDTH
|
289
|
+
].min
|
290
|
+
second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1: 0 }.max
|
291
|
+
|
292
|
+
if first_max + BRIEF_COL_SEP + second_max <= width
|
293
|
+
first_width = first_max
|
294
|
+
second_width = second_max
|
295
|
+
elsif first_max + BRIEF_COL_SEP + BRIEF_COL2_MAX_WIDTH <= width
|
296
|
+
first_width = first_max
|
297
|
+
second_width = width - first_width - BRIEF_COL_SEP
|
298
|
+
else
|
299
|
+
first_width = [width - BRIEF_COL_SEP - BRIEF_COL2_MAX_WIDTH, BRIEF_COL1_MAX_WIDTH].min
|
300
|
+
second_width = BRIEF_COL2_MAX_WIDTH
|
301
|
+
end
|
302
|
+
|
303
|
+
[first_width, second_width]
|
304
|
+
end
|
305
|
+
|
306
|
+
def self.width()
|
307
|
+
@width ||= TermInfo.screen_width - MARGIN_RIGHT
|
308
|
+
end
|
309
|
+
|
310
|
+
def self.rest() width - $stdout.margin end
|
311
|
+
|
312
|
+
private
|
313
|
+
# TODO Get rid of?
|
314
|
+
def self.setup_indent(indent, &block)
|
315
|
+
default_indent = IndentedIO.default_indent
|
316
|
+
begin
|
317
|
+
IndentedIO.default_indent = " " * indent
|
318
|
+
indent(0) { yield } # Ensure IndentedIO is on the top of the stack so we can use $stdout.levels
|
319
|
+
ensure
|
320
|
+
IndentedIO.default_indent = default_indent
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|