shellopts 2.0.0.pre.13 → 2.0.0.pre.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/TODO +15 -135
- data/lib/ext/algorithm.rb +14 -0
- data/lib/ext/ruby_env.rb +8 -0
- data/lib/shellopts.rb +90 -228
- data/lib/shellopts/args.rb +1 -1
- data/lib/shellopts/ast/command.rb +101 -30
- data/lib/shellopts/ast/dump.rb +28 -0
- data/lib/shellopts/ast/option.rb +8 -14
- data/lib/shellopts/ast/parser.rb +106 -0
- data/lib/shellopts/constants.rb +88 -0
- data/lib/shellopts/exceptions.rb +21 -0
- data/lib/shellopts/formatter.rb +125 -0
- data/lib/shellopts/grammar/analyzer.rb +76 -0
- data/lib/shellopts/grammar/command.rb +67 -60
- data/lib/shellopts/grammar/dump.rb +56 -0
- data/lib/shellopts/grammar/lexer.rb +56 -0
- data/lib/shellopts/grammar/option.rb +49 -60
- data/lib/shellopts/grammar/parser.rb +78 -0
- data/lib/shellopts/version.rb +2 -2
- data/shellopts.gemspec +1 -1
- metadata +13 -15
- data/lib/ext/array.rb +0 -9
- data/lib/main.rb +0 -1
- data/lib/shellopts/ast/node.rb +0 -37
- 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/node.rb +0 -33
- 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/parser.rb +0 -106
- data/lib/shellopts/shellopts.rb +0 -123
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
module ShellOpts
|
3
|
+
module Grammar
|
4
|
+
class Analyzer
|
5
|
+
def self.analyze(commands)
|
6
|
+
@program = commands.shift
|
7
|
+
@commands = commands
|
8
|
+
build_options
|
9
|
+
link_up
|
10
|
+
@program
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def self.error(mesg, command)
|
15
|
+
mesg += " in #{command.path}" if !command.program?
|
16
|
+
raise CompileError, mesg
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.program() @program end
|
20
|
+
def self.commands() @commands end
|
21
|
+
|
22
|
+
# Initialize Command#options
|
23
|
+
def self.build_options
|
24
|
+
([program] + commands).each { |command|
|
25
|
+
command.opts.each { |opt|
|
26
|
+
opt.names.each { |name|
|
27
|
+
!command.options.key?(name) or
|
28
|
+
error "Duplicate option name '#{name}'", command
|
29
|
+
command.options[name] = opt
|
30
|
+
}
|
31
|
+
|
32
|
+
!command.options.key?(opt.ident) or
|
33
|
+
error "Duplicate option identifier '#{opt.ident}'", command
|
34
|
+
command.options[opt.ident] = opt
|
35
|
+
}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize Command#commands
|
40
|
+
def self.link_up
|
41
|
+
# Hash from path to command
|
42
|
+
cmds = { "" => program }
|
43
|
+
|
44
|
+
# Add placeholders for actual commands and virtual commands for empty parent commands
|
45
|
+
commands.sort.each { |cmd|
|
46
|
+
# Place holder for actual command
|
47
|
+
cmds[cmd.path] = nil
|
48
|
+
|
49
|
+
# Add parent virtual commands
|
50
|
+
curr = cmd
|
51
|
+
while !cmds.key?(curr.parent_path)
|
52
|
+
curr = cmds[curr.parent_path] = VirtualCommand.new(curr.parent_path)
|
53
|
+
end
|
54
|
+
}
|
55
|
+
|
56
|
+
# Add actual commands
|
57
|
+
commands.sort.each { |cmd|
|
58
|
+
!cmds[cmd.path] or
|
59
|
+
error "Duplicate command name '#{cmd.name}'", cmds[cmd.parent_path]
|
60
|
+
cmds[cmd.path] = cmd
|
61
|
+
}
|
62
|
+
|
63
|
+
# Link up
|
64
|
+
cmds.values.each { |cmd|
|
65
|
+
next if cmd == program
|
66
|
+
cmd.instance_variable_set(:@parent, cmds[cmd.parent_path])
|
67
|
+
cmd.parent.commands[cmd.name] = cmd
|
68
|
+
cmd.parent.cmds << cmd
|
69
|
+
!cmd.parent.commands.key?(cmd.ident) or
|
70
|
+
error "Duplicate command identifier '#{cmd.ident}'", cmd.parent
|
71
|
+
cmd.parent.commands[cmd.ident] = cmd
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,80 +1,87 @@
|
|
1
1
|
module ShellOpts
|
2
2
|
module Grammar
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
class Command < Node
|
8
|
-
# Parent command. Nil if this is the top level command (the program)
|
3
|
+
# TODO: Command aliases: list.something!,list.somethingelse!
|
4
|
+
class Command
|
5
|
+
# Parent command. nil for the program-level Command object. Initialized
|
6
|
+
# by the analyzer
|
9
7
|
attr_reader :parent
|
10
8
|
|
11
|
-
# Name of command
|
9
|
+
# Name of command. nil for the program-level Command object
|
12
10
|
attr_reader :name
|
13
11
|
|
14
|
-
#
|
15
|
-
|
12
|
+
# Ident of command. nil for the program-level Command object
|
13
|
+
attr_reader :ident
|
16
14
|
|
17
|
-
#
|
18
|
-
attr_reader :
|
15
|
+
# Path of command. The empty string for the program-level Command object
|
16
|
+
attr_reader :path
|
19
17
|
|
20
|
-
#
|
21
|
-
|
18
|
+
# Path of parent command. nil for the program-level Command object. This
|
19
|
+
# is the same as #parent&.path but is available before #parent is
|
20
|
+
# intialized. It is used to build the command hierarchy in the analyzer
|
21
|
+
attr_reader :parent_path
|
22
22
|
|
23
|
-
#
|
24
|
-
|
25
|
-
def options()
|
26
|
-
@option_multihash ||= @option_list.flat_map { |option|
|
27
|
-
option.identifiers.map { |ident| [ident, option] }
|
28
|
-
}.to_h
|
29
|
-
end
|
23
|
+
# List of comments. Initialized by the parser
|
24
|
+
attr_reader :text
|
30
25
|
|
31
|
-
#
|
32
|
-
|
33
|
-
# after initialization
|
34
|
-
def subcommands()
|
35
|
-
@subcommand_multihash ||= @subcommand_list.flat_map { |subcommand|
|
36
|
-
subcommand.identifiers.map { |name| [name, subcommand] }
|
37
|
-
}.to_h
|
38
|
-
end
|
26
|
+
# List of options. Initialized by the parser
|
27
|
+
attr_reader :opts
|
39
28
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
29
|
+
# List of sub-commands. Initialized by the parser
|
30
|
+
attr_reader :cmds
|
31
|
+
|
32
|
+
# List of arguments. Initialized by the parser
|
33
|
+
attr_reader :args
|
34
|
+
|
35
|
+
# Hash from name/identifier to option. Note that each option has at least
|
36
|
+
# two entries in the hash: One by name and one by identifier. Option
|
37
|
+
# aliases are also keys in the hash. Initialized by the analyzer
|
38
|
+
attr_reader :options
|
39
|
+
|
40
|
+
# Hash from name to sub-command. Note that each command has two entries in
|
41
|
+
# the hash: One by name and one by identifier. Initialized by the analyzer
|
42
|
+
attr_reader :commands
|
49
43
|
|
50
|
-
|
51
|
-
|
52
|
-
|
44
|
+
def initialize(path, virtual: false)
|
45
|
+
if path == ""
|
46
|
+
@path = path
|
47
|
+
else
|
48
|
+
@path = path.sub(/!$/, "")
|
49
|
+
components = @path.split(".")
|
50
|
+
@name = components.pop
|
51
|
+
@parent_path = components.join(".")
|
52
|
+
@ident = @name.gsub(/-/, "_").to_sym
|
53
|
+
end
|
54
|
+
@virtual = virtual
|
55
|
+
@text = []
|
56
|
+
@opts = []
|
57
|
+
@cmds = []
|
58
|
+
@args = []
|
59
|
+
|
60
|
+
@options = {}
|
61
|
+
@commands = {}
|
53
62
|
end
|
54
63
|
|
55
|
-
#
|
56
|
-
def
|
57
|
-
|
58
|
-
#
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
yield if block_given?
|
65
|
-
puts "options:"
|
66
|
-
indent { option_list.each { |opt| opt.dump } }
|
67
|
-
puts "subcommands: "
|
68
|
-
indent { subcommand_list.each { |cmd| cmd.dump } }
|
69
|
-
}
|
64
|
+
# True if this is the program-level command
|
65
|
+
def program?() @path == "" end
|
66
|
+
|
67
|
+
# True if this is a virtual command that cannot be called without a
|
68
|
+
# sub-command
|
69
|
+
def virtual?() @virtual end
|
70
|
+
|
71
|
+
def <=>(other)
|
72
|
+
path <=> other.path
|
70
73
|
end
|
71
|
-
|
74
|
+
end
|
72
75
|
|
73
|
-
|
74
|
-
def
|
75
|
-
|
76
|
-
@
|
76
|
+
class Program < Command
|
77
|
+
def initialize(name)
|
78
|
+
super("")
|
79
|
+
@name = name
|
77
80
|
end
|
78
81
|
end
|
82
|
+
|
83
|
+
class VirtualCommand < Command
|
84
|
+
def initialize(path) super(path, virtual: true) end
|
85
|
+
end
|
79
86
|
end
|
80
87
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
class Command
|
4
|
+
def dump
|
5
|
+
print (path ? "#{path}!" : 'nil')
|
6
|
+
print " (virtual)" if virtual?
|
7
|
+
print " [PROGRAM]" if program?
|
8
|
+
puts
|
9
|
+
indent {
|
10
|
+
puts "name: #{name.inspect}"
|
11
|
+
puts "ident: #{ident.inspect}"
|
12
|
+
puts "path: #{path.inspect}"
|
13
|
+
puts "parent_path: #{parent_path.inspect}"
|
14
|
+
if !text.empty?
|
15
|
+
puts "text"
|
16
|
+
indent { text.each { |txt| puts txt } }
|
17
|
+
end
|
18
|
+
if !opts.empty?
|
19
|
+
puts "opts"
|
20
|
+
indent { opts.each(&:dump) }
|
21
|
+
end
|
22
|
+
if !cmds.empty?
|
23
|
+
puts "cmds (#{cmds.size})"
|
24
|
+
indent { cmds.each(&:dump) }
|
25
|
+
end
|
26
|
+
if !args.empty?
|
27
|
+
puts "args"
|
28
|
+
indent { args.each { |arg| puts arg } }
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Option
|
35
|
+
def dump
|
36
|
+
puts name
|
37
|
+
indent {
|
38
|
+
if !text.empty?
|
39
|
+
puts "text"
|
40
|
+
indent { text.each { |txt| puts txt } }
|
41
|
+
end
|
42
|
+
puts "ident: #{ident.inspect}"
|
43
|
+
puts "names: #{names.join(', ')}"
|
44
|
+
puts "repeatable: #{repeatable?}"
|
45
|
+
puts "argument: #{argument?}"
|
46
|
+
if argument?
|
47
|
+
puts "argument_name: #{argument_name}" if argument_name
|
48
|
+
puts "integer: #{integer?}"
|
49
|
+
puts "float: #{float?}"
|
50
|
+
puts "optional: #{optional?}"
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
class Lexer
|
4
|
+
def self.lex(source)
|
5
|
+
lines = source.split("\n").map(&:strip)
|
6
|
+
|
7
|
+
# Skip initial blank lines
|
8
|
+
lines = lines.drop_while { |line| line == "" }
|
9
|
+
|
10
|
+
# Split lines into command, option, argument, or text
|
11
|
+
res = []
|
12
|
+
while line = lines.shift
|
13
|
+
if line =~ SCAN_RE
|
14
|
+
# Collect following comments
|
15
|
+
txts = []
|
16
|
+
while lines.first && lines.first !~ SCAN_RE
|
17
|
+
txts << lines.shift
|
18
|
+
end
|
19
|
+
|
20
|
+
words = line.split(/\s+/)
|
21
|
+
while word = words.shift
|
22
|
+
type =
|
23
|
+
case word
|
24
|
+
when OPTION_RE
|
25
|
+
"OPT"
|
26
|
+
when COMMAND_PATH_RE
|
27
|
+
"CMD"
|
28
|
+
when ARGUMENT_EXPR_RE
|
29
|
+
args = [word]
|
30
|
+
# Scan arguments
|
31
|
+
while words.first =~ ARGUMENT_EXPR_RE
|
32
|
+
args << words.shift
|
33
|
+
end
|
34
|
+
word = args.join(" ")
|
35
|
+
"ARG"
|
36
|
+
when /^[a-z0-9]/
|
37
|
+
raise CompileError, "Illegal argument: #{word} (should be uppercase)"
|
38
|
+
else
|
39
|
+
raise CompileError, "Illegal syntax: #{line}"
|
40
|
+
end
|
41
|
+
res << [type, word]
|
42
|
+
txts.each { |txt| res << ["TXT", txt] } # Add comments after first command or option
|
43
|
+
txts = []
|
44
|
+
end
|
45
|
+
elsif line =~ /^-|\+/
|
46
|
+
raise CompileError, "Illegal short option name: #{line}"
|
47
|
+
else
|
48
|
+
res << ["TXT", line]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
res
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -1,66 +1,55 @@
|
|
1
1
|
module ShellOpts
|
2
2
|
module Grammar
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
#
|
11
|
-
attr_reader :
|
12
|
-
|
13
|
-
#
|
14
|
-
attr_reader :
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
attr_reader :
|
21
|
-
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
def float?() argument? && @flags[:float] || false end
|
53
|
-
|
54
|
-
# :nocov:
|
55
|
-
def dump
|
56
|
-
super {
|
57
|
-
puts "short_names: #{short_names.inspect}"
|
58
|
-
puts "long_names: #{long_names.inspect}"
|
59
|
-
puts "flags: #{flags.inspect}"
|
60
|
-
puts "label: #{label.inspect}"
|
61
|
-
}
|
3
|
+
class Option
|
4
|
+
# Symbolic identifier. This is the name of the option with dashes ('-')
|
5
|
+
# replaced with underscores ('_')
|
6
|
+
attr_reader :ident
|
7
|
+
|
8
|
+
# Name of option. This is the name of the first long option or the name
|
9
|
+
# of the first short option if there is no long option name. It is used
|
10
|
+
# to compute #ident
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Long name of option or nil if not present
|
14
|
+
attr_reader :longname
|
15
|
+
|
16
|
+
# Short name of option or nil if not present
|
17
|
+
attr_reader :shortname
|
18
|
+
|
19
|
+
# List of all names
|
20
|
+
attr_reader :names
|
21
|
+
|
22
|
+
# Name of argument or nil if not present
|
23
|
+
attr_reader :argument_name
|
24
|
+
|
25
|
+
# Comment
|
26
|
+
attr_reader :text
|
27
|
+
|
28
|
+
def repeatable?() @repeatable end
|
29
|
+
def argument?() @argument end
|
30
|
+
def integer?() @integer end
|
31
|
+
def float?() @float end
|
32
|
+
def string?() !@integer && !@float end
|
33
|
+
def optional?() @optional end
|
34
|
+
|
35
|
+
def initialize(names, repeatable: nil, argument: nil, integer: nil, float: nil, optional: nil)
|
36
|
+
@names = names.dup
|
37
|
+
@longname = @names.find { |name| name.length > 1 }
|
38
|
+
@shortname = @names.find { |name| name.length == 1 }
|
39
|
+
@name = @longname || @shortname
|
40
|
+
@ident = @name.gsub("-", "_").to_sym
|
41
|
+
@repeatable = repeatable || false
|
42
|
+
if argument
|
43
|
+
@argument = true
|
44
|
+
@argument_name = argument if argument.is_a?(String)
|
45
|
+
else
|
46
|
+
@argument = false
|
47
|
+
end
|
48
|
+
@integer = integer || false
|
49
|
+
@float = float || false
|
50
|
+
@optional = optional || false
|
51
|
+
@text = []
|
62
52
|
end
|
63
|
-
# :nocov:
|
64
53
|
end
|
65
54
|
end
|
66
55
|
end
|