shellopts 2.0.0.pre.7 → 2.0.0.pre.14
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/TODO +15 -135
- data/lib/ext/algorithm.rb +14 -0
- data/lib/ext/ruby_env.rb +8 -0
- data/lib/shellopts.rb +90 -213
- data/lib/shellopts/args.rb +18 -12
- 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 -13
- data/lib/ext/array.rb +0 -9
- 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/option_struct.rb +0 -148
- data/lib/shellopts/parser.rb +0 -106
- data/lib/shellopts/shellopts.rb +0 -116
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module ShellOpts
|
3
|
+
class CompileError < StandardError; end
|
4
|
+
|
5
|
+
class ShellOptsError < RuntimeError; end
|
6
|
+
|
7
|
+
class Error < ShellOptsError
|
8
|
+
attr_reader :subject
|
9
|
+
|
10
|
+
def initialize(subject = nil)
|
11
|
+
super()
|
12
|
+
@subject = subject
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Fail < ShellOptsError; end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NotYet < NotImplementedError; end
|
20
|
+
class NotThis < ScriptError; end
|
21
|
+
class NotHere < ScriptError; end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
|
2
|
+
require 'ext/algorithm'
|
3
|
+
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
module ShellOpts
|
7
|
+
class Formatter
|
8
|
+
# Return string describing usage of command
|
9
|
+
def self.usage_string(command, levels: 1, margin: "")
|
10
|
+
elements(command, levels: levels, help: false).map { |line|
|
11
|
+
"#{margin}#{line}"
|
12
|
+
}.join("\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return string with help for the given command
|
16
|
+
def self.help_string(command, levels: 10, margin: "", tab: " ")
|
17
|
+
io = StringIO.new
|
18
|
+
elements(command, levels: levels, help: true).each { |head, texts, options|
|
19
|
+
io.puts "#{margin}#{head}"
|
20
|
+
texts.each { |text| io.puts "#{margin}#{tab}#{text}" }
|
21
|
+
options.each { |opt_head, opt_texts|
|
22
|
+
io.puts
|
23
|
+
io.puts "#{margin}#{tab}#{opt_head}"
|
24
|
+
opt_texts.each { |text| io.puts "#{margin}#{tab*2}#{text}" }
|
25
|
+
}
|
26
|
+
io.puts
|
27
|
+
}
|
28
|
+
io.string[0..-2]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def self.elements(command, levels: 1, help: false)
|
33
|
+
result = []
|
34
|
+
recursive_elements(result, command, levels: levels, help: help)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.recursive_elements(acc, command, levels: 1, help: false)
|
39
|
+
cmds = (command.virtual? ? command.cmds : [command])
|
40
|
+
cmds.each { |cmd|
|
41
|
+
if levels == 1 || cmd.cmds.empty?
|
42
|
+
usage = (
|
43
|
+
path_elements(cmd) +
|
44
|
+
option_elements(cmd) +
|
45
|
+
subcommand_element(cmd) +
|
46
|
+
argument_elements(cmd)
|
47
|
+
).compact.join(" ")
|
48
|
+
if help
|
49
|
+
opts = []
|
50
|
+
cmd.opts.each { |opt|
|
51
|
+
next if opt.text.empty?
|
52
|
+
opts << [option_help(opt), opt.text]
|
53
|
+
}
|
54
|
+
acc << [usage, cmd.text, opts]
|
55
|
+
else
|
56
|
+
acc << usage
|
57
|
+
end
|
58
|
+
else
|
59
|
+
cmd.cmds.each { |subcmd|
|
60
|
+
recursive_elements(acc, subcmd, levels: levels - 1, help: help)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return command line usage string
|
67
|
+
def self.command(cmd)
|
68
|
+
(path_elements(cmd) + option_elements(cmd) + argument_elements(cmd)).compact.join(" ")
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.path_elements(cmd)
|
72
|
+
Algorithm.follow(cmd, :parent).map { |parent| parent.name }.reverse
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.option_elements(cmd)
|
76
|
+
elements = []
|
77
|
+
collapsable_opts, other_opts = cmd.opts.partition { |opt| opt.shortname && !opt.argument? }
|
78
|
+
|
79
|
+
if !collapsable_opts.empty?
|
80
|
+
elements << "-" + collapsable_opts.map(&:shortname).join
|
81
|
+
end
|
82
|
+
|
83
|
+
elements + other_opts.map { |opt|
|
84
|
+
if opt.shortname
|
85
|
+
"-#{opt.shortname} #{opt.argument_name}" # We know opt has an argument
|
86
|
+
elsif opt.argument?
|
87
|
+
"--#{opt.longname}=#{opt.argument_name}"
|
88
|
+
else
|
89
|
+
"--#{opt.longname}"
|
90
|
+
end
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.option_help(opt)
|
95
|
+
result = opt.names.map { |name|
|
96
|
+
if name.size == 1
|
97
|
+
"-#{name}"
|
98
|
+
else
|
99
|
+
"--#{name}"
|
100
|
+
end
|
101
|
+
}.join(", ")
|
102
|
+
if opt.argument?
|
103
|
+
if opt.longname
|
104
|
+
result += "=#{opt.argument_name}"
|
105
|
+
else
|
106
|
+
result += " #{opt.argument_name}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.subcommand_element(cmd)
|
113
|
+
!cmd.cmds.empty? ? [cmd.cmds.map(&:name).join("|")] : []
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.argument_elements(cmd)
|
117
|
+
cmd.args
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.help_element(cmd)
|
121
|
+
text.map { |l| l.sub(/^\s*# /, "").rstrip }.join(" ")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -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
|
+
|