shellopts 0.9.4 → 2.0.0.pre.1

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.
@@ -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+/)
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
+
@@ -0,0 +1,71 @@
1
+
2
+ module ShellOpts
3
+ # Service object for output of messages
4
+ #
5
+ # Messages are using the common command line formats
6
+ #
7
+ class Messenger
8
+ # Name of the program. When assigning to +name+ prefixed and suffixed
9
+ # whitespaces are removed
10
+ attr_accessor :name
11
+
12
+ # :nodoc:
13
+ def name=(name) @name = name.strip end
14
+ # :nodoc:
15
+
16
+ # Usage string. If not nil the usage string is printed by #error. When
17
+ # assigning to +usage+ suffixed whitespaces are removed and the format
18
+ # automatically set to +:custom+
19
+ attr_accessor :usage
20
+
21
+ # :nodoc:
22
+ def usage=(usage)
23
+ @format = :custom
24
+ @usage = usage&.rstrip
25
+ end
26
+ # :nodoc:
27
+
28
+ # Format of the usage string: +:default+ prefixes the +usage+ with 'Usage:
29
+ # #{name} ' before printing. +:custom+ prints +usage+ as is
30
+ attr_accessor :format
31
+
32
+ # Initialize a Messenger object. +name+ is the name of the name and +usage+
33
+ # is a short description of the options (eg. '-a -b') or a longer multiline
34
+ # explanation. The +:format+ option selects bewtween the two: +short+ (the
35
+ # default) or :long. Note that
36
+ #
37
+ def initialize(name, usage, format: :default)
38
+ @name = name
39
+ @usage = usage
40
+ @format = format
41
+ end
42
+
43
+ # Print error message and usage string and exit with status 1. Output is
44
+ # using the following format
45
+ #
46
+ # <name name>: <message>
47
+ # Usage: <name name> <options and arguments>
48
+ #
49
+ def error(*msgs)
50
+ $stderr.print "#{name}: #{msgs.join}\n"
51
+ if usage
52
+ $stderr.print "Usage: #{name} " if format == :default
53
+ $stderr.print "#{usage}\n"
54
+ end
55
+ exit 1
56
+ end
57
+
58
+ # Print error message and exit with status 1. It use the current ShellOpts
59
+ # object if defined. This method should not be called in response to
60
+ # user-errors but system errors (like disk full). Output is using the
61
+ # following format:
62
+ #
63
+ # <name name>: <message>
64
+ #
65
+ def fail(*msgs)
66
+ $stderr.puts "#{name}: #{msgs.join}"
67
+ exit 1
68
+ end
69
+ end
70
+ end
71
+