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
@@ -1,76 +0,0 @@
|
|
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,87 +0,0 @@
|
|
1
|
-
module ShellOpts
|
2
|
-
module Grammar
|
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
|
7
|
-
attr_reader :parent
|
8
|
-
|
9
|
-
# Name of command. nil for the program-level Command object
|
10
|
-
attr_reader :name
|
11
|
-
|
12
|
-
# Ident of command. nil for the program-level Command object
|
13
|
-
attr_reader :ident
|
14
|
-
|
15
|
-
# Path of command. The empty string for the program-level Command object
|
16
|
-
attr_reader :path
|
17
|
-
|
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
|
-
|
23
|
-
# List of comments. Initialized by the parser
|
24
|
-
attr_reader :text
|
25
|
-
|
26
|
-
# List of options. Initialized by the parser
|
27
|
-
attr_reader :opts
|
28
|
-
|
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
|
43
|
-
|
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 = {}
|
62
|
-
end
|
63
|
-
|
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
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
class Program < Command
|
77
|
-
def initialize(name)
|
78
|
-
super("")
|
79
|
-
@name = name
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
class VirtualCommand < Command
|
84
|
-
def initialize(path) super(path, virtual: true) end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -1,56 +0,0 @@
|
|
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
|
@@ -1,56 +0,0 @@
|
|
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,55 +0,0 @@
|
|
1
|
-
module ShellOpts
|
2
|
-
module Grammar
|
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 = []
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
module ShellOpts
|
2
|
-
module Grammar
|
3
|
-
class Parser
|
4
|
-
def self.parse(program_name, exprs)
|
5
|
-
@commands = []
|
6
|
-
@commands << (@current = @cmd = Program.new(program_name))
|
7
|
-
@exprs = exprs.dup
|
8
|
-
|
9
|
-
while !@exprs.empty?
|
10
|
-
type, value = @exprs.shift
|
11
|
-
case type
|
12
|
-
when "OPT"
|
13
|
-
parse_option(value)
|
14
|
-
when "CMD"
|
15
|
-
parse_command(value)
|
16
|
-
when "ARG"
|
17
|
-
parse_argument(value)
|
18
|
-
when "TXT"
|
19
|
-
parse_text(value)
|
20
|
-
else
|
21
|
-
raise
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
@commands.each { |cmd| # Remove empty last-lines in comments and options
|
26
|
-
while cmd.text.last =~ /^\s*$/
|
27
|
-
cmd.text.pop
|
28
|
-
end
|
29
|
-
cmd.opts.each { |opt|
|
30
|
-
while opt.text.last =~ /^\s*$/
|
31
|
-
opt.text.pop
|
32
|
-
end
|
33
|
-
}
|
34
|
-
}
|
35
|
-
|
36
|
-
@commands
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.parse_option_names(names)
|
40
|
-
names.split(",")
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.parse_option(source)
|
44
|
-
OPTION_RE =~ source or raise CompilerError, "Illegal option: #{source}"
|
45
|
-
option_group = $1
|
46
|
-
argument = $4 || $2 && true
|
47
|
-
type = $3
|
48
|
-
optional = $5
|
49
|
-
|
50
|
-
option_group =~ /^(\+\+?|--?)(.*)/
|
51
|
-
repeatable = ($1 == '+' || $1 == '++' ? '+' : nil)
|
52
|
-
names = parse_option_names($2)
|
53
|
-
|
54
|
-
@cmd.opts << (@current = Option.new(
|
55
|
-
names,
|
56
|
-
repeatable: repeatable, argument: argument,
|
57
|
-
integer: (type == '#'), float: (type == '$'),
|
58
|
-
optional: optional))
|
59
|
-
!OPTION_RESERVED_WORDS.include?(@current.name) or
|
60
|
-
raise CompilerError, "Reserved option name: #{@current.name}"
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.parse_argument(source)
|
64
|
-
@cmd.args << source
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.parse_command(value)
|
68
|
-
@commands << (@current = @cmd = Command.new(value))
|
69
|
-
!COMMAND_RESERVED_WORDS.include?(@current.name) or
|
70
|
-
raise CompilerError, "Reserved command name: #{@current.name}"
|
71
|
-
end
|
72
|
-
|
73
|
-
def self.parse_text(value)
|
74
|
-
@current.text << value
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|