shellopts 2.0.0.pre.14 → 2.0.0
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 +37 -5
- 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 +62 -0
- 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 +25 -15
- data/lib/shellopts/argument_type.rb +139 -0
- data/lib/shellopts/dump.rb +158 -0
- data/lib/shellopts/formatter.rb +292 -92
- 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 +293 -0
- 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 +1 -1
- data/lib/shellopts.rb +359 -3
- data/main +1180 -0
- data/shellopts.gemspec +8 -14
- metadata +86 -41
- data/lib/ext/algorithm.rb +0 -14
- data/lib/ext/ruby_env.rb +0 -8
- data/lib/shellopts/ast/command.rb +0 -112
- data/lib/shellopts/ast/dump.rb +0 -28
- data/lib/shellopts/ast/option.rb +0 -15
- data/lib/shellopts/ast/parser.rb +0 -106
- data/lib/shellopts/constants.rb +0 -88
- data/lib/shellopts/exceptions.rb +0 -21
- data/lib/shellopts/grammar/analyzer.rb +0 -76
- data/lib/shellopts/grammar/command.rb +0 -87
- data/lib/shellopts/grammar/dump.rb +0 -56
- data/lib/shellopts/grammar/lexer.rb +0 -56
- data/lib/shellopts/grammar/option.rb +0 -55
- data/lib/shellopts/grammar/parser.rb +0 -78
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'terminfo'
|
2
|
+
|
3
|
+
# Option rendering
|
4
|
+
# -a, --all # Only used in brief and doc formats (enum)
|
5
|
+
# --all # Only used in usage (long)
|
6
|
+
# -a # Only used in usage (short)
|
7
|
+
#
|
8
|
+
# Option group rendering
|
9
|
+
# -a, --all -b, --beta # Only used in brief formats (enum)
|
10
|
+
# --all --beta # Used in usage (long)
|
11
|
+
# -a -b # Used in usage (short)
|
12
|
+
#
|
13
|
+
# -a, --all # Only used in doc format (:multi)
|
14
|
+
# -b, --beta
|
15
|
+
#
|
16
|
+
# Command rendering
|
17
|
+
# cmd --all --beta [cmd1|cmd2] ARG1 ARG2 # Single-line formats (:single)
|
18
|
+
# cmd --all --beta [cmd1|cmd2] ARGS...
|
19
|
+
# cmd -a -b [cmd1|cmd2] ARG1 ARG2
|
20
|
+
# cmd -a -b [cmd1|cmd2] ARGS...
|
21
|
+
#
|
22
|
+
# cmd -a -b [cmd1|cmd2] ARG1 ARG2 # One line for each argument description (:enum)
|
23
|
+
# cmd -a -b [cmd1|cmd2] ARG3 ARG4 # (used in the USAGE section)
|
24
|
+
#
|
25
|
+
# cmd --all --beta # Multi-line formats (:multi)
|
26
|
+
# [cmd1|cmd2] ARG1 ARG2
|
27
|
+
# cmd --all --beta
|
28
|
+
# <commands> ARGS
|
29
|
+
#
|
30
|
+
module ShellOpts
|
31
|
+
module Grammar
|
32
|
+
class Option
|
33
|
+
# Formats:
|
34
|
+
#
|
35
|
+
# :enum -a, --all
|
36
|
+
# :long --all
|
37
|
+
# :short -a
|
38
|
+
#
|
39
|
+
def render(format)
|
40
|
+
constrain format, :enum, :long, :short
|
41
|
+
case format
|
42
|
+
when :enum; names.join(", ")
|
43
|
+
when :long; name
|
44
|
+
when :short; short_names.first || name
|
45
|
+
else
|
46
|
+
raise ArgumentError, "Illegal format: #{format.inspect}"
|
47
|
+
end + (argument? ? "=#{argument_name}" : "")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class OptionGroup
|
52
|
+
# Formats:
|
53
|
+
#
|
54
|
+
# :enum -a, --all -r, --recursive
|
55
|
+
# :long --all --recursive
|
56
|
+
# :short -a -r
|
57
|
+
# :multi -a, --all
|
58
|
+
# -r, --recursive
|
59
|
+
#
|
60
|
+
def render(format)
|
61
|
+
constrain format, :enum, :long, :short, :multi
|
62
|
+
if format == :multi
|
63
|
+
options.map { |option| option.render(:enum) }.join("\n")
|
64
|
+
else
|
65
|
+
options.map { |option| option.render(format) }.join(" ")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# brief one-line commands should optionally use compact options
|
71
|
+
class Command
|
72
|
+
using Ext::Array::Wrap
|
73
|
+
|
74
|
+
OPTIONS_ABBR = "[OPTIONS]"
|
75
|
+
COMMANDS_ABBR = "[COMMANDS]"
|
76
|
+
DESCRS_ABBR = "ARGS..."
|
77
|
+
|
78
|
+
# Format can be one of :single, :enum, or :multi. :single force one-line
|
79
|
+
# output and compacts options and commands if needed. :enum outputs a
|
80
|
+
# :single line for each argument specification/description, :multi tries
|
81
|
+
# one-line output but wrap options if needed. Multiple argument
|
82
|
+
# specifications/descriptions are always compacted
|
83
|
+
#
|
84
|
+
def render(format, width, root: false, **opts)
|
85
|
+
case format
|
86
|
+
when :single; render_single(width, **opts)
|
87
|
+
when :enum; render_enum(width, **opts)
|
88
|
+
when :multi; render_multi2(width, **opts)
|
89
|
+
else
|
90
|
+
raise ArgumentError, "Illegal format: #{format.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def names(root: false)
|
95
|
+
(root ? ancestors : []) + [self]
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
# Force one line. Compact options, commands, arguments if needed
|
100
|
+
def render_single(width, args: nil)
|
101
|
+
long_options = options.map { |option| option.render(:long) }
|
102
|
+
short_options = options.map { |option| option.render(:short) }
|
103
|
+
compact_options = options.empty? ? [] : [OPTIONS_ABBR]
|
104
|
+
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
105
|
+
compact_commands = commands.empty? ? [] : [COMMANDS_ABBR]
|
106
|
+
|
107
|
+
# TODO: Refactor and implement recursive detection of any argument
|
108
|
+
args ||=
|
109
|
+
case descrs.size
|
110
|
+
when 0; args = []
|
111
|
+
when 1; [descrs.first.text]
|
112
|
+
else [DESCRS_ABBR]
|
113
|
+
end
|
114
|
+
|
115
|
+
begin # to be able to use 'break' below
|
116
|
+
words = [name] + long_options + short_commands + args
|
117
|
+
break if pass?(words, width)
|
118
|
+
words = [name] + short_options + short_commands + args
|
119
|
+
break if pass?(words, width)
|
120
|
+
words = [name] + long_options + compact_commands + args
|
121
|
+
break if pass?(words, width)
|
122
|
+
words = [name] + short_options + compact_commands + args
|
123
|
+
break if pass?(words, width)
|
124
|
+
words = [name] + compact_options + short_commands + args
|
125
|
+
break if pass?(words, width)
|
126
|
+
words = [name] + compact_options + compact_commands + args
|
127
|
+
break if pass?(words, width)
|
128
|
+
words = [name] + compact_options + compact_commands + [DESCRS_ABBR]
|
129
|
+
end while false
|
130
|
+
words.join(" ")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Render one line for each argument specification/description
|
134
|
+
def render_enum(width)
|
135
|
+
# TODO: Also refactor args here
|
136
|
+
args_texts = self.descrs.empty? ? [""] : descrs.map(&:text)
|
137
|
+
args_texts.map { |args_text| render_single(width, args: [args_text]) }
|
138
|
+
end
|
139
|
+
|
140
|
+
# Render the description using the given method (:single, :multi)
|
141
|
+
def render_descr(method, width, descr)
|
142
|
+
send.send method, width, args: descr
|
143
|
+
end
|
144
|
+
|
145
|
+
# Try to keep on one line but wrap options if needed. Multiple argument
|
146
|
+
# specifications/descriptions are always compacted
|
147
|
+
def render_multi(width, args: nil)
|
148
|
+
long_options = options.map { |option| option.render(:long) }
|
149
|
+
short_options = options.map { |option| option.render(:short) }
|
150
|
+
short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
|
151
|
+
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
|
+
|
175
|
+
# TODO: Refactor and implement recursive detection of any argument
|
176
|
+
args ||=
|
177
|
+
case descrs.size
|
178
|
+
when 0; args = []
|
179
|
+
when 1; [descrs.first.text]
|
180
|
+
else [DESCRS_ABBR]
|
181
|
+
end
|
182
|
+
|
183
|
+
# On one line
|
184
|
+
words = [name] + long_options + short_commands + args
|
185
|
+
return [words.join(" ")] if pass?(words, width)
|
186
|
+
words = [name] + short_options + short_commands + args
|
187
|
+
return [words.join(" ")] if pass?(words, width)
|
188
|
+
|
189
|
+
# On multiple lines
|
190
|
+
lead = name + " "
|
191
|
+
options = long_options.wrap(width - lead.size)
|
192
|
+
options = [lead + options[0]] + indent_lines(lead.size, options[1..-1])
|
193
|
+
|
194
|
+
begin
|
195
|
+
words = short_commands + args
|
196
|
+
break if pass?(words, width)
|
197
|
+
words = compact_commands + args
|
198
|
+
break if pass?(words, width)
|
199
|
+
words = compact_commands + [DESCRS_ABBR]
|
200
|
+
end while false
|
201
|
+
|
202
|
+
cmdargs = words.empty? ? [] : [words.join(" ")]
|
203
|
+
options + indent_lines(lead.size, cmdargs)
|
204
|
+
end
|
205
|
+
|
206
|
+
protected
|
207
|
+
# Helper method that returns true if words can fit in width characters
|
208
|
+
def pass?(words, width)
|
209
|
+
words.sum(&:size) + words.size - 1 <= width
|
210
|
+
end
|
211
|
+
|
212
|
+
# Indent array of lines
|
213
|
+
def indent_lines(indent, lines)
|
214
|
+
indent = [indent, 0].max
|
215
|
+
lines.map { |line| ' ' * indent + line }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
|
225
|
+
|
226
|
+
|
227
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
module ShellOpts
|
3
|
+
class Token
|
4
|
+
# Each kind should have a corresponding Grammar class with the same name
|
5
|
+
KINDS = [
|
6
|
+
:program, :section, :option, :command, :spec, :argument, :usage,
|
7
|
+
:usage_string, :brief, :text, :blank
|
8
|
+
]
|
9
|
+
|
10
|
+
# Kind of token
|
11
|
+
attr_reader :kind
|
12
|
+
|
13
|
+
# Line number (one-based)
|
14
|
+
attr_reader :lineno
|
15
|
+
|
16
|
+
# Char number (one-based). The lexer may adjust the char number (eg. for
|
17
|
+
# blank lines)
|
18
|
+
attr_accessor :charno
|
19
|
+
|
20
|
+
# Source of the token
|
21
|
+
attr_reader :source
|
22
|
+
|
23
|
+
def initialize(kind, lineno, charno, source)
|
24
|
+
constrain kind, :program, *KINDS
|
25
|
+
@kind, @lineno, @charno, @source = kind, lineno, charno, source
|
26
|
+
end
|
27
|
+
|
28
|
+
forward_to :source, :to_s, :empty?
|
29
|
+
|
30
|
+
def pos(start_lineno = 1, start_charno = 1)
|
31
|
+
"#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s() source end
|
35
|
+
|
36
|
+
def inspect()
|
37
|
+
"<#{self.class.to_s.sub(/.*::/, "")} #{pos} #{kind.inspect} #{source.inspect}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
def dump
|
41
|
+
puts "#{kind}@#{lineno}:#{charno} #{source.inspect}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/shellopts/version.rb
CHANGED
data/lib/shellopts.rb
CHANGED
@@ -1,3 +1,358 @@
|
|
1
|
+
|
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/lexer.rb'
|
25
|
+
require 'shellopts/argument_type.rb'
|
26
|
+
require 'shellopts/parser.rb'
|
27
|
+
require 'shellopts/analyzer.rb'
|
28
|
+
require 'shellopts/interpreter.rb'
|
29
|
+
require 'shellopts/ansi.rb'
|
30
|
+
require 'shellopts/renderer.rb'
|
31
|
+
require 'shellopts/formatter.rb'
|
32
|
+
require 'shellopts/dump.rb'
|
33
|
+
|
34
|
+
|
35
|
+
module ShellOpts
|
36
|
+
# Base error class
|
37
|
+
#
|
38
|
+
# Note that errors in the usage of the ShellOpts library are reported using
|
39
|
+
# standard exceptions
|
40
|
+
#
|
41
|
+
class ShellOptsError < StandardError
|
42
|
+
attr_reader :token
|
43
|
+
def initialize(token)
|
44
|
+
super
|
45
|
+
@token = token
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Raised on syntax errors on the command line (eg. unknown option). When
|
50
|
+
# ShellOpts handles the exception a message with the following format is
|
51
|
+
# printed on standard error:
|
52
|
+
#
|
53
|
+
# <program>: <message>
|
54
|
+
# Usage: <program> ...
|
55
|
+
#
|
56
|
+
class Error < ShellOptsError; end
|
57
|
+
|
58
|
+
# Default class for program failures. Failures are raised on missing files or
|
59
|
+
# illegal paths. When ShellOpts handles the exception a message with the
|
60
|
+
# following format is printed on standard error:
|
61
|
+
#
|
62
|
+
# <program>: <message>
|
63
|
+
#
|
64
|
+
class Failure < Error; end
|
65
|
+
|
66
|
+
# ShellOptsErrors during compilation. These errors are caused by syntax errors in the
|
67
|
+
# source. Messages are formatted as '<file> <lineno>:<charno> <message>' when
|
68
|
+
# handled by ShellOpts
|
69
|
+
class CompilerError < ShellOptsError; end
|
70
|
+
class LexerError < CompilerError; end
|
71
|
+
class ParserError < CompilerError; end
|
72
|
+
class AnalyzerError < CompilerError; end
|
73
|
+
|
74
|
+
# Internal errors. These are caused by bugs in the ShellOpts library
|
75
|
+
class InternalError < ShellOptsError; end
|
76
|
+
|
77
|
+
class ShellOpts
|
78
|
+
using Ext::Array::ShiftWhile
|
79
|
+
using Ext::Array::PopWhile
|
80
|
+
|
81
|
+
# Name of program. Defaults to the name of the executable
|
82
|
+
attr_reader :name
|
83
|
+
|
84
|
+
# Specification (String). Initialized by #compile
|
85
|
+
attr_reader :spec
|
86
|
+
|
87
|
+
# Array of arguments. Initialized by #interpret
|
88
|
+
attr_reader :argv
|
89
|
+
|
90
|
+
# Grammar. Grammar::Program object. Initialized by #compile
|
91
|
+
attr_reader :grammar
|
92
|
+
|
93
|
+
# Resulting ShellOpts::Program object containing options and optional
|
94
|
+
# subcommand. Initialized by #interpret
|
95
|
+
def program() @program end
|
96
|
+
|
97
|
+
# Array of remaining arguments. Initialized by #interpret
|
98
|
+
attr_reader :args
|
99
|
+
|
100
|
+
# Compiler flags
|
101
|
+
attr_accessor :stdopts
|
102
|
+
attr_accessor :msgopts
|
103
|
+
|
104
|
+
# Interpreter flags
|
105
|
+
attr_accessor :float
|
106
|
+
|
107
|
+
# True if ShellOpts lets exceptions through instead of writing an error
|
108
|
+
# message and exit
|
109
|
+
attr_accessor :exception
|
110
|
+
|
111
|
+
# File of source
|
112
|
+
attr_reader :file
|
113
|
+
|
114
|
+
# Debug: Internal variables made public
|
115
|
+
attr_reader :tokens
|
116
|
+
alias_method :ast, :grammar # Oops - defined earlier FIXME
|
117
|
+
|
118
|
+
def initialize(name: nil, stdopts: true, msgopts: false, float: true, exception: false)
|
119
|
+
@name = name || File.basename($PROGRAM_NAME)
|
120
|
+
@stdopts, @msgopts, @float, @exception = stdopts, msgopts, float, exception
|
121
|
+
end
|
122
|
+
|
123
|
+
# Compile source and return grammar object. Also sets #spec and #grammar.
|
124
|
+
# Returns the grammar
|
125
|
+
def compile(spec)
|
126
|
+
handle_exceptions {
|
127
|
+
@oneline = spec.index("\n").nil?
|
128
|
+
@spec = spec.sub(/^\s*\n/, "")
|
129
|
+
@file = find_caller_file
|
130
|
+
@tokens = Lexer.lex(name, @spec, @oneline)
|
131
|
+
ast = Parser.parse(tokens)
|
132
|
+
# TODO: Add standard and message options and their handlers
|
133
|
+
@grammar = Analyzer.analyze(ast)
|
134
|
+
}
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Use grammar to interpret arguments. Return a ShellOpts::Program and
|
139
|
+
# ShellOpts::Args tuple
|
140
|
+
#
|
141
|
+
def interpret(argv)
|
142
|
+
handle_exceptions {
|
143
|
+
@argv = argv.dup
|
144
|
+
@program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
|
145
|
+
}
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
# Compile +spec+ and interpret +argv+. Returns a tuple of a
|
150
|
+
# ShellOpts::Program and ShellOpts::Args object
|
151
|
+
#
|
152
|
+
def process(spec, argv)
|
153
|
+
compile(spec)
|
154
|
+
interpret(argv)
|
155
|
+
self
|
156
|
+
end
|
157
|
+
|
158
|
+
# Create a ShellOpts object and sets the global instance, then process the
|
159
|
+
# spec and arguments. Returns a tuple of a ShellOpts::Program with the
|
160
|
+
# options and subcommands and a ShellOpts::Args object with the remaining
|
161
|
+
# arguments
|
162
|
+
#
|
163
|
+
def self.process(spec, argv, **opts)
|
164
|
+
::ShellOpts.instance = shellopts = ShellOpts.new(**opts)
|
165
|
+
shellopts.process(spec, argv)
|
166
|
+
[shellopts.program, shellopts.argv]
|
167
|
+
end
|
168
|
+
|
169
|
+
# Write short usage and error message to standard error and terminate
|
170
|
+
# program with status 1
|
171
|
+
#
|
172
|
+
# #error is supposed to be used when the user made an error and the usage
|
173
|
+
# is written to help correcting the error
|
174
|
+
#
|
175
|
+
def error(subject = nil, message)
|
176
|
+
saved = $stdout
|
177
|
+
$stdout = $stderr
|
178
|
+
$stderr.puts "#{name}: #{message}"
|
179
|
+
Formatter.usage(program)
|
180
|
+
exit 1
|
181
|
+
ensure
|
182
|
+
$stdout = saved
|
183
|
+
end
|
184
|
+
|
185
|
+
# Write error message to standard error and terminate program with status 1
|
186
|
+
#
|
187
|
+
# #failure is supposed to be used the used specified the correct arguments
|
188
|
+
# but something went wrong during processing. Since the used didn't cause
|
189
|
+
# the problem, only the error message is written
|
190
|
+
#
|
191
|
+
def failure(message)
|
192
|
+
$stderr.puts "#{name}: #{message}"
|
193
|
+
exit 1
|
194
|
+
end
|
195
|
+
|
196
|
+
# Print usage
|
197
|
+
def usage() Formatter.usage(@grammar) end
|
198
|
+
|
199
|
+
# Print brief help
|
200
|
+
def brief() Formatter.brief(@grammar) end
|
201
|
+
|
202
|
+
# Print help for the given subject or the full documentation if +subject+
|
203
|
+
# is nil
|
204
|
+
#
|
205
|
+
def help(subject = nil)
|
206
|
+
node = (subject ? @grammar[subject] : @grammar) or
|
207
|
+
raise ArgumentError, "No such command: '#{subject&.sub(".", " ")}'"
|
208
|
+
Formatter.help(node)
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.usage() ::ShellOpts.instance.usage end
|
212
|
+
def self.brief() ::ShellOpts.instance.brief end
|
213
|
+
def self.help(subject = nil) ::ShellOpts.instance.help(subject) end
|
214
|
+
|
215
|
+
private
|
216
|
+
def handle_exceptions(&block)
|
217
|
+
return yield if exception
|
218
|
+
begin
|
219
|
+
yield
|
220
|
+
rescue Error => ex
|
221
|
+
error(ex.message)
|
222
|
+
rescue Failure => ex
|
223
|
+
failure(ex.message)
|
224
|
+
rescue CompilerError => ex
|
225
|
+
filename = file =~ /\// ? file : "./#{file}"
|
226
|
+
lineno, charno = find_spec_in_file
|
227
|
+
charno = 1 if !@oneline
|
228
|
+
$stderr.puts "#{filename}:#{ex.token.pos(lineno, charno)} #{ex.message}"
|
229
|
+
exit(1)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def find_caller_file
|
234
|
+
caller.reverse.select { |line| line !~ /^\s*#{__FILE__}:/ }.last.sub(/:.*/, "").sub(/^\.\//, "")
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.compare_lines(text, spec)
|
238
|
+
return true if text == spec
|
239
|
+
return true if text =~ /[#\$\\]/
|
240
|
+
false
|
241
|
+
end
|
242
|
+
|
243
|
+
public
|
244
|
+
# Find line and char index of spec in text. Returns [nil, nil] if not found
|
245
|
+
def self.find_spec_in_text(text, spec, oneline)
|
246
|
+
text_lines = text.split("\n")
|
247
|
+
spec_lines = spec.split("\n")
|
248
|
+
spec_lines.pop_while { |line| line =~ /^\s*$/ }
|
249
|
+
|
250
|
+
if oneline
|
251
|
+
line_i = nil
|
252
|
+
char_i = nil
|
253
|
+
char_z = 0
|
254
|
+
|
255
|
+
(0 ... text_lines.size).each { |text_i|
|
256
|
+
curr_char_i, curr_char_z =
|
257
|
+
LCS.find_longest_common_substring_index(text_lines[text_i], spec_lines.first.strip)
|
258
|
+
if curr_char_z > char_z
|
259
|
+
line_i = text_i
|
260
|
+
char_i = curr_char_i
|
261
|
+
char_z = curr_char_z
|
262
|
+
end
|
263
|
+
}
|
264
|
+
line_i ? [line_i, char_i] : [nil, nil]
|
265
|
+
else
|
266
|
+
spec_string = spec_lines.first.strip
|
267
|
+
line_i = (0 ... text_lines.size - spec_lines.size + 1).find { |text_i|
|
268
|
+
(0 ... spec_lines.size).all? { |spec_i|
|
269
|
+
compare_lines(text_lines[text_i + spec_i], spec_lines[spec_i])
|
270
|
+
}
|
271
|
+
} or return [nil, nil]
|
272
|
+
char_i, char_z =
|
273
|
+
LCS.find_longest_common_substring_index(text_lines[line_i], spec_lines.first.strip)
|
274
|
+
[line_i, char_i || 0]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def find_spec_in_file
|
279
|
+
self.class.find_spec_in_text(IO.read(@file), @spec, @oneline).map { |i| (i || 0) + 1 }
|
280
|
+
end
|
281
|
+
|
282
|
+
def lookup(name)
|
283
|
+
a = name.split(".")
|
284
|
+
cmd = grammar
|
285
|
+
while element = a.shift
|
286
|
+
cmd = cmd.commands[element]
|
287
|
+
end
|
288
|
+
cmd
|
289
|
+
end
|
290
|
+
|
291
|
+
def find_subject(obj)
|
292
|
+
case obj
|
293
|
+
when String; lookup(obj)
|
294
|
+
when Ast::Command; Command.grammar(obj) # FIXME
|
295
|
+
when Grammar::Command; obj
|
296
|
+
when NilClass; grammar
|
297
|
+
else
|
298
|
+
raise Internal, "Illegal object: #{obj.class}"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def self.process(spec, argv, msgopts: false, **opts)
|
304
|
+
msgopts ||= Messages.is_included?
|
305
|
+
ShellOpts.process(spec, argv, msgopts: msgopts, **opts)
|
306
|
+
end
|
307
|
+
|
308
|
+
@instance = nil
|
309
|
+
def self.instance?() !@instance.nil? end
|
310
|
+
def self.instance() @instance or raise Error, "ShellOpts is not initialized" end
|
311
|
+
def self.instance=(instance) @instance = instance end
|
312
|
+
|
313
|
+
forward_self_to :instance, :error, :failure
|
314
|
+
|
315
|
+
# The Include module brings the reporting methods into the namespace when
|
316
|
+
# included
|
317
|
+
module Messages
|
318
|
+
@is_included = false
|
319
|
+
def self.is_included?() @is_included end
|
320
|
+
def self.include(...)
|
321
|
+
@is_included = true
|
322
|
+
super
|
323
|
+
end
|
324
|
+
|
325
|
+
def notice(message)
|
326
|
+
$stderr.puts "#{name}: #{message}" if !quiet?
|
327
|
+
end
|
328
|
+
|
329
|
+
def mesg(message)
|
330
|
+
$stdout.puts message if !quiet?
|
331
|
+
end
|
332
|
+
|
333
|
+
def verb(level = 1, message)
|
334
|
+
$stdout.puts message if level <= @verbose
|
335
|
+
end
|
336
|
+
|
337
|
+
def debug(message)
|
338
|
+
$stdout.puts message if debug?
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
module ErrorHandling
|
343
|
+
# TODO: Set up global exception handlers
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
|
348
|
+
|
349
|
+
|
350
|
+
|
351
|
+
|
352
|
+
|
353
|
+
|
354
|
+
__END__
|
355
|
+
|
1
356
|
require "shellopts/version"
|
2
357
|
|
3
358
|
require "ext/algorithm.rb"
|
@@ -38,7 +393,7 @@ module ShellOpts
|
|
38
393
|
attr_reader :program
|
39
394
|
attr_reader :arguments
|
40
395
|
|
41
|
-
def initialize(spec, argv, name: nil)
|
396
|
+
def initialize(spec, argv, name: nil, exception: false)
|
42
397
|
@name = name || File.basename($PROGRAM_NAME)
|
43
398
|
@spec, @argv = spec, argv.dup
|
44
399
|
exprs = Grammar::Lexer.lex(@spec)
|
@@ -48,6 +403,7 @@ module ShellOpts
|
|
48
403
|
begin
|
49
404
|
@program, @arguments = Ast::Parser.parse(@grammar, @argv)
|
50
405
|
rescue Error => ex
|
406
|
+
raise if exception
|
51
407
|
error(ex.subject, ex.message)
|
52
408
|
end
|
53
409
|
end
|
@@ -95,8 +451,8 @@ module ShellOpts
|
|
95
451
|
end
|
96
452
|
end
|
97
453
|
|
98
|
-
def self.process(spec, argv, name: nil)
|
99
|
-
$shellopts = ShellOpts.new(spec, argv, name: name)
|
454
|
+
def self.process(spec, argv, name: nil, exception: false)
|
455
|
+
$shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
|
100
456
|
[$shellopts.program, $shellopts.arguments]
|
101
457
|
end
|
102
458
|
|