shellopts 0.9.7 → 2.0.0.pre.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+
2
+ module ShellOpts
3
+ # Specialization of Array for arguments lists. Args extends Array with a
4
+ # #extract and an #expect method to extract elements from the array. The
5
+ # methods call #error() in response to errors
6
+ class Args < Array
7
+ def initialize(shellopts, *args)
8
+ @shellopts = shellopts
9
+ super(*args)
10
+ end
11
+
12
+ # Remove and return elements from beginning of the array. If
13
+ # +count_or_range+ is a number, that number of elements will be returned.
14
+ # If the count is one, a simple value is returned instead of an array. If
15
+ # the count is negative, the elements will be removed from the end of the
16
+ # array. If +count_or_range+ is a range, the number of elements returned
17
+ # will be in that range. The range can't contain negative numbers #expect
18
+ # calls #error() if there's is not enough elements in the array to satisfy
19
+ # the request
20
+ def extract(count_or_range, message = nil)
21
+ if count_or_range.is_a?(Range)
22
+ range = count_or_range
23
+ range.min <= self.size or inoa(message)
24
+ n_extract = [self.size, range.max].min
25
+ n_extend = range.max > self.size ? range.max - self.size : 0
26
+ r = self.shift(n_extract) + Array.new(n_extend)
27
+ else
28
+ count = count_or_range
29
+ self.size >= count.abs or inoa(message)
30
+ start = count >= 0 ? 0 : size + count
31
+ r = slice!(start, count.abs)
32
+ r.size == 0 ? nil : (r.size == 1 ? r.first : r)
33
+ end
34
+ end
35
+
36
+ # Remove and returns elements from the array. If +count_or_range+ is a
37
+ # number, that number of elements will be returned. If the count is one, a
38
+ # simple value is returned instead of an array. If +count_or_range+ is a
39
+ # range, the number of elements returned will be in that range. The range
40
+ # can't contain negative numbers. #expect calls #error() if the array has
41
+ # remaning elemens after removal satisfy the request
42
+ def expect(count_or_range, message = nil)
43
+ if count_or_range.is_a?(Range)
44
+ range = count_or_range
45
+ range.cover?(self.size) or inoa(message)
46
+ self.shift(self.size)
47
+ else
48
+ count = count_or_range
49
+ count == self.size or inoa(message)
50
+ r = self.shift(count)
51
+ r.size == 0 ? nil : (r.size == 1 ? r.first : r)
52
+ end
53
+ end
54
+
55
+ private
56
+ def inoa(message = nil)
57
+ @shellopts.messenger.error(message || "Illegal number of arguments")
58
+ end
59
+ end
60
+ end
@@ -23,7 +23,7 @@ module ShellOpts
23
23
  end
24
24
 
25
25
  # Return either a value (option value), an array of values (command), or
26
- # nil (option without a value). Should be defined in sub-classes
26
+ # nil (option without a value). It must be defined in sub-classes of Ast::Node
27
27
  def values() raise end
28
28
 
29
29
  # :nocov:
@@ -8,12 +8,12 @@ require 'shellopts/grammar/program.rb'
8
8
  module ShellOpts
9
9
  module Grammar
10
10
  # Compiles an option definition string and returns a Grammar::Program
11
- # object. program_name is the name of the program and source is the
11
+ # object. name is the name of the program and source is the
12
12
  # option definition string
13
- def self.compile(program_name, source)
14
- program_name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{program_name.class}"
13
+ def self.compile(name, source)
14
+ name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{name.class}"
15
15
  source.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{source.class}"
16
- Compiler.new(program_name, source).call
16
+ Compiler.new(name, source).call
17
17
  end
18
18
 
19
19
  # Service object for compiling an option definition string. Returns a
@@ -26,8 +26,8 @@ module ShellOpts
26
26
  class Error < RuntimeError; end
27
27
 
28
28
  # Initialize a Compiler object. source is the option definition string
29
- def initialize(program_name, source)
30
- @program_name, @tokens = program_name, source.split(/\s+/).reject(&:empty?)
29
+ def initialize(name, source)
30
+ @name, @tokens = name, source.split(/\s+/).reject(&:empty?)
31
31
 
32
32
  # @commands_by_path is an hash from command-path to Command or Program
33
33
  # object. The top level Program object has nil as its path.
