shellopts 2.0.0.pre.14 → 2.0.2
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 +29 -21
- 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 +360 -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,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. Ie. an Integer object
|
18
|
+
# if type is an IntegerArgument
|
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) value?(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
|
+
|