shellopts 2.0.0.pre.7 → 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,21 @@
1
+
2
+ module ShellOpts
3
+ class CompileError < StandardError; end
4
+
5
+ class ShellOptsError < RuntimeError; end
6
+
7
+ class Error < ShellOptsError
8
+ attr_reader :subject
9
+
10
+ def initialize(subject = nil)
11
+ super()
12
+ @subject = subject
13
+ end
14
+ end
15
+
16
+ class Fail < ShellOptsError; end
17
+ end
18
+
19
+ class NotYet < NotImplementedError; end
20
+ class NotThis < ScriptError; end
21
+ class NotHere < ScriptError; end
@@ -0,0 +1,125 @@
1
+
2
+ require 'ext/algorithm'
3
+
4
+ require 'stringio'
5
+
6
+ module ShellOpts
7
+ class Formatter
8
+ # Return string describing usage of command
9
+ def self.usage_string(command, levels: 1, margin: "")
10
+ elements(command, levels: levels, help: false).map { |line|
11
+ "#{margin}#{line}"
12
+ }.join("\n")
13
+ end
14
+
15
+ # Return string with help for the given command
16
+ def self.help_string(command, levels: 10, margin: "", tab: " ")
17
+ io = StringIO.new
18
+ elements(command, levels: levels, help: true).each { |head, texts, options|
19
+ io.puts "#{margin}#{head}"
20
+ texts.each { |text| io.puts "#{margin}#{tab}#{text}" }
21
+ options.each { |opt_head, opt_texts|
22
+ io.puts
23
+ io.puts "#{margin}#{tab}#{opt_head}"
24
+ opt_texts.each { |text| io.puts "#{margin}#{tab*2}#{text}" }
25
+ }
26
+ io.puts
27
+ }
28
+ io.string[0..-2]
29
+ end
30
+
31
+ private
32
+ def self.elements(command, levels: 1, help: false)
33
+ result = []
34
+ recursive_elements(result, command, levels: levels, help: help)
35
+ result
36
+ end
37
+
38
+ def self.recursive_elements(acc, command, levels: 1, help: false)
39
+ cmds = (command.virtual? ? command.cmds : [command])
40
+ cmds.each { |cmd|
41
+ if levels == 1 || cmd.cmds.empty?
42
+ usage = (
43
+ path_elements(cmd) +
44
+ option_elements(cmd) +
45
+ subcommand_element(cmd) +
46
+ argument_elements(cmd)
47
+ ).compact.join(" ")
48
+ if help
49
+ opts = []
50
+ cmd.opts.each { |opt|
51
+ next if opt.text.empty?
52
+ opts << [option_help(opt), opt.text]
53
+ }
54
+ acc << [usage, cmd.text, opts]
55
+ else
56
+ acc << usage
57
+ end
58
+ else
59
+ cmd.cmds.each { |subcmd|
60
+ recursive_elements(acc, subcmd, levels: levels - 1, help: help)
61
+ }
62
+ end
63
+ }
64
+ end
65
+
66
+ # Return command line usage string
67
+ def self.command(cmd)
68
+ (path_elements(cmd) + option_elements(cmd) + argument_elements(cmd)).compact.join(" ")
69
+ end
70
+
71
+ def self.path_elements(cmd)
72
+ Algorithm.follow(cmd, :parent).map { |parent| parent.name }.reverse
73
+ end
74
+
75
+ def self.option_elements(cmd)
76
+ elements = []
77
+ collapsable_opts, other_opts = cmd.opts.partition { |opt| opt.shortname && !opt.argument? }
78
+
79
+ if !collapsable_opts.empty?
80
+ elements << "-" + collapsable_opts.map(&:shortname).join
81
+ end
82
+
83
+ elements + other_opts.map { |opt|
84
+ if opt.shortname
85
+ "-#{opt.shortname} #{opt.argument_name}" # We know opt has an argument
86
+ elsif opt.argument?
87
+ "--#{opt.longname}=#{opt.argument_name}"
88
+ else
89
+ "--#{opt.longname}"
90
+ end
91
+ }
92
+ end
93
+
94
+ def self.option_help(opt)
95
+ result = opt.names.map { |name|
96
+ if name.size == 1
97
+ "-#{name}"
98
+ else
99
+ "--#{name}"
100
+ end
101
+ }.join(", ")
102
+ if opt.argument?
103
+ if opt.longname
104
+ result += "=#{opt.argument_name}"
105
+ else
106
+ result += " #{opt.argument_name}"
107
+ end
108
+ end
109
+ result
110
+ end
111
+
112
+ def self.subcommand_element(cmd)
113
+ !cmd.cmds.empty? ? [cmd.cmds.map(&:name).join("|")] : []
114
+ end
115
+
116
+ def self.argument_elements(cmd)
117
+ cmd.args
118
+ end
119
+
120
+ def self.help_element(cmd)
121
+ text.map { |l| l.sub(/^\s*# /, "").rstrip }.join(" ")
122
+ end
123
+ end
124
+ end
125
+
@@ -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
+