@@ -54,7 +54,7 @@ module ShellOpts
54
54
  end
55
55
 
56
56
  def compile_program
57
- program = @commands_by_path[nil] = Grammar::Program.new(@program_name, compile_options)
57
+ program = @commands_by_path[nil] = Grammar::Program.new(@name, compile_options)
58
58
  while curr_token && curr_token != "--"
59
59
  compile_command
60
60
  end
@@ -0,0 +1,15 @@
1
+
2
+ require 'shellopts/idr.rb'
3
+
4
+ module ShellOpts
5
+ module Idr
6
+ # Generates an Idr::Program from an Ast::Program object
7
+ def self.generate(ast, messenger)
8
+ Idr::Program.new(ast, messenger)
9
+ end
10
+ end
11
+ end
12
+
13
+
14
+
15
+
@@ -11,12 +11,8 @@ module ShellOpts
11
11
  # Name of command (String). Name doesn't include the exclamation point ('!')
12
12
  attr_reader :name
13
13
 
14
- # Hash from option names (both short and long names) to option. This
15
- # means an option can occur more than once as the hash value
16
- attr_reader :options
17
-
18
- # Sub-commands of this command. Is a hash from sub-command name to command object
19
- attr_reader :commands
14
+ # Same as #name. TODO Define in Grammar::Node instead
15
+ alias :key_name :name
20
16
 
21
17
  # List of options in declaration order
22
18
  attr_reader :option_list
@@ -24,6 +20,23 @@ module ShellOpts
24
20
  # List of commands in declaration order
25
21
  attr_reader :command_list
26
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
30
+
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 commands()
35
+ @command_multihash ||= @command_list.flat_map { |command|
36
+ command.identifiers.map { |name| [name, command] }
37
+ }.to_h
38
+ end
39
+
27
40
  # Initialize a Command object. parent is the parent Command object or nil
28
41
  # if this is the root object. name is the name of the command (without
29
42
  # the exclamation mark), and option_list a list of Option objects
@@ -32,11 +45,17 @@ module ShellOpts
32
45
  @name = name
33
46
  parent.attach(self) if parent
34
47
  @option_list = option_list
35
- @options = @option_list.flat_map { |opt| opt.names.map { |name| [name, opt] } }.to_h
36
- @commands = {}
37
48
  @command_list = []
38
49
  end
39
50
 
51
+ # Return key for the identifier
52
+ def identifier2key(ident)
53
+ options[ident]&.key || commands[ident]&.key
54
+ end
55
+
56
+ # Return list of identifiers for the command
57
+ def identifiers() [key, name] end
58
+
40
59
  # :nocov:
41
60
  def dump(&block)
42
61
  puts "#{key.inspect}"
@@ -55,7 +74,6 @@ module ShellOpts
55
74
  protected
56
75
  def attach(command)
57
76
  command.instance_variable_set(:@parent, self)
58
- @commands[command.name] = command
59
77
  @command_list << command
60
78
  end
61
79
  end
@@ -10,6 +10,9 @@ module ShellOpts
10
10
  # List of long names (incl. '--')
11
11
  attr_reader :long_names
12
12
 
13
+ # Name of the key attribute (eg. if key is :all then key_name is '--all'
14
+ attr_reader :key_name
15
+
13
16
  # List of flags (Symbol)
14
17
  def flags() @flags.keys end
15
18
 
@@ -23,7 +26,8 @@ module ShellOpts
23
26
  # there's no :string flag, it's status is inferred. label is the optional
24
27
  # informal name of the option argument (eg. 'FILE') or nil if not present
25
28
  def initialize(short_names, long_names, flags, label = nil)
26
- super((long_names.first || short_names.first).sub(/^-+/, "").to_sym)
29
+ @key_name = long_names.first || short_names.first
30
+ super(@key_name.sub(/^-+/, "").to_sym)
27
31
  @short_names, @long_names = short_names, long_names
28
32
  @flags = flags.map { |flag| [flag, true] }.to_h
29
33
  @label = label
@@ -32,6 +36,12 @@ module ShellOpts
32
36
  # Array of option names with short names first and then the long names
33
37
  def names() @short_names + @long_names end
34
38
 
39
+ # Array of names and the key
40
+ def identifiers() names + [key] end
41
+
42
+ # Return true if +ident+ is equal to any name or to key
43
+ def match?(ident) names.include?(ident) || ident == key end
44
+
35
45
  # Flag query methods. Returns true if the flag is present and otherwise nil
36
46
  def repeated?() @flags[:repeated] || false end
37
47
  def argument?() @flags[:argument] || false end
@@ -17,7 +17,7 @@ module ShellOpts
17
17
  def usage
18
18
  (
19
19
  render_options(option_list) +
20
- commands.values.map { |cmd| render_command(cmd) } +
20
+ command_list.map { |cmd| render_command(cmd) } +
21
21
  args
22
22
  ).flatten.join(" ")
23
23
  end
@@ -34,7 +34,7 @@ module ShellOpts
34
34
  private
35
35
  def render_command(command)
36
36
  [command.name] + render_options(command.option_list) +
37
- command.commands.values.map { |cmd| render_command(cmd) }.flatten
37
+ command.command_list.map { |cmd| render_command(cmd) }.flatten
38
38
  end
39
39
 
40
40
  def render_options(options)
@@ -0,0 +1,209 @@
1
+
2
+ module ShellOpts
3
+ # Idr models the Internal Data Representation of a program. It is the native
4
+ # representation of a command
5
+ #
6
+ # The IDR should ideally be completely detached from the compile-time grammar
7
+ # and AST but they are only hidden from view in this implementation. Create
8
+ # a Shellopts object instead to access the compiler data
9
+ #
10
+ module Idr
11
+ # Base class for the Idr class hierarchy. It is constructed from an Ast
12
+ # object by #generate. Node is modelled as an element of a hash with a key
13
+ # and a value. Options have their (optional) argument as value while
14
+ # commands use +self+ as value
15
+ class Node
16
+ # Unique key (within context) for the option or command. nil for the
17
+ # top-level Program object
18
+ #
19
+ # It is usually the first long option if present and else the first short
20
+ # option turned into a Symbol by first removing prefixed dashed, eg.
21
+ # '--all' becomes :all
22
+ attr_reader :key
23
+
24
+ # Name of command and option as used on the command line
25
+ attr_reader :name
26
+
27
+ # Value of node. This can be a simple value (String, Integer, or Float),
28
+ # an Array of values, or a Idr::Command object. Note that the value of a
29
+ # Command object is the object itself
30
+ #
31
+ # Repeated options are implemented as an Array with one element for each
32
+ # use of the option. The element is nil if the option doesn't take
33
+ # arguments or if an optional argument is missing.
34
+ attr_reader :value
35
+
36
+ protected
37
+ # Copy arguments into instance variables
38
+ def initialize(ast, key, name, value)
39
+ @ast, @key, @name, @value = ast, key, name, value
40
+ end
41
+
42
+ # The AST node for this Idr object
43
+ attr_reader :ast
44
+
45
+ # Shorthand to the grammar node for this Idr object
46
+ def grammar() @ast.grammar end
47
+ end
48
+
49
+ # Base class for Options
50
+ class Option < Node
51
+ end
52
+
53
+ class SimpleOption < Option
54
+ protected
55
+ # Initialize with defauls from the Ast. +value+ is set to true if option
56
+ # doesn't take an argument
57
+ def initialize(ast)
58
+ value = ast.grammar.argument? ? ast.value : true
59
+ super(ast, ast.key, ast.name, value)
60
+ end
61
+ end
62
+
63
+ # An OptionGroup models repeated options collapsed into a single key. The
64
+ # name of the group should be set to the name of the key (eg. '--all' if
65
+ # the key is :all)
66
+ class OptionGroup < Option
67
+ # Array of names of the options
68
+ attr_reader :names
69
+
70
+ # Array of values of the options
71
+ alias :values :value
72
+
73
+ # Name is set to the key name and value to an array of option values
74
+ def initialize(key, name, options)
75
+ @names = options.map(&:name)
76
+ super(nil, key, name, options.map(&:value))
77
+ end
78
+ end
79
+
80
+ class Command < Node
81
+ # Hash from key to options with repeated option_list collapsed into a
82
+ # option group. It also include an entry for the subcommand. Options are
83
+ # ordered by first use on the command line. The command entry will always
84
+ # be last
85
+ attr_reader :options
86
+
87
+ # List of command line options in the same order as on the command line
88
+ attr_reader :option_list
89
+
90
+ # Subcommand object. Possibly nil
91
+ attr_reader :subcommand
92
+
93
+ # True if ident is declared
94
+ def declared?(ident) option?(ident) || command?(ident) end
95
+
96
+ # True if ident is declared as an option
97
+ def option?(ident) grammar.options.key?(ident) end
98
+
99
+ # True if ident is declared as a command
100
+ def command?(ident) grammar.commands.key?(ident) end
101
+
102
+ # True if ident is present
103
+ def key?(ident)
104
+ declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
105
+ key = grammar.identifier2key(ident)
106
+ @options.key?(key)
107
+ end
108
+
109
+ # Value of ident. Repeated options are collapsed into an OptionGroup object
110
+ def [](ident)
111
+ declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
112
+ key = grammar.identifier2key(ident)
113
+ if @options.key?(key)
114
+ @options[key].value
115
+ elsif option?(key)
116
+ false
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ # Apply defaults recursively. Values can be lambdas that will be evaluated to
123
+ # get the default value
124
+ def apply(defaults = {}) end
125
+
126
+ # Return options and command as an array
127
+ def to_a() @ast.values end
128
+
129
+ # Return options and command as a hash. The hash also define the
130
+ # singleton method #subcommand that returns the key of the subcommand
131
+ #
132
+ # +key+ controls the type of keys used: +:key+ (the default) use the
133
+ # symbolic key, +:name+ use key_name. Note that using +:name+ can cause name collisions between
134
+ # option and command names and that #to_s raises an exception if it detects a collision
135
+ #
136
+ # +aliases+ maps from key to replacement key (which could be any object).
137
+ # +aliases+ can be used to avoid name collisions between options and
138
+ # commands
139
+ #
140
+ # IDEA: Make subcommand _not_ follow the +key+ setting so that setting key to
141
+ # IDEA: Add a singleton method #subcommand to the hash
142
+ #
143
+ def to_h(use: :key, aliases: {})
144
+ value = {}
145
+ value.define_singleton_method(:subcommand) { nil }
146
+ options.values.each { |opt|
147
+ ident = aliases[opt.key] || (use == :key ? opt.key : opt.ast.grammar.key_name)
148
+ !value.key?(ident) or raise ConversionError, "Duplicate key: #{ident.inspect}"
149
+ case opt
150
+ when Option
151
+ value[ident] = opt.value
152
+ when Command
153
+ value[ident] = opt.value.to_h
154
+ value.define_singleton_method(:subcommand) { ident } # Redefine
155
+ else
156
+ raise InternalError, "Oops"
157
+ end
158
+ }
159
+ value
160
+ end
161
+
162
+ # Return options and command as a struct
163
+ def to_struct(key = :key, aliases = {}) OptionStruct.new(self, key, aliases) end
164
+
165
+ protected
166
+ # Initialize an Idr::Command object and all dependent objects
167
+ def initialize(ast)
168
+ super(ast, ast.key, ast.name, self)
169
+ @option_list = ast.options.map { |node| SimpleOption.new(node) }
170
+ @subcommand = Command.new(ast.command) if ast.command
171
+ @options = @option_list.group_by { |option| option.key }.map { |key, option_list|
172
+ option =
173
+ if ast.grammar.options[key].repeated?
174
+ OptionGroup.new(key, ast.grammar.options[key].key_name, option_list)
175
+ else
176
+ option_list.first
177
+ end
178
+ [key, option]
179
+ }.to_h
180
+ @options[subcommand.key] = @subcommand if @subcommand
181
+ end
182
+ end
183
+
184
+ class Program < Command
185
+ # #key is nil for the top-level Program object
186
+ def key() nil end
187
+
188
+ # Remaining command line arguments
189
+ def args() @ast.arguments end
190
+
191
+ # Messenger object that is used to emit error messages. It should
192
+ # implement #error(*args) and #fail(*args)
193
+ attr_reader :messenger
194
+
195
+ # Initialize the top-level Idr::Program object
196
+ def initialize(ast, messenger)
197
+ @messenger = messenger
198
+ super(ast)
199
+ end
200
+
201
+ # Emit error message and a usage description before exiting with status 1
202
+ def error(*args) messenger.error(*error_messages) end
203
+
204
+ # Emit error message before exiting with status 1
205
+ def fail(*args) messenger.fail(*error_messages) end
206
+ end
207
+ end
208
+ end
209
+