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.
@@ -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
- # A command. Commands are organized hierarchically with a Program object as
4
- # the root node
5
- #
6
- # Sets Node#key to the name of the command incl. the exclamation point
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 (String). Name doesn't include the exclamation point ('!')
9
+ # Name of command. nil for the program-level Command object
12
10
  attr_reader :name
13
11
 
14
- # Same as #name. TODO Define in Grammar::Node instead
15
- alias :key_name :name
12
+ # Ident of command. nil for the program-level Command object
13
+ attr_reader :ident
16
14
 
17
- # List of options in declaration order
18
- attr_reader :option_list
15
+ # Path of command. The empty string for the program-level Command object
16
+ attr_reader :path
19
17
 
20
- # List of commands in declaration order
21
- attr_reader :subcommand_list
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
- # Multihash from option key or names (both short and long names) to option. This
24
- # means an option can occur more than once as the hash value
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
- # Sub-commands of this command. Is a multihash from sub-command key or
32
- # name to command object. Lazily constructed because subcommands are added
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
- # Initialize a Command object. parent is the parent Command object or nil
41
- # if this is the root object. name is the name of the command (without
42
- # the exclamation mark), and option_list a list of Option objects
43
- def initialize(parent, name, option_list)
44
- super("#{name}!".to_sym, name)
45
- parent.attach(self) if parent
46
- @option_list = option_list
47
- @subcommand_list = []
48
- end
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
- # Return key for the identifier
51
- def identifier2key(ident)
52
- options[ident]&.key || subcommands[ident]&.key
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
- # Return list of identifiers for the command
56
- def identifiers() [key, name] end
57
-
58
- # :nocov:
59
- def dump(&block)
60
- puts "#{key.inspect}"
61
- indent {
62
- puts "parent: #{parent&.key.inspect}"
63
- puts "name: #{name.inspect}"
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
- # :nocov:
74
+ end
72
75
 
73
- protected
74
- def attach(subcommand)
75
- subcommand.instance_variable_set(:@parent, self)
76
- @subcommand_list << subcommand
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
- # Models an Option
4
- #
5
- # Sets Node#key to the first long option name if present or else the first short option
6
- class Option < Node
7
- # List of short names (incl. '-')
8
- attr_reader :short_names
9
-
10
- # List of long names (incl. '--')
11
- attr_reader :long_names
12
-
13
- # Name of the key attribute (eg. if key is :all then key_name is '--all'
14
- attr_reader :key_name
15
-
16
- # List of flags (Symbol)
17
- def flags() @flags.keys end
18
-
19
- # Informal name of argument (eg. 'FILE'). nil if not present
20
- attr_reader :label
21
-
22
- # Initialize an option. Short and long names are arrays of the short/long
23
- # option names (incl. the '-'/'--' prefix). It is assumed that at least
24
- # one name is given. Flags is a list of symbolic flags. Allowed flags are
25
- # :repeated, :argument, :optional, :integer, and :float. Note that
26
- # there's no :string flag, it's status is inferred. label is the optional
27
- # informal name of the option argument (eg. 'FILE') or nil if not present
28
- def initialize(short_names, long_names, flags, label = nil)
29
- @key_name = long_names.first || short_names.first
30
- name = @key_name.sub(/^-+/, "")
31
- super(name.to_sym, name)
32
- @short_names, @long_names = short_names, long_names
33
- @flags = flags.map { |flag| [flag, true] }.to_h
34
- @label = label
35
- end
36
-
37
- # Array of option names with short names first and then the long names
38
- def names() @short_names + @long_names end
39
-
40
- # Array of names and the key
41
- def identifiers() names + [key] end
42
-
43
- # Return true if +ident+ is equal to any name or to key
44
- def match?(ident) names.include?(ident) || ident == key end
45
-
46
- # Flag query methods. Returns true if the flag is present and otherwise nil
47
- def repeated?() @flags[:repeated] || false end
48
- def argument?() @flags[:argument] || false end
49
- def optional?() argument? && @flags[:optional] || false end
50
- def string?() argument? && !integer? && !float? end
51
- def integer?() argument? && @flags[:integer] || false end
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