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.rb
CHANGED
@@ -1,257 +1,476 @@
|
|
1
|
-
require "shellopts/version"
|
2
1
|
|
3
|
-
|
2
|
+
$quiet = nil
|
3
|
+
$verb = nil
|
4
|
+
$debug = nil
|
5
|
+
$shellopts = nil
|
6
|
+
|
7
|
+
require 'indented_io'
|
8
|
+
|
9
|
+
#$LOAD_PATH.unshift "../constrain/lib"
|
10
|
+
require 'constrain'
|
11
|
+
include Constrain
|
12
|
+
|
13
|
+
require 'ext/array.rb'
|
14
|
+
require 'ext/forward_to.rb'
|
15
|
+
require 'ext/lcs.rb'
|
16
|
+
include ForwardTo
|
17
|
+
|
18
|
+
require 'shellopts/version.rb'
|
19
|
+
|
20
|
+
require 'shellopts/stack.rb'
|
21
|
+
require 'shellopts/token.rb'
|
22
|
+
require 'shellopts/grammar.rb'
|
23
|
+
require 'shellopts/program.rb'
|
24
|
+
require 'shellopts/args.rb'
|
25
|
+
require 'shellopts/lexer.rb'
|
26
|
+
require 'shellopts/argument_type.rb'
|
4
27
|
require 'shellopts/parser.rb'
|
5
|
-
require 'shellopts/
|
6
|
-
require 'shellopts/
|
7
|
-
require 'shellopts/
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
# This module contains methods to process command line options and arguments.
|
15
|
-
# ShellOpts keeps a reference in ShellOpts.shellopts to the result of the last
|
16
|
-
# command that was processed through its interface and use it as the implicit
|
17
|
-
# object of many of its methods. This matches the typical use case where only
|
18
|
-
# one command line is ever processed and makes it possible to create class
|
19
|
-
# methods that knows about the command like #error and #fail
|
20
|
-
#
|
21
|
-
# For example; the following process and convert a command line into a struct
|
22
|
-
# representation and also sets ShellOpts.shellopts object so that the #error
|
23
|
-
# method can print a relevant spec string:
|
24
|
-
#
|
25
|
-
# USAGE = "a,all f,file=FILE -- ARG1 ARG2"
|
26
|
-
# opts, args = ShellOpts.as_struct(USAGE, ARGV)
|
27
|
-
# File.exist?(opts.file) or error "Can't find #{opts.file}"
|
28
|
-
#
|
29
|
-
# The command line is processed through one of the methods #process, #as_array,
|
30
|
-
# #as_hash, or #as_struct that returns a [data, args] tuple. The data type
|
31
|
-
# depends on the method: #process yields a Idr object that internally serves as
|
32
|
-
# the base for the #as_array and #as_hash and #as_struct that converts it into
|
33
|
-
# an Array, Hash, or ShellOpts::OptionStruct object. For example:
|
34
|
-
#
|
35
|
-
# USAGE = "..."
|
36
|
-
# ShellOpts.process(USAGE, ARGV)
|
37
|
-
# program, args = ShellOpts.as_program(USAGE, ARGV)
|
38
|
-
# array, args = ShellOpts.as_array(USAGE, ARGV)
|
39
|
-
# hash, args = ShellOpts.as_hash(USAGE, ARGV)
|
40
|
-
# struct, args = ShellOpts.as_struct(USAGE, ARGV)
|
41
|
-
#
|
42
|
-
# +args+ is a ShellOpts::Argv object containing the the remaning command line
|
43
|
-
# arguments. Argv is derived from Array
|
44
|
-
#
|
45
|
-
# ShellOpts can raise the exception CompilerError is there is an error in the
|
46
|
-
# USAGE string. If there is an error in the user supplied command line, #error
|
47
|
-
# is called instead and the program terminates with exit code 1. ShellOpts
|
48
|
-
# raises ConversionError is there is a name collision when converting to the
|
49
|
-
# hash or struct representations. Note that CompilerError and ConversionError
|
50
|
-
# are caused by misuse of the library and the problem should be corrected by
|
51
|
-
# the developer
|
52
|
-
#
|
53
|
-
# ShellOpts injects the constant PROGRAM into the global scope. It contains the
|
54
|
-
# name of the program
|
55
|
-
#
|
56
|
-
# INCLUDING SHELLOPTS
|
57
|
-
#
|
58
|
-
# ShellOpts can optionally be included in your shell application main file but
|
59
|
-
# it is not supposed to be included anywhere else
|
60
|
-
#
|
61
|
-
# Some behind the scenes magic happen if you include the ShellOpts module in your
|
62
|
-
# main exe file
|
63
|
-
#
|
28
|
+
require 'shellopts/analyzer.rb'
|
29
|
+
require 'shellopts/interpreter.rb'
|
30
|
+
require 'shellopts/ansi.rb'
|
31
|
+
require 'shellopts/renderer.rb'
|
32
|
+
require 'shellopts/formatter.rb'
|
33
|
+
require 'shellopts/dump.rb'
|
34
|
+
|
35
|
+
|
64
36
|
module ShellOpts
|
65
|
-
|
66
|
-
|
37
|
+
# Base error class
|
38
|
+
#
|
39
|
+
# Note that errors in the usage of the ShellOpts library are reported using
|
40
|
+
# standard exceptions
|
41
|
+
#
|
42
|
+
class ShellOptsError < StandardError
|
43
|
+
attr_reader :token
|
44
|
+
def initialize(token)
|
45
|
+
super
|
46
|
+
@token = token
|
47
|
+
end
|
67
48
|
end
|
68
49
|
|
69
|
-
|
70
|
-
|
71
|
-
|
50
|
+
# Raised on syntax errors on the command line (eg. unknown option). When
|
51
|
+
# ShellOpts handles the exception a message with the following format is
|
52
|
+
# printed on standard error:
|
53
|
+
#
|
54
|
+
# <program>: <message>
|
55
|
+
# Usage: <program> ...
|
56
|
+
#
|
57
|
+
class Error < ShellOptsError; end
|
58
|
+
|
59
|
+
# Default class for program failures. Failures are raised on missing files or
|
60
|
+
# illegal paths. When ShellOpts handles the exception a message with the
|
61
|
+
# following format is printed on standard error:
|
62
|
+
#
|
63
|
+
# <program>: <message>
|
64
|
+
#
|
65
|
+
class Failure < Error; end
|
66
|
+
|
67
|
+
# ShellOptsErrors during compilation. These errors are caused by syntax errors in the
|
68
|
+
# source. Messages are formatted as '<file> <lineno>:<charno> <message>' when
|
69
|
+
# handled by ShellOpts
|
70
|
+
class CompilerError < ShellOptsError; end
|
71
|
+
class LexerError < CompilerError; end
|
72
|
+
class ParserError < CompilerError; end
|
73
|
+
class AnalyzerError < CompilerError; end
|
74
|
+
|
75
|
+
# Internal errors. These are caused by bugs in the ShellOpts library
|
76
|
+
class InternalError < ShellOptsError; end
|
77
|
+
|
78
|
+
class ShellOpts
|
79
|
+
using Ext::Array::ShiftWhile
|
80
|
+
using Ext::Array::PopWhile
|
81
|
+
|
82
|
+
# Name of program. Defaults to the name of the executable
|
83
|
+
attr_reader :name
|
84
|
+
|
85
|
+
# Specification (String). Initialized by #compile
|
86
|
+
attr_reader :spec
|
87
|
+
|
88
|
+
# Array of arguments. Initialized by #interpret
|
89
|
+
attr_reader :argv
|
90
|
+
|
91
|
+
# Grammar. Grammar::Program object. Initialized by #compile
|
92
|
+
attr_reader :grammar
|
93
|
+
|
94
|
+
# Resulting ShellOpts::Program object containing options and optional
|
95
|
+
# subcommand. Initialized by #interpret
|
96
|
+
def program() @program end
|
97
|
+
|
98
|
+
# Array of remaining arguments. Initialized by #interpret
|
99
|
+
attr_reader :args
|
100
|
+
|
101
|
+
# Compiler flags
|
102
|
+
attr_accessor :stdopts
|
103
|
+
attr_accessor :msgopts
|
104
|
+
|
105
|
+
# Interpreter flags
|
106
|
+
attr_accessor :float
|
107
|
+
|
108
|
+
# True if ShellOpts lets exceptions through instead of writing an error
|
109
|
+
# message and exit
|
110
|
+
attr_accessor :exception
|
111
|
+
|
112
|
+
# File of source
|
113
|
+
attr_reader :file
|
114
|
+
|
115
|
+
# Debug: Internal variables made public
|
116
|
+
attr_reader :tokens
|
117
|
+
alias_method :ast, :grammar # Oops - defined earlier FIXME
|
118
|
+
|
119
|
+
def initialize(name: nil, stdopts: true, msgopts: false, float: true, exception: false)
|
120
|
+
@name = name || File.basename($PROGRAM_NAME)
|
121
|
+
@stdopts, @msgopts, @float, @exception = stdopts, msgopts, float, exception
|
122
|
+
end
|
72
123
|
|
73
|
-
|
74
|
-
|
75
|
-
|
124
|
+
# Compile source and return grammar object. Also sets #spec and #grammar.
|
125
|
+
# Returns the grammar
|
126
|
+
def compile(spec)
|
127
|
+
handle_exceptions {
|
128
|
+
@oneline = spec.index("\n").nil?
|
129
|
+
@spec = spec.sub(/^\s*\n/, "")
|
130
|
+
@file = find_caller_file
|
131
|
+
@tokens = Lexer.lex(name, @spec, @oneline)
|
132
|
+
ast = Parser.parse(tokens)
|
133
|
+
# TODO: Add standard and message options and their handlers
|
134
|
+
@grammar = Analyzer.analyze(ast)
|
135
|
+
}
|
136
|
+
self
|
137
|
+
end
|
76
138
|
|
77
|
-
|
78
|
-
|
79
|
-
|
139
|
+
# Use grammar to interpret arguments. Return a ShellOpts::Program and
|
140
|
+
# ShellOpts::Args tuple
|
141
|
+
#
|
142
|
+
def interpret(argv)
|
143
|
+
handle_exceptions {
|
144
|
+
@argv = argv.dup
|
145
|
+
@program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
|
146
|
+
}
|
147
|
+
self
|
148
|
+
end
|
80
149
|
|
81
|
-
|
82
|
-
|
83
|
-
|
150
|
+
# Compile +spec+ and interpret +argv+. Returns a tuple of a
|
151
|
+
# ShellOpts::Program and ShellOpts::Args object
|
152
|
+
#
|
153
|
+
def process(spec, argv)
|
154
|
+
compile(spec)
|
155
|
+
interpret(argv)
|
156
|
+
self
|
157
|
+
end
|
84
158
|
|
85
|
-
|
86
|
-
|
87
|
-
|
159
|
+
# Create a ShellOpts object and sets the global instance, then process the
|
160
|
+
# spec and arguments. Returns a tuple of a ShellOpts::Program with the
|
161
|
+
# options and subcommands and a ShellOpts::Args object with the remaining
|
162
|
+
# arguments
|
163
|
+
#
|
164
|
+
def self.process(spec, argv, **opts)
|
165
|
+
::ShellOpts.instance = shellopts = ShellOpts.new(**opts)
|
166
|
+
shellopts.process(spec, argv)
|
167
|
+
[shellopts.program, shellopts.args]
|
168
|
+
end
|
169
|
+
|
170
|
+
# Write short usage and error message to standard error and terminate
|
171
|
+
# program with status 1
|
172
|
+
#
|
173
|
+
# #error is supposed to be used when the user made an error and the usage
|
174
|
+
# is written to help correcting the error
|
175
|
+
#
|
176
|
+
def error(subject = nil, message)
|
177
|
+
saved = $stdout
|
178
|
+
$stdout = $stderr
|
179
|
+
$stderr.puts "#{name}: #{message}"
|
180
|
+
Formatter.usage(program)
|
181
|
+
exit 1
|
182
|
+
ensure
|
183
|
+
$stdout = saved
|
184
|
+
end
|
185
|
+
|
186
|
+
# Write error message to standard error and terminate program with status 1
|
187
|
+
#
|
188
|
+
# #failure is supposed to be used the used specified the correct arguments
|
189
|
+
# but something went wrong during processing. Since the used didn't cause
|
190
|
+
# the problem, only the error message is written
|
191
|
+
#
|
192
|
+
def failure(message)
|
193
|
+
$stderr.puts "#{name}: #{message}"
|
194
|
+
exit 1
|
195
|
+
end
|
88
196
|
|
89
|
-
|
90
|
-
|
91
|
-
def self.args() @args end
|
197
|
+
# Print usage
|
198
|
+
def usage() Formatter.usage(@grammar) end
|
92
199
|
|
93
|
-
|
94
|
-
|
200
|
+
# Print brief help
|
201
|
+
def brief() Formatter.brief(@grammar) end
|
95
202
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
203
|
+
# Print help for the given subject or the full documentation if +subject+
|
204
|
+
# is nil
|
205
|
+
#
|
206
|
+
def help(subject = nil)
|
207
|
+
node = (subject ? @grammar[subject] : @grammar) or
|
208
|
+
raise ArgumentError, "No such command: '#{subject&.sub(".", " ")}'"
|
209
|
+
Formatter.help(node)
|
101
210
|
end
|
102
|
-
end
|
103
211
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
# The current compilation object. It is set by #process
|
126
|
-
def self.shellopts() @shellopts end
|
127
|
-
|
128
|
-
# Name of program
|
129
|
-
def program_name() shellopts!.name end
|
130
|
-
def program_name=(name) shellopts!.name = name end
|
131
|
-
|
132
|
-
# Usage string
|
133
|
-
def usage() shellopts!.spec end
|
134
|
-
def usage=(spec) shellopts!.spec = spec end
|
135
|
-
|
136
|
-
# Process command line, set current shellopts object, and return it.
|
137
|
-
# Remaining arguments from the command line can be accessed through
|
138
|
-
# +shellopts.args+
|
139
|
-
def self.process(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
|
140
|
-
@shellopts.nil? or reset
|
141
|
-
@shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
|
142
|
-
@shellopts.process
|
143
|
-
end
|
212
|
+
def self.usage() ::ShellOpts.instance.usage end
|
213
|
+
def self.brief() ::ShellOpts.instance.brief end
|
214
|
+
def self.help(subject = nil) ::ShellOpts.instance.help(subject) end
|
215
|
+
|
216
|
+
private
|
217
|
+
def handle_exceptions(&block)
|
218
|
+
return yield if exception
|
219
|
+
begin
|
220
|
+
yield
|
221
|
+
rescue Error => ex
|
222
|
+
error(ex.message)
|
223
|
+
rescue Failure => ex
|
224
|
+
failure(ex.message)
|
225
|
+
rescue CompilerError => ex
|
226
|
+
filename = file =~ /\// ? file : "./#{file}"
|
227
|
+
lineno, charno = find_spec_in_file
|
228
|
+
charno = 1 if !@oneline
|
229
|
+
$stderr.puts "#{filename}:#{ex.token.pos(lineno, charno)} #{ex.message}"
|
230
|
+
exit(1)
|
231
|
+
end
|
232
|
+
end
|
144
233
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
|
149
|
-
Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
|
150
|
-
process(spec, argv, name: name, usage: usage)
|
151
|
-
@opts = shellopts.idr
|
152
|
-
@args = shellopts.args
|
153
|
-
[@opts, @args]
|
154
|
-
end
|
234
|
+
def find_caller_file
|
235
|
+
caller.reverse.select { |line| line !~ /^\s*#{__FILE__}:/ }.last.sub(/:.*/, "").sub(/^\.\//, "")
|
236
|
+
end
|
155
237
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
process(spec, argv, name: name, usage: usage)
|
162
|
-
@opts = shellopts.to_a
|
163
|
-
@args = shellopts.args
|
164
|
-
[@opts, @args]
|
165
|
-
end
|
238
|
+
def self.compare_lines(text, spec)
|
239
|
+
return true if text == spec
|
240
|
+
return true if text =~ /[#\$\\]/
|
241
|
+
false
|
242
|
+
end
|
166
243
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
244
|
+
public
|
245
|
+
# Find line and char index of spec in text. Returns [nil, nil] if not found
|
246
|
+
def self.find_spec_in_text(text, spec, oneline)
|
247
|
+
text_lines = text.split("\n")
|
248
|
+
spec_lines = spec.split("\n")
|
249
|
+
spec_lines.pop_while { |line| line =~ /^\s*$/ }
|
250
|
+
|
251
|
+
if oneline
|
252
|
+
line_i = nil
|
253
|
+
char_i = nil
|
254
|
+
char_z = 0
|
255
|
+
|
256
|
+
(0 ... text_lines.size).each { |text_i|
|
257
|
+
curr_char_i, curr_char_z =
|
258
|
+
LCS.find_longest_common_substring_index(text_lines[text_i], spec_lines.first.strip)
|
259
|
+
if curr_char_z > char_z
|
260
|
+
line_i = text_i
|
261
|
+
char_i = curr_char_i
|
262
|
+
char_z = curr_char_z
|
263
|
+
end
|
264
|
+
}
|
265
|
+
line_i ? [line_i, char_i] : [nil, nil]
|
266
|
+
else
|
267
|
+
spec_string = spec_lines.first.strip
|
268
|
+
line_i = (0 ... text_lines.size - spec_lines.size + 1).find { |text_i|
|
269
|
+
(0 ... spec_lines.size).all? { |spec_i|
|
270
|
+
compare_lines(text_lines[text_i + spec_i], spec_lines[spec_i])
|
271
|
+
}
|
272
|
+
} or return [nil, nil]
|
273
|
+
char_i, char_z =
|
274
|
+
LCS.find_longest_common_substring_index(text_lines[line_i], spec_lines.first.strip)
|
275
|
+
[line_i, char_i || 0]
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def find_spec_in_file
|
280
|
+
self.class.find_spec_in_text(IO.read(@file), @spec, @oneline).map { |i| (i || 0) + 1 }
|
281
|
+
end
|
282
|
+
|
283
|
+
def lookup(name)
|
284
|
+
a = name.split(".")
|
285
|
+
cmd = grammar
|
286
|
+
while element = a.shift
|
287
|
+
cmd = cmd.commands[element]
|
288
|
+
end
|
289
|
+
cmd
|
290
|
+
end
|
181
291
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
@args = shellopts.args
|
193
|
-
[@opts, @args]
|
292
|
+
def find_subject(obj)
|
293
|
+
case obj
|
294
|
+
when String; lookup(obj)
|
295
|
+
when Ast::Command; Command.grammar(obj) # FIXME
|
296
|
+
when Grammar::Command; obj
|
297
|
+
when NilClass; grammar
|
298
|
+
else
|
299
|
+
raise Internal, "Illegal object: #{obj.class}"
|
300
|
+
end
|
301
|
+
end
|
194
302
|
end
|
195
303
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
# argument. Automatically includes the ShellOpts module if called from the
|
200
|
-
# main Ruby object (ie. your executable)
|
201
|
-
def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
|
202
|
-
Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
|
203
|
-
process(spec, argv, name: name, usage: usage)
|
204
|
-
@opts = shellopts.to_a
|
205
|
-
@args = shellopts.args
|
206
|
-
shellopts.each(&block)
|
304
|
+
def self.process(spec, argv, msgopts: false, **opts)
|
305
|
+
msgopts ||= Messages.is_included?
|
306
|
+
ShellOpts.process(spec, argv, msgopts: msgopts, **opts)
|
207
307
|
end
|
208
308
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
def self.
|
213
|
-
|
309
|
+
@instance = nil
|
310
|
+
def self.instance?() !@instance.nil? end
|
311
|
+
def self.instance() @instance or raise Error, "ShellOpts is not initialized" end
|
312
|
+
def self.instance=(instance) @instance = instance end
|
313
|
+
|
314
|
+
forward_self_to :instance, :error, :failure
|
315
|
+
|
316
|
+
# The Include module brings the reporting methods into the namespace when
|
317
|
+
# included
|
318
|
+
module Messages
|
319
|
+
@is_included = false
|
320
|
+
def self.is_included?() @is_included end
|
321
|
+
def self.include(...)
|
322
|
+
@is_included = true
|
323
|
+
super
|
324
|
+
end
|
325
|
+
|
326
|
+
def notice(message)
|
327
|
+
$stderr.puts "#{name}: #{message}" if !quiet?
|
328
|
+
end
|
329
|
+
|
330
|
+
def mesg(message)
|
331
|
+
$stdout.puts message if !quiet?
|
332
|
+
end
|
333
|
+
|
334
|
+
def verb(level = 1, message)
|
335
|
+
$stdout.puts message if level <= @verbose
|
336
|
+
end
|
337
|
+
|
338
|
+
def debug(message)
|
339
|
+
$stdout.puts message if debug?
|
340
|
+
end
|
214
341
|
end
|
215
342
|
|
216
|
-
|
217
|
-
|
218
|
-
def self.fail(*msgs, exit: true)
|
219
|
-
shellopts!.fail(*msgs, exit: exit)
|
343
|
+
module ErrorHandling
|
344
|
+
# TODO: Set up global exception handlers
|
220
345
|
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
|
350
|
+
|
351
|
+
|
352
|
+
|
353
|
+
|
354
|
+
|
355
|
+
__END__
|
356
|
+
|
357
|
+
require "shellopts/version"
|
358
|
+
|
359
|
+
require "ext/algorithm.rb"
|
360
|
+
require "ext/ruby_env.rb"
|
361
|
+
|
362
|
+
require "shellopts/constants.rb"
|
363
|
+
require "shellopts/exceptions.rb"
|
364
|
+
|
365
|
+
require "shellopts/grammar/analyzer.rb"
|
366
|
+
require "shellopts/grammar/lexer.rb"
|
367
|
+
require "shellopts/grammar/parser.rb"
|
368
|
+
require "shellopts/grammar/command.rb"
|
369
|
+
require "shellopts/grammar/option.rb"
|
370
|
+
|
371
|
+
require "shellopts/ast/parser.rb"
|
372
|
+
require "shellopts/ast/command.rb"
|
373
|
+
require "shellopts/ast/option.rb"
|
374
|
+
|
375
|
+
require "shellopts/args.rb"
|
376
|
+
require "shellopts/formatter.rb"
|
221
377
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
378
|
+
if RUBY_ENV == "development"
|
379
|
+
require "shellopts/grammar/dump.rb"
|
380
|
+
require "shellopts/ast/dump.rb"
|
381
|
+
end
|
382
|
+
|
383
|
+
$verb = nil
|
384
|
+
$quiet = nil
|
385
|
+
$shellopts = nil
|
386
|
+
|
387
|
+
module ShellOpts
|
388
|
+
class ShellOpts
|
389
|
+
attr_reader :name # Name of program. Defaults to the name of the executable
|
390
|
+
attr_reader :spec
|
391
|
+
attr_reader :argv
|
392
|
+
|
393
|
+
attr_reader :grammar
|
394
|
+
attr_reader :program
|
395
|
+
attr_reader :arguments
|
396
|
+
|
397
|
+
def initialize(spec, argv, name: nil, exception: false)
|
398
|
+
@name = name || File.basename($PROGRAM_NAME)
|
399
|
+
@spec, @argv = spec, argv.dup
|
400
|
+
exprs = Grammar::Lexer.lex(@spec)
|
401
|
+
commands = Grammar::Parser.parse(@name, exprs)
|
402
|
+
@grammar = Grammar::Analyzer.analyze(commands)
|
403
|
+
|
404
|
+
begin
|
405
|
+
@program, @arguments = Ast::Parser.parse(@grammar, @argv)
|
406
|
+
rescue Error => ex
|
407
|
+
raise if exception
|
408
|
+
error(ex.subject, ex.message)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def error(subject = nil, message)
|
413
|
+
$stderr.puts "#{name}: #{message}"
|
414
|
+
usage(subject, device: $stderr)
|
415
|
+
exit 1
|
416
|
+
end
|
417
|
+
|
418
|
+
def fail(message)
|
419
|
+
$stderr.puts "#{name}: #{message}"
|
420
|
+
exit 1
|
421
|
+
end
|
422
|
+
|
423
|
+
def usage(subject = nil, device: $stdout, levels: 1, margin: "")
|
424
|
+
subject = find_subject(subject)
|
425
|
+
device.puts Formatter.usage_string(subject, levels: levels, margin: margin)
|
426
|
+
end
|
427
|
+
|
428
|
+
def help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
|
429
|
+
subject = find_subject(subject)
|
430
|
+
device.puts Formatter.help_string(subject, levels: levels, margin: margin, tab: tab)
|
431
|
+
end
|
432
|
+
|
433
|
+
private
|
434
|
+
def lookup(name)
|
435
|
+
a = name.split(".")
|
436
|
+
cmd = grammar
|
437
|
+
while element = a.shift
|
438
|
+
cmd = cmd.commands[element]
|
439
|
+
end
|
440
|
+
cmd
|
441
|
+
end
|
442
|
+
|
443
|
+
def find_subject(obj)
|
444
|
+
case obj
|
445
|
+
when String; lookup(obj)
|
446
|
+
when Ast::Command; Command.grammar(obj)
|
447
|
+
when Grammar::Command; obj
|
448
|
+
when NilClass; grammar
|
449
|
+
else
|
450
|
+
raise Internal, "Illegal object: #{obj.class}"
|
235
451
|
end
|
236
452
|
end
|
237
|
-
super
|
238
453
|
end
|
239
454
|
|
240
|
-
|
241
|
-
|
242
|
-
|
455
|
+
def self.process(spec, argv, name: nil, exception: false)
|
456
|
+
$shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
|
457
|
+
[$shellopts.program, $shellopts.arguments]
|
458
|
+
end
|
243
459
|
|
244
|
-
|
245
|
-
|
246
|
-
@shellopts = nil
|
460
|
+
def self.error(subject = nil, message)
|
461
|
+
$shellopts.error(subject, message)
|
247
462
|
end
|
248
463
|
|
249
|
-
|
250
|
-
|
251
|
-
::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
|
464
|
+
def self.fail(message)
|
465
|
+
$shellopts.fail(message)
|
252
466
|
end
|
253
467
|
|
254
|
-
|
255
|
-
|
468
|
+
def self.help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
|
469
|
+
$shellopts.help(subject, device: device, levels: levels, margin: margin, tab: tab)
|
470
|
+
end
|
471
|
+
|
472
|
+
def self.usage(subject = nil, device: $stdout, levels: 1, margin: "")
|
473
|
+
$shellopts.usage(subject, device: device, levels: levels, margin: margin)
|
474
|
+
end
|
256
475
|
end
|
257
476
|
|