shellopts 2.0.7 → 2.0.10
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/shellopts/analyzer.rb +52 -20
- data/lib/shellopts/args.rb +10 -3
- data/lib/shellopts/formatter.rb +30 -21
- data/lib/shellopts/grammar.rb +24 -11
- data/lib/shellopts/interpreter.rb +1 -1
- data/lib/shellopts/lexer.rb +2 -2
- data/lib/shellopts/parser.rb +15 -19
- 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
- data/shellopts.gemspec +7 -6
- metadata +6 -8
- data/.gitignore +0 -30
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 812078e64c677a6aff78081e25a04b81be3b3a1fe9496709838e2abb80573b4a
|
4
|
+
data.tar.gz: 92c14741c409697f543820d656683b9552d4c6c7ce5c29b01a30677a174549c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b1ac7453a534a50cf250e489688aa82ad1928220a615b9e4f7076c27b588d19a7b355e866f3e3cc789fd2259bb2e5de14ac92e905b7b033528c11be26564356
|
7
|
+
data.tar.gz: d50433ec49a4bf67fd9947d2028ed81be9c8310e7410d93c691e2cd59ed36ef622937c861ef1f4608e7ef5869804cd0f1a4197abf03fd4c2d96cef4985d5dab4
|
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/shellopts/analyzer.rb
CHANGED
@@ -20,18 +20,17 @@ 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
|
30
26
|
|
31
|
-
# Move options before first command
|
27
|
+
# Move options before first command or before explicit COMMAND section
|
32
28
|
def reorder_options
|
33
29
|
if commands.any?
|
34
|
-
|
30
|
+
i = children.find_index { |child|
|
31
|
+
child.is_a?(Command) || child.is_a?(Section) && child.name == "COMMAND"
|
32
|
+
}
|
33
|
+
if i
|
35
34
|
options, rest = children[i+1..-1].partition { |child| child.is_a?(OptionGroup) }
|
36
35
|
@children = children[0, i] + options + children[i..i] + rest
|
37
36
|
end
|
@@ -51,9 +50,9 @@ module ShellOpts
|
|
51
50
|
}
|
52
51
|
end
|
53
52
|
|
53
|
+
# TODO Check for dash-collision
|
54
54
|
def compute_command_hashes
|
55
55
|
commands.each { |command|
|
56
|
-
# TODO Check for dash-collision
|
57
56
|
!@commands_hash.key?(command.name) or
|
58
57
|
analyzer_error command.token, "Duplicate command name: #{command.name}"
|
59
58
|
@commands_hash[command.name] = command
|
@@ -73,41 +72,74 @@ module ShellOpts
|
|
73
72
|
@grammar = grammar
|
74
73
|
end
|
75
74
|
|
76
|
-
|
77
|
-
|
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
|
78
93
|
# We can't use Command#[] at this point so we collect the commands here
|
79
94
|
h = {}
|
80
95
|
@grammar.traverse(Grammar::Command) { |command|
|
81
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
|
82
113
|
}
|
83
114
|
|
84
|
-
# Find commands to
|
115
|
+
# Find commands to link
|
85
116
|
#
|
86
|
-
# Commands are
|
87
|
-
# not defined when the data structure changes beneath it
|
88
|
-
|
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?)
|
89
120
|
@grammar.traverse(Grammar::Command) { |command|
|
90
121
|
if command.path.size > 1 && command.parent && command.parent.path != command.path[0..-2]
|
91
|
-
|
122
|
+
# if command.path.size > 1 && command.parent.path != command.path[0..-2]
|
123
|
+
link << command
|
92
124
|
else
|
93
125
|
command.instance_variable_set(:@command, command.parent)
|
94
126
|
end
|
95
127
|
}
|
96
128
|
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
101
134
|
supercommand.commands << command
|
102
135
|
command.instance_variable_set(:@command, supercommand)
|
103
136
|
}
|
104
137
|
end
|
105
138
|
|
106
139
|
def analyze()
|
107
|
-
|
140
|
+
link_commands
|
108
141
|
|
109
142
|
@grammar.traverse(Grammar::Command) { |command|
|
110
|
-
command.set_supercommand
|
111
143
|
command.reorder_options
|
112
144
|
command.collect_options
|
113
145
|
command.compute_option_hashes
|
data/lib/shellopts/args.rb
CHANGED
@@ -5,6 +5,11 @@ module ShellOpts
|
|
5
5
|
# methods raise a ShellOpts::Error exception in case of errors
|
6
6
|
#
|
7
7
|
class Args < Array
|
8
|
+
def initialize(*args, exception: false)
|
9
|
+
super(*args)
|
10
|
+
@exception = exception
|
11
|
+
end
|
12
|
+
|
8
13
|
# :call-seq:
|
9
14
|
# extract(count, message = nil)
|
10
15
|
# extract(range, message = nil)
|
@@ -21,7 +26,7 @@ module ShellOpts
|
|
21
26
|
# #extract raise a ShellOpts::Error exception if there's is not enough
|
22
27
|
# elements in the array to satisfy the request
|
23
28
|
#
|
24
|
-
def extract(count_or_range, message = nil)
|
29
|
+
def extract(count_or_range, message = nil)
|
25
30
|
case count_or_range
|
26
31
|
when Range
|
27
32
|
range = count_or_range
|
@@ -59,8 +64,10 @@ module ShellOpts
|
|
59
64
|
end
|
60
65
|
|
61
66
|
private
|
62
|
-
def inoa(message = nil)
|
63
|
-
|
67
|
+
def inoa(message = nil)
|
68
|
+
message ||= "Illegal number of arguments"
|
69
|
+
raise Error, message if @exception
|
70
|
+
::ShellOpts.error(message)
|
64
71
|
end
|
65
72
|
end
|
66
73
|
end
|
data/lib/shellopts/formatter.rb
CHANGED
@@ -81,7 +81,15 @@ module ShellOpts
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
|
84
|
-
|
84
|
+
# Use one-line mode if all options are declared on one line
|
85
|
+
if options.all? { |option| option.token.lineno == token.lineno }
|
86
|
+
puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
|
87
|
+
puts_options = false
|
88
|
+
else
|
89
|
+
puts Ansi.bold([prefix, render(:abbr, Formatter.rest)].flatten.compact.join(" "))
|
90
|
+
puts_options = true
|
91
|
+
end
|
92
|
+
|
85
93
|
indent {
|
86
94
|
if brief
|
87
95
|
puts self.brief.words.wrap(Formatter.rest)
|
@@ -93,7 +101,10 @@ module ShellOpts
|
|
93
101
|
|
94
102
|
if child.is_a?(Command)
|
95
103
|
child.puts_descr(prefix, name: :path)
|
96
|
-
|
104
|
+
elsif child.is_a?(OptionGroup)
|
105
|
+
child.puts_descr if puts_options
|
106
|
+
newline = false
|
107
|
+
else
|
97
108
|
child.puts_descr
|
98
109
|
end
|
99
110
|
}
|
@@ -112,37 +123,34 @@ module ShellOpts
|
|
112
123
|
|
113
124
|
section = {
|
114
125
|
Paragraph => "DESCRIPTION",
|
115
|
-
OptionGroup => "
|
116
|
-
Command => "
|
126
|
+
OptionGroup => "OPTION",
|
127
|
+
Command => "COMMAND"
|
117
128
|
}
|
118
129
|
|
130
|
+
seen_sections = {}
|
119
131
|
newline = false # True if a newline should be printed before child
|
120
132
|
indent {
|
121
133
|
children.each { |child|
|
122
|
-
|
123
|
-
#
|
124
|
-
|
125
|
-
indent(-1).puts Ansi.bold child.name
|
126
|
-
section.delete_if { |_,v| v == child.name }
|
134
|
+
klass = child.is_a?(Section) ? section.key(child.name) : child.class
|
135
|
+
if s = section[klass] # Implicit section
|
136
|
+
section.delete(klass)
|
127
137
|
section.delete(Paragraph)
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
138
|
+
if klass <= OptionGroup
|
139
|
+
s += "S" if options.size > 1
|
140
|
+
elsif klass <= Command
|
141
|
+
s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
|
142
|
+
end
|
143
|
+
puts
|
133
144
|
indent(-1).puts Ansi.bold s
|
134
|
-
section.delete(child.class)
|
135
|
-
section.delete(Paragraph)
|
136
145
|
newline = false
|
137
|
-
|
138
|
-
#
|
146
|
+
next if child.is_a?(Section)
|
147
|
+
else # Any other node adds a newline
|
139
148
|
puts if newline
|
140
149
|
newline = true
|
141
150
|
end
|
142
151
|
|
143
152
|
if child.is_a?(Command)
|
144
|
-
|
145
|
-
prefix = child.supercommand == self ? nil : child.supercommand&.name
|
153
|
+
prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
|
146
154
|
child.puts_descr(prefix, brief: false, name: :path)
|
147
155
|
newline = true
|
148
156
|
else
|
@@ -153,9 +161,10 @@ module ShellOpts
|
|
153
161
|
|
154
162
|
# Also emit commands not declared in nested scope
|
155
163
|
(commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
|
164
|
+
next if cmd.parent.nil? # Skip implicit commands
|
156
165
|
puts if newline
|
157
166
|
newline = true
|
158
|
-
prefix = cmd.
|
167
|
+
prefix = cmd.command == self ? nil : cmd.command&.name
|
159
168
|
cmd.puts_descr(prefix, brief: false, name: path)
|
160
169
|
}
|
161
170
|
}
|
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"
|
@@ -151,6 +160,10 @@ module ShellOpts
|
|
151
160
|
class OptionGroup < Node
|
152
161
|
alias_method :command, :parent
|
153
162
|
|
163
|
+
# Duck typing for compatibility with IdrNode (TODO: maybe just make
|
164
|
+
# OptionGroup an IdrNode and be over with it)
|
165
|
+
def name() options.first&.name end
|
166
|
+
|
154
167
|
# Array of options in declaration order
|
155
168
|
attr_reader :options
|
156
169
|
|
@@ -185,10 +198,6 @@ module ShellOpts
|
|
185
198
|
# methods are initialized by the analyzer
|
186
199
|
#
|
187
200
|
class Command < IdrNode
|
188
|
-
# Supercommand or nil if this is the top-level Program object.
|
189
|
-
# Initialized by the analyzer
|
190
|
-
attr_reader :supercommand
|
191
|
-
|
192
201
|
# Brief description of command
|
193
202
|
attr_accessor :brief
|
194
203
|
|
@@ -352,6 +361,10 @@ module ShellOpts
|
|
352
361
|
end
|
353
362
|
|
354
363
|
class Section < Node
|
364
|
+
def initialize(parent, token)
|
365
|
+
constrain token.source, *%w(DESCRIPTION OPTION COMMAND)
|
366
|
+
super
|
367
|
+
end
|
355
368
|
def name() token.source end
|
356
369
|
end
|
357
370
|
|
data/lib/shellopts/lexer.rb
CHANGED
@@ -45,7 +45,7 @@ module ShellOpts
|
|
45
45
|
# Match ArgDescr words (should be at least two characters long)
|
46
46
|
DESCR_RE = /^[^a-z]{2,}$/
|
47
47
|
|
48
|
-
SECTIONS = %w(DESCRIPTION OPTIONS COMMANDS)
|
48
|
+
SECTIONS = %w(DESCRIPTION OPTION OPTIONS COMMAND COMMANDS)
|
49
49
|
|
50
50
|
using Ext::Array::ShiftWhile
|
51
51
|
|
@@ -109,7 +109,7 @@ module ShellOpts
|
|
109
109
|
|
110
110
|
# Sections
|
111
111
|
elsif SECTIONS.include?(line.text)
|
112
|
-
@tokens << Token.new(:section, line.lineno, line.charno, line.text)
|
112
|
+
@tokens << Token.new(:section, line.lineno, line.charno, line.text.sub(/S$/, ""))
|
113
113
|
|
114
114
|
# Options, commands, usage, arguments, and briefs
|
115
115
|
elsif line =~ DECL_RE
|
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,16 +124,19 @@ 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
|
-
paragraph_token = Token.new(:text, 1, 1,
|
138
|
+
paragraph_token = Token.new(:text, 1, 1,
|
139
|
+
"-h prints a brief help text, --help prints a longer man-style description of the command")
|
136
140
|
group = OptionGroup.new(self, option_token)
|
137
141
|
option = Option.parse(group, option_token)
|
138
142
|
brief = Brief.parse(group, brief_token)
|
@@ -162,14 +166,6 @@ module ShellOpts
|
|
162
166
|
@nodes = {}
|
163
167
|
end
|
164
168
|
|
165
|
-
# def add_stdopts
|
166
|
-
# version_token = Token.new(:option, 1, 1, "--version")
|
167
|
-
# version_brief = Token.new(:brief, 1, 1, "Gryf gryf")
|
168
|
-
# group = Grammar::OptionGroup.new(@program, version_token)
|
169
|
-
# option = Grammar::Option.parse(group, version_token)
|
170
|
-
# brief = Grammr::Brief.parse(option, version_brief)
|
171
|
-
# end
|
172
|
-
|
173
169
|
def parse()
|
174
170
|
@program = Grammar::Program.parse(@tokens.shift)
|
175
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
|
|
data/shellopts.gemspec
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "shellopts/version"
|
2
|
+
require_relative "lib/shellopts/version"
|
5
3
|
|
6
4
|
Gem::Specification.new do |spec|
|
7
5
|
spec.name = "shellopts"
|
@@ -15,15 +13,18 @@ Gem::Specification.new do |spec|
|
|
15
13
|
and has built-in help and error messages}
|
16
14
|
spec.homepage = "http://github.com/clrgit/shellopts"
|
17
15
|
|
18
|
-
spec.files
|
19
|
-
|
16
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
17
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
19
|
+
end
|
20
20
|
end
|
21
|
+
|
21
22
|
spec.bindir = "exe"
|
22
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
24
|
spec.require_paths = ["lib"]
|
24
25
|
|
25
26
|
spec.add_dependency "forward_to"
|
26
|
-
spec.add_dependency "constrain"
|
27
|
+
spec.add_dependency "constrain", "~> 0.5.1"
|
27
28
|
spec.add_dependency "ruby-terminfo"
|
28
29
|
spec.add_dependency "indented_io"
|
29
30
|
|
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.10
|
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-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: forward_to
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: constrain
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.5.1
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.5.1
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: ruby-terminfo
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -132,10 +132,8 @@ executables: []
|
|
132
132
|
extensions: []
|
133
133
|
extra_rdoc_files: []
|
134
134
|
files:
|
135
|
-
- ".gitignore"
|
136
135
|
- ".rspec"
|
137
136
|
- ".ruby-version"
|
138
|
-
- ".travis.yml"
|
139
137
|
- Gemfile
|
140
138
|
- README.md
|
141
139
|
- Rakefile
|
data/.gitignore
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
/.bundle/
|
2
|
-
/.yardoc
|
3
|
-
/_yardoc/
|
4
|
-
/rdoc/
|
5
|
-
/pkg/
|
6
|
-
/spec/reports/
|
7
|
-
/tmp/
|
8
|
-
|
9
|
-
# rspec failure tracking
|
10
|
-
.rspec_status
|
11
|
-
|
12
|
-
# simplecov
|
13
|
-
/coverage/
|
14
|
-
|
15
|
-
# Ignore Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
|
16
|
-
/Gemfile.lock
|
17
|
-
|
18
|
-
# Ignore vim files
|
19
|
-
.*.swp
|
20
|
-
|
21
|
-
# Ignore t.* files
|
22
|
-
t
|
23
|
-
t.*
|
24
|
-
tt
|
25
|
-
tt.*
|
26
|
-
s
|
27
|
-
s.*
|
28
|
-
|
29
|
-
# Ignore temporary directory
|
30
|
-
/spec/tmpdir/
|