shellopts 2.0.0.pre.7 → 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,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
+