shellopts 2.0.0.pre.14 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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
|