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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +201 -267
  5. data/TODO +37 -5
  6. data/doc/format.rb +95 -0
  7. data/doc/grammar.txt +27 -0
  8. data/doc/syntax.rb +110 -0
  9. data/doc/syntax.txt +10 -0
  10. data/lib/ext/array.rb +62 -0
  11. data/lib/ext/forward_to.rb +15 -0
  12. data/lib/ext/lcs.rb +34 -0
  13. data/lib/shellopts/analyzer.rb +130 -0
  14. data/lib/shellopts/ansi.rb +8 -0
  15. data/lib/shellopts/args.rb +29 -21
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +292 -92
  19. data/lib/shellopts/grammar.rb +375 -0
  20. data/lib/shellopts/interpreter.rb +103 -0
  21. data/lib/shellopts/lexer.rb +175 -0
  22. data/lib/shellopts/parser.rb +293 -0
  23. data/lib/shellopts/program.rb +279 -0
  24. data/lib/shellopts/renderer.rb +227 -0
  25. data/lib/shellopts/stack.rb +7 -0
  26. data/lib/shellopts/token.rb +44 -0
  27. data/lib/shellopts/version.rb +1 -1
  28. data/lib/shellopts.rb +360 -3
  29. data/main +1180 -0
  30. data/shellopts.gemspec +8 -14
  31. metadata +86 -41
  32. data/lib/ext/algorithm.rb +0 -14
  33. data/lib/ext/ruby_env.rb +0 -8
  34. data/lib/shellopts/ast/command.rb +0 -112
  35. data/lib/shellopts/ast/dump.rb +0 -28
  36. data/lib/shellopts/ast/option.rb +0 -15
  37. data/lib/shellopts/ast/parser.rb +0 -106
  38. data/lib/shellopts/constants.rb +0 -88
  39. data/lib/shellopts/exceptions.rb +0 -21
  40. data/lib/shellopts/grammar/analyzer.rb +0 -76
  41. data/lib/shellopts/grammar/command.rb +0 -87
  42. data/lib/shellopts/grammar/dump.rb +0 -56
  43. data/lib/shellopts/grammar/lexer.rb +0 -56
  44. data/lib/shellopts/grammar/option.rb +0 -55
  45. 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