shellopts 2.0.0.pre.13 → 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.
@@ -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