shellopts 1.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cc3c3ef5b7b13876949b290b9d247c41778eaa38ea01432d5abac47b663478a
4
- data.tar.gz: bc2c44f163f81d8b51545679e8f8280ac889fbb1f96a7898e5a65fa63d337d4a
3
+ metadata.gz: e60db3cf5de50dcd106cb0fca007d064e18354278a65207bb167bf8aa63d3433
4
+ data.tar.gz: 85b0108262357d6e5654e725ab12293f51fd2579beb13fa0d910be2200c32310
5
5
  SHA512:
6
- metadata.gz: d3c501335a899e4b14280cbb14b7f9e8a0d9ef28715760394d8f13ea837d0a5d3d9b91b9d07dcb42a747753e97a10c157ae5c5b20b95804b1746e0ebad7d88c9
7
- data.tar.gz: 387c888158bbbebe127c6efde55c7bbb14436b7dcb8227cac38e2c9dac85e9a3d956c062e04b86101e28fa0bde4977fefadad4d1a643dd48d9218e5c04558ad2
6
+ metadata.gz: 375fe97651622560d786b288217e1a8581d12bbc497dee55b36848597d9410d393a127e86088cfe4ac8f949270d9f6797d6f135f65492f15bb9d4a84bff1ffbc
7
+ data.tar.gz: c69a813e4672d87c6a4da69e0e8086d8acfe54c8506cda64ce6dc36dbc12669cc79c29ffd480c6871937d87491139f4e7448fd35a6d6a7d333451addd6c634a6
data/README.md CHANGED
@@ -378,6 +378,16 @@ release a new version, update the version number in `version.rb`, and then run
378
378
  git commits and tags, and push the `.gem` file to
379
379
  [rubygems.org](https://rubygems.org).
380
380
 
381
+ ## Implementation
382
+
383
+ FIXME
384
+ # ShellOpts is a library for parsing command line options and commands. It
385
+ # consists of the interface module {ShellOpts}, the implementation class
386
+ # {ShellOpts::ShellOpts} and the representation classes
387
+ # {ShellOpts::OptionsHash} and {ShellOpts::OptionsStruct}.
388
+ # {ShellOpts::Messenger} is used for error messages
389
+
390
+
381
391
  ## Contributing
382
392
 
383
393
  Bug reports and pull requests are welcome on GitHub at
data/TODO CHANGED
@@ -1,5 +1,18 @@
1
1
 
2
2
  TODO
3
+ o Add validation block to ShellOpts class methods
4
+ o Get rid of key_name. Define #name on Grammar::Node instead
5
+ o Define #name to the string name of the option/command without prefixed '--'
6
+ for options. This can cause collisions but they can be avoided using aliases
7
+ o Clean-up
8
+ o Grammar::options -> Grammar::option_multihash
9
+ o Clean-up identifiers etc.
10
+ o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
11
+ o subcommand vs. command consistency
12
+ o Implement ObjectStruct#key! and ObjectStruct#value! (?)
13
+ o Allow command_alias == nil to suppress the method
14
+ o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
15
+ o Use hash_tree
3
16
  o Also allow assignment to usage string for ShellOpts::ShellOpts objects
4
17
  o Create a ShellOpts.args method? It would be useful when processing commands:
5
18
  case opt
@@ -9,7 +22,10 @@ TODO
9
22
  ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
10
23
  Another option would be to create an argument-processing method:
11
24
  shellopts.argv(2) -> call error if not exactly two arguments else return elements
12
-
25
+ o Add a ShellOpts.option method:
26
+ file = ShellOpts.option("--file")
27
+ This will only work for options on the outermost level... maybe:
28
+ file = ShellOpts.option("load! --file")
13
29
  o Check on return value from #process block to see if all options was handled:
14
30
  case opt
15
31
  when '-v'; verbose = true # Return value 'true' is ok
@@ -2,231 +2,145 @@ require "shellopts/version"
2
2
 
3
3
  require 'shellopts/compiler.rb'
4
4
  require 'shellopts/parser.rb'
5
+ require 'shellopts/generator.rb'
6
+ require 'shellopts/option_struct.rb'
7
+ require 'shellopts/messenger.rb'
5
8
  require 'shellopts/utils.rb'
6
9
 
7
- # ShellOpts is a library for parsing command line options and sub-commands. The
8
- # library API consists of the methods {ShellOpts.process}, {ShellOpts.error},
9
- # and {ShellOpts.fail} and the result class {ShellOpts::ShellOpts}
10
+ # Name of program. Defined as the basename of the program file
11
+ PROGRAM = File.basename($PROGRAM_NAME)
12
+
13
+ # ShellOpts main Module
14
+ #
15
+ # This module contains methods to process command line options and arguments.
16
+ # ShellOpts keeps a reference in ShellOpts.shellopts to the result of the last
17
+ # command that was processed through its interface and use it as the implicit
18
+ # object of many of its methods. This matches the typical use case where only
19
+ # one command line is ever processed and makes it possible to create class
20
+ # methods that knows about the command like #error and #fail
21
+ #
22
+ # For example; the following process and convert a command line into a struct
23
+ # representation and also sets ShellOpts.shellopts object so that the #error
24
+ # method can print a relevant usage string:
25
+ #
26
+ # USAGE = "a,all f,file=FILE -- ARG1 ARG2"
27
+ # opts, args = ShellOpts.as_struct(USAGE, ARGV)
28
+ # File.exist?(opts.file) or error "Can't find #{opts.file}"
29
+ #
30
+ # The command line is processed through one of the methods #process, #as_array,
31
+ # #as_hash, or #as_struct that returns a [data, args] tuple. The data type
32
+ # depends on the method: #process yields a Idr object that internally serves as
33
+ # the base for the #as_array and #as_hash and #as_struct that converts it into
34
+ # an Array, Hash, or ShellOpts::OptionStruct object. For example:
35
+ #
36
+ # USAGE = "..."
37
+ # ShellOpts.process(USAGE, ARGV)
38
+ # program, args = ShellOpts.as_program(USAGE, ARGV)
39
+ # array, args = ShellOpts.as_array(USAGE, ARGV)
40
+ # hash, args = ShellOpts.as_hash(USAGE, ARGV)
41
+ # struct, args = ShellOpts.as_struct(USAGE, ARGV)
10
42
  #
11
- # ShellOpts inject the constant PROGRAM into the global scope. It contains the
43
+ # ShellOpts can raise the exception CompilerError is there is an error in the
44
+ # USAGE string. If there is an error in the user supplied command line, #error
45
+ # is called instead and the program terminates with exit code 1. ShellOpts
46
+ # raises ConversionError is there is a name collision when converting to the
47
+ # hash or struct representations. Note that CompilerError and ConversionError
48
+ # are caused by misuse of the library and the problem should be corrected by
49
+ # the developer
50
+ #
51
+ # ShellOpts injects the constant PROGRAM into the global scope. It contains the
12
52
  # name of the program
13
53
  #
14
54
  module ShellOpts
15
- # Return the hidden +ShellOpts::ShellOpts+ object (see .process)
16
- def self.shellopts()
17
- @shellopts
18
- end
55
+ # Base class for ShellOpts exceptions
56
+ class Error < RuntimeError; end
19
57
 
20
- # Prettified usage string used by #error and #fail. Default is +usage+ of
21
- # the current +ShellOpts::ShellOpts+ object
22
- def self.usage() @usage || @shellopts&.usage end
23
-
24
- # Set the usage string
25
- def self.usage=(usage) @usage = usage end
26
-
27
- # Process command line options and arguments. #process takes a usage string
28
- # defining the options and the array of command line arguments to be parsed
29
- # as arguments
30
- #
31
- # If called with a block, the block is called with name and value of each
32
- # option or command and #process returns a list of remaining command line
33
- # arguments. If called without a block a ShellOpts::ShellOpts object is
34
- # returned
35
- #
36
- # The value of an option is its argument, the value of a command is an array
37
- # of name/value pairs of options and subcommands. Option values are converted
38
- # to the target type (String, Integer, Float) if specified
39
- #
40
- # Example
41
- #
42
- # # Define options
43
- # USAGE = 'a,all g,global +v,verbose h,help save! snapshot f,file=FILE h,help'
44
- #
45
- # # Define defaults
46
- # all = false
47
- # global = false
48
- # verbose = 0
49
- # save = false
50
- # snapshot = false
51
- # file = nil
52
- #
53
- # # Process options
54
- # argv = ShellOpts.process(USAGE, ARGV) do |name, value|
55
- # case name
56
- # when '-a', '--all'; all = true
57
- # when '-g', '--global'; global = value
58
- # when '-v', '--verbose'; verbose += 1
59
- # when '-h', '--help'; print_help(); exit(0)
60
- # when 'save'
61
- # save = true
62
- # value.each do |name, value|
63
- # case name
64
- # when '--snapshot'; snapshot = true
65
- # when '-f', '--file'; file = value
66
- # when '-h', '--help'; print_save_help(); exit(0)
67
- # end
68
- # end
69
- # else
70
- # raise "Not a user error. The developer forgot or misspelled an option"
71
- # end
72
- # end
73
- #
74
- # # Process remaining arguments
75
- # argv.each { |arg| ... }
76
- #
77
- # If an error is encountered while compiling the usage string, a
78
- # +ShellOpts::Compiler+ exception is raised. If the error happens while
79
- # parsing the command line arguments, the program prints an error message and
80
- # exits with status 1. Failed assertions raise a +ShellOpts::InternalError+
81
- # exception
82
- #
83
- # Note that you can't process more than one command line at a time because
84
- # #process saves a hidden {ShellOpts::ShellOpts} class variable used by the
85
- # class methods #error and #fail. Call #reset to clear the global object if
86
- # you really need to parse more than one command line. Alternatively you can
87
- # create +ShellOpts::ShellOpts+ objects yourself and also use the object methods
88
- # #error and #fail:
89
- #
90
- # shellopts = ShellOpts::ShellOpts.new(USAGE, ARGS)
91
- # shellopts.each { |name, value| ... }
92
- # shellopts.args.each { |arg| ... }
93
- # shellopts.error("Something went wrong")
94
- #
95
- # Use #shellopts to get the hidden +ShellOpts::ShellOpts+ object
96
- #
97
- def self.process(usage, argv, program_name: PROGRAM, &block)
98
- if !block_given?
99
- ShellOpts.new(usage, argv, program_name: program_name)
100
- else
101
- @shellopts.nil? or raise InternalError, "ShellOpts class variable already initialized"
102
- @shellopts = ShellOpts.new(usage, argv, program_name: program_name)
103
- @shellopts.each(&block)
104
- @shellopts.args
58
+ # Raised when a syntax error is detected in the usage string
59
+ class CompilerError < Error
60
+ def initialize(start, message)
61
+ super(message)
62
+ set_backtrace(caller(start))
105
63
  end
106
64
  end
107
65
 
108
- # Reset the hidden +ShellOpts::ShellOpts+ class variable so that you can process
109
- # another command line
110
- def self.reset()
111
- @shellopts = nil
112
- @usage = nil
113
- end
66
+ # Raised when an error is detected during conversion from the Idr to array,
67
+ # hash, or struct
68
+ class ConversionError < Error; end
114
69
 
115
- # Print error message and usage string and exit with status 1. It use the
116
- # current ShellOpts object if defined. This method should be called in
117
- # response to user-errors (eg. specifying an illegal option)
118
- #
119
- # If there is no current ShellOpts object +error+ will look for USAGE to make
120
- # it possible to use +error+ before the command line is processed and also as
121
- # a stand-alone error reporting method
122
- def self.error(*msgs)
123
- program = @shellopts&.program_name || PROGRAM
124
- usage_string = usage || (defined?(USAGE) && USAGE ? Grammar.compile(PROGRAM, USAGE).usage : nil)
125
- emit_and_exit(program, @usage.nil?, usage_string, *msgs)
126
- end
70
+ # Raised when an internal error is detected
71
+ class InternalError < Error; end
127
72
 
128
- # Print error message and exit with status 1. It use the current ShellOpts
129
- # object if defined. This method should not be called in response to
130
- # user-errors but system errors (like disk full)
131
- def self.fail(*msgs)
132
- program = @shellopts&.program_name || PROGRAM
133
- emit_and_exit(program, false, nil, *msgs)
134
- end
73
+ # The current compilation object. It is set by #process
74
+ def self.shellopts() @shellopts end
135
75
 
136
- # The compilation object
137
- class ShellOpts
138
- # Name of program
139
- attr_reader :program_name
140
-
141
- # Prettified usage string used by #error and #fail. Shorthand for +grammar.usage+
142
- def usage() @grammar.usage end
143
-
144
- # The grammar compiled from the usage string. If #ast is defined, it's
145
- # equal to ast.grammar
146
- attr_reader :grammar
147
-
148
- # The AST resulting from parsing the command line arguments
149
- attr_reader :ast
150
-
151
- # List of remaining non-option command line arguments. Shorthand for ast.arguments
152
- def args() @ast.arguments end
153
-
154
- # Compile a usage string into a grammar and use that to parse command line
155
- # arguments
156
- #
157
- # +usage+ is the usage string, and +argv+ the command line (typically the
158
- # global ARGV array). +program_name+ is the name of the program and is
159
- # used in error messages. It defaults to the basename of the program
160
- #
161
- # Errors in the usage string raise a CompilerError exception. Errors in the
162
- # argv arguments terminates the program with an error message
163
- def initialize(usage, argv, program_name: File.basename($0))
164
- @program_name = program_name
165
- begin
166
- @grammar = Grammar.compile(program_name, usage)
167
- @ast = Ast.parse(@grammar, argv)
168
- rescue Grammar::Compiler::Error => ex
169
- raise CompilerError.new(5, ex.message)
170
- rescue Ast::Parser::Error => ex
171
- error(ex.message)
172
- end
173
- end
76
+ # Process command line and set and return the shellopts compile object
77
+ def self.process(usage, argv, name: self.name, message: nil)
78
+ @shellopts.nil? or reset
79
+ messenger = message && Messenger.new(name, message, format: :custom)
80
+ @shellopts = ShellOpts.new(usage, argv, name: name, messenger: messenger)
81
+ end
174
82
 
175
- # Unroll the AST into a nested array
176
- def to_a
177
- @ast.values
178
- end
83
+ # Return the internal data representation of the command line (Idr::Program).
84
+ # Note that #as_program that the remaning arguments are accessible through
85
+ # the returned object
86
+ def self.as_program(usage, argv, name: self.name, message: nil)
87
+ process(usage, argv, name: name, message: message)
88
+ [shellopts.idr, shellopts.args]
89
+ end
179
90
 
180
- # Iterate the result as name/value pairs. See {ShellOpts.process} for a
181
- # detailed description
182
- def each(&block)
183
- if block_given?
184
- to_a.each { |*args| yield(*args) }
185
- else
186
- to_a # FIXME: Iterator
187
- end
188
- end
91
+ # Process command line, set current shellopts object, and return a [array, argv]
92
+ # tuple. Returns the representation of the current object if not given any
93
+ # arguments
94
+ def self.as_array(usage, argv, name: self.name, message: nil)
95
+ process(usage, argv, name: name, message: message)
96
+ [shellopts.to_a, shellopts.args]
97
+ end
189
98
 
190
- # Print error message and usage string and exit with status 1. This method
191
- # should be called in response to user-errors (eg. specifying an illegal
192
- # option)
193
- def error(*msgs)
194
- ::ShellOpts.emit_and_exit(program_name, true, usage, msgs)
195
- end
99
+ # Process command line, set current shellopts object, and return a [hash, argv]
100
+ # tuple. Returns the representation of the current object if not given any
101
+ # arguments
102
+ def self.as_hash(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
103
+ process(usage, argv, name: name, message: message)
104
+ [shellopts.to_hash(use: use, aliases: aliases), shellopts.args]
105
+ end
196
106
 
197
- # Print error message and exit with status 1. This method should not be
198
- # called in response to user-errors but system errors (like disk full)
199
- def fail(*msgs)
200
- ::ShellOpts.emit_and_exit(program_name, false, nil, msgs)
201
- end
107
+ # Process command line, set current shellopts object, and return a [struct, argv]
108
+ # tuple. Returns the representation of the current object if not given any
109
+ # arguments
110
+ def self.as_struct(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
111
+ process(usage, argv, name: name, message: message)
112
+ [shellopts.to_struct(use: use, aliases: aliases), shellopts.args]
202
113
  end
203
114
 
204
- # Base class for ShellOpts exceptions
205
- class Error < RuntimeError; end
115
+ # Process command line, set current shellopts object, and then iterate
116
+ # options and commands as an array. Returns an enumerator to the array
117
+ # representation of the current shellopts object if not given a block
118
+ # argument
119
+ def self.each(usage = nil, argv = nil, name: self.name, message: nil, &block)
120
+ process(usage, argv, name: name, message: message)
121
+ shellopts.each(&block)
122
+ end
206
123
 
207
- # Raised when an error is detected in the usage string
208
- class CompilerError < Error
209
- def initialize(start, message)
210
- super(message)
211
- set_backtrace(caller(start))
212
- end
124
+ # Print error message and usage string and exit with status 1. This method
125
+ # should be called in response to user-errors (eg. specifying an illegal
126
+ # option)
127
+ def self.error(*msgs)
128
+ raise "Oops" if shellopts.nil?
129
+ shellopts.error(*msgs)
213
130
  end
214
131
 
215
- # Raised when an internal error is detected
216
- class InternalError < Error; end
132
+ # Print error message and exit with status 1. This method should not be
133
+ # called in response to system errors (eg. disk full)
134
+ def self.fail(*msgs)
135
+ raise "Oops" if shellopts.nil?
136
+ shellopts.fail(*msgs)
137
+ end
217
138
 
218
139
  private
219
- @shellopts = nil
220
-
221
- def self.emit_and_exit(program, use_usage, usage, *msgs)
222
- $stderr.puts "#{program}: #{msgs.join}"
223
- if use_usage
224
- $stderr.puts "Usage: #{program} #{usage}" if usage
225
- else
226
- $stderr.puts usage if usage
227
- end
228
- exit 1
140
+ # Reset state variables
141
+ def self.reset()
142
+ @shellopts = nil
229
143
  end
230
- end
231
144
 
232
- PROGRAM = File.basename($PROGRAM_NAME)
145
+ @shellopts = nil
146
+ 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
+
@@ -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
+
@@ -0,0 +1,245 @@
1
+
2
+ require 'shellopts/shellopts.rb'
3
+ require 'shellopts/idr'
4
+
5
+ module ShellOpts
6
+ class OptionStruct < BasicObject
7
+ # +key=:name+ cause command methods to be named without the exclamation
8
+ # mark. It doesn't change how options are named
9
+ def self.new(idr, key = :key, aliases = {})
10
+ ast = idr.instance_variable_get("@ast")
11
+ grammar = ast.grammar
12
+ instance = allocate
13
+
14
+ # Generate option accessor methods
15
+ grammar.option_list.each { |option|
16
+ key = alias_key(option.key, aliases)
17
+ instance.instance_eval("def #{key}() @#{key} end")
18
+ present = set_variable(instance, "@#{key}", idr[option.key])
19
+ instance.instance_eval("def #{key}?() #{present} end")
20
+ }
21
+
22
+ # Generate #subcommand default methods
23
+ if !idr.subcommand
24
+ instance.instance_eval("def subcommand() nil end")
25
+ instance.instance_eval("def subcommand?() false end")
26
+ instance.instance_eval("def subcommand!() nil end")
27
+ end
28
+
29
+ # Generate subcommand methods
30
+ grammar.command_list.each { |command|
31
+ key = alias_key(command.key, aliases)
32
+ if command.key == idr.subcommand&.key
33
+ struct = OptionStruct.new(idr.subcommand, aliases[idr.subcommand.key] || {})
34
+ set_variable(instance, "@subcommand", struct)
35
+ instance.instance_eval("def #{key}() @subcommand end")
36
+ instance.instance_eval("def subcommand() :#{key} end")
37
+ instance.instance_eval("def subcommand?() true end")
38
+ instance.instance_eval("def subcommand!() @subcommand end")
39
+ else
40
+ instance.instance_eval("def #{key}() nil end")
41
+ end
42
+ }
43
+
44
+ instance
45
+ end
46
+
47
+ private
48
+ # Return class of object. #class is not defined for BasicObjects so this
49
+ # method provides an alternative way of getting the class
50
+ def self.class_of(object)
51
+ # https://stackoverflow.com/a/18621313/2130986
52
+ ::Kernel.instance_method(:class).bind(object).call
53
+ end
54
+
55
+ # Replace key with alias and check against the list of reserved words
56
+ def self.alias_key(internal_key, aliases)
57
+ key = aliases[internal_key] || internal_key
58
+ !RESERVED_WORDS.include?(key.to_s) or
59
+ raise ::ShellOpts::ConversionError, "Can't create struct: '#{key}' is a reserved word"
60
+ key
61
+ end
62
+
63
+ # Shorthand helper method. Substitutes the undefined ObjectStruct#instance_variable_set
64
+ def self.set_variable(this, var, value)
65
+ # https://stackoverflow.com/a/18621313/2130986
66
+ ::Kernel.instance_method(:instance_variable_set).bind(this).call(var, value)
67
+ end
68
+
69
+ BASIC_OBJECT_RESERVED_WORDS = %w(
70
+ __id__ __send__ instance_eval instance_exec method_missing
71
+ singleton_method_added singleton_method_removed
72
+ singleton_method_undefined)
73
+ OPTIONS_STRUCT_RESERVED_WORDS = %w(subcommand)
74
+ RESERVED_WORDS = BASIC_OBJECT_RESERVED_WORDS + OPTIONS_STRUCT_RESERVED_WORDS
75
+ end
76
+ end
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+ __END__
86
+
87
+ module ShellOpts
88
+ # Struct representation of options. Usually created by ShellOpts::to_struct
89
+ #
90
+ # OptionStruct objects give easy access to configuration option values but
91
+ # meta data are more circuitously accessed through class methods with an
92
+ # explicit instance argument
93
+ #
94
+ # Option values are accessed through a member methods named after the key of
95
+ # the option. Repeated options have an Array value with one element (possibly
96
+ # nil) for each use of the option. A query method with a '?' suffixed to the
97
+ # name returns true or false depending on whether the option was used or not
98
+ #
99
+ # option - Value of option. Either an object or an Array if the option can
100
+ # be repeated
101
+ # option? - True iff option was given
102
+ #
103
+ # Command methods return a nested OptionStruct object while the special
104
+ # #command method returns the key of actual command (if any). Use
105
+ # +strukt.send(strukt.command)+ to get the subcommand of a OptionStruct. It
106
+ # is possible to rename #command method to avoid name collisions
107
+ #
108
+ # name! - Command. An OptionStruct or nil if not given on the command line
109
+ # subcommand - Key of command. Can be renamed
110
+ #
111
+ # ---------------------------------
112
+ # name! - Command. An OptionStruct or nil if not given on the command line
113
+ #
114
+ # key! - Key of command
115
+ # value! - Value of command (a subcommand). Can be renamed
116
+ #
117
+ # Note: There is no command query method because option and command names
118
+ # live in seperate namespaces and could cause colllisions. Check +name!+ for
119
+ # nil to detect if a command was given
120
+ #
121
+ # Meta data are extracted through class methods to avoid polluting the object
122
+ # namespace. OptionStruct use an OptionsHash object internally and
123
+ # implements a subset of its meta methods by forwarding to it. The
124
+ # OptionsHash object can be accessed through the #options_hash method
125
+ #
126
+ # Note that #command is defined as both an instance method and a class
127
+ # method. Use the class method to make the code work with all OptionStruct
128
+ # objects even if #command has been renamed
129
+ #
130
+ # +ShellOpts+ is derived from +BascicObject+ that reserves some words for
131
+ # internal use (+__id__+, +__send__+, +instance_eval+, +instance_exec+,
132
+ # +method_missing+, +singleton_method_added+, +singleton_method_removed+,
133
+ # +singleton_method_undefined+). ShellOpts also define two reserved words of
134
+ # its own (+__options_hash__+ and +__command__+). ShellOpts raise an
135
+ # ShellOpts::ConversionError if an option collides with one of the
136
+ # reserved words or with the special #command method
137
+ #
138
+ class OptionStruct < BasicObject
139
+ # Create a new OptionStruct instance from an AST. The optional
140
+ # +options_hash+ argument is used to create subcommands without creating a
141
+ # new options_hash argument. It is not meant for end-users. The
142
+ # +command_alias+ names the method holding the key for the subcommand (if
143
+ # any)
144
+ def self.new(ast, options_hash = OptionsHash.new(ast), command_alias: :command)
145
+ instance = allocate
146
+ set_variable(instance, "@__options_hash__", options_hash)
147
+
148
+ # Check for reserved words and +command_alias+
149
+ options_hash.keys.each { |key|
150
+ !RESERVED_WORDS.include?(key.to_s) or
151
+ raise ::ShellOpts::ConversionError, "Can't create struct: '#{key}' is a reserved word"
152
+ key != command_alias or
153
+ raise ::ShellOpts::ConversionError, "Can't create struct: '#{key}' is the command alias"
154
+ }
155
+
156
+ # Create accessor methods
157
+ ast.grammar.option_list.each { |option|
158
+ instance.instance_eval("def #{option.key}() @#{option.key} end")
159
+ instance.instance_eval("def #{option.key}?() false end")
160
+ }
161
+ ast.grammar.command_list.each { |command|
162
+ instance.instance_eval("def #{command.key}() nil end")
163
+ }
164
+
165
+ # Assign values
166
+ options_hash.each { |key, value|
167
+ if value.is_a?(OptionsHash)
168
+ set_variable(instance, "@__command__", OptionStruct.new(value.ast, value))
169
+ instance.instance_eval("def #{key}() @__command__ end")
170
+ else
171
+ set_variable(instance, "@#{key}", value)
172
+ instance.instance_eval("def #{key}?() true end")
173
+ end
174
+ }
175
+
176
+ # Command accessor method
177
+ instance.instance_eval("def #{command_alias}() @__options_hash__.command end")
178
+
179
+ instance
180
+ end
181
+
182
+ # Return the OptionsHash object from the instance
183
+ def self.options_hash(instance)
184
+ get_variable(instance, "@__options_hash__")
185
+ end
186
+
187
+ # Return class of object. #class is not defined for BasicObjects so this
188
+ # method provides an alternative way of getting the class a BasicObject
189
+ def self.class_of(object)
190
+ # https://stackoverflow.com/a/18621313/2130986
191
+ ::Kernel.instance_method(:class).bind(object).call
192
+ end
193
+
194
+ # Return the number of options and commands
195
+ def self.size(instance)
196
+ options_hash(instance).size
197
+ end
198
+
199
+ # Return the option and command keys. The keys are in order of occurrence
200
+ # on the command line. A subcommand will always be the last element
201
+ def self.keys(instance)
202
+ options_hash(instance).keys
203
+ end
204
+
205
+ # Return the actual option name used on the command line for +name+. Use
206
+ # +index+ to select between repeated options. Return the name of the
207
+ # program/subcommand if key is nil
208
+ def self.name(struct, key = nil, index = nil)
209
+ options_hash(struct).name(key, index)
210
+ end
211
+
212
+ # Return the AST node for the option key or the AST node for the
213
+ # OptionStruct if key is nil. Use +index+ to select between repeated
214
+ # options. Raise InternalError if key doesn't exists
215
+ def self.node(struct, key = nil, index = nil)
216
+ options_hash(struct).node(key, index)
217
+ end
218
+
219
+ # Return key of the command of the struct (possibly nil)
220
+ def self.command(struct)
221
+ options_hash(struct).command
222
+ end
223
+
224
+ private
225
+ BASIC_OBJECT_RESERVED_WORDS = %w(
226
+ __id__ __send__ instance_eval instance_exec method_missing
227
+ singleton_method_added singleton_method_removed
228
+ singleton_method_undefined)
229
+ OPTIONS_STRUCT_RESERVED_WORDS = %w(__options_hash__ __command__)
230
+ RESERVED_WORDS = BASIC_OBJECT_RESERVED_WORDS + OPTIONS_STRUCT_RESERVED_WORDS
231
+
232
+ # Shorthand helper method. Substitutes the undefined ObjectStruct#instance_variable_set
233
+ def self.set_variable(this, var, value)
234
+ # https://stackoverflow.com/a/18621313/2130986
235
+ ::Kernel.instance_method(:instance_variable_set).bind(this).call(var, value)
236
+ end
237
+
238
+ # Shorthand helper method: Substitutes the undefined ObjectStruct#instance_variable_get
239
+ def self.get_variable(this, var)
240
+ # https://stackoverflow.com/a/18621313/2130986
241
+ ::Kernel.instance_method(:instance_variable_get).bind(this).call(var)
242
+ end
243
+ end
244
+ end
245
+
@@ -0,0 +1,98 @@
1
+
2
+ require "shellopts"
3
+
4
+ # TODO
5
+ #
6
+ # PROCESSING
7
+ # 1. Compile usage string and yield a grammar
8
+ # 2. Parse the options using the grammar and yield an AST
9
+ # 3. Construct the Program model from the AST
10
+ # 4. Apply defaults to the model
11
+ # 6. Run validations on the model
12
+ # 5. Create representation from the model
13
+ #
14
+
15
+ module ShellOpts
16
+ # The command line processing object
17
+ class ShellOpts
18
+ # One of :key, :name, :option
19
+ #
20
+ # Option Command
21
+ # :key key #command! (no collision)
22
+ # :name name #command (possible collision)
23
+ # :option --option #command (multihash, no collision) (TODO)
24
+ #
25
+ DEFAULT_USE = :key
26
+
27
+ # Name of program
28
+ attr_reader :name
29
+
30
+ # The grammar compiled from the usage string
31
+ attr_reader :grammar
32
+
33
+ # The AST parsed from the command line arguments
34
+ attr_reader :ast
35
+
36
+ # The IDR generated from the Ast
37
+ attr_reader :idr
38
+
39
+ # Object for error & fail messages. Default is to write a message on
40
+ # standard error and exit with status 1
41
+ attr_accessor :messenger
42
+
43
+ # Compile a usage string into a grammar and use that to parse command line
44
+ # arguments
45
+ #
46
+ # +usage+ is the usage string, and +argv+ the command line (typically the
47
+ # global ARGV array). +name+ is the name of the program and defaults to the
48
+ # basename of the program
49
+ #
50
+ # Syntax errors in the usage string are caused by the developer and raise a
51
+ # +ShellOpts::CompilerError+ exception. Errors in the +argv+ arguments are
52
+ # caused by the user and terminates the program with an error message and a
53
+ # short description of its usage
54
+ def initialize(usage, argv, name: PROGRAM, messenger: nil)
55
+ @name = name
56
+ begin
57
+ @grammar = Grammar.compile(name, usage)
58
+ @messenger = messenger || Messenger.new(name, @grammar.usage)
59
+ @ast = Ast.parse(@grammar, argv)
60
+ @idr = Idr.generate(@ast, @messenger)
61
+ rescue Grammar::Compiler::Error => ex
62
+ raise CompilerError.new(5, ex.message)
63
+ rescue Ast::Parser::Error => ex
64
+ error(ex.message)
65
+ end
66
+ end
67
+
68
+ # Return an array representation of options and commands in the same order
69
+ # as on the command line. Each option or command is represented by a [name,
70
+ # value] pair. The value of an option is be nil if the option didn't have
71
+ # an argument and else either a String, Integer, or Float. The value of a
72
+ # command is an array of its options and commands
73
+ def to_a() idr.to_a end
74
+
75
+ # Return a hash representation of the options. See {ShellOpts::OptionsHash}
76
+ def to_h(use: :key, aliases: {}) @idr.to_h(use: use, aliases: aliases) end
77
+
78
+ # Return a struct representation of the options. See {ShellOpts::OptionStruct}
79
+ def to_struct(use: :key, aliases: {}) @idr.to_struct(use: use, aliases: aliases) end
80
+
81
+ # List of remaining non-option command line arguments. Shorthand for +ast&.arguments+
82
+ def args() @ast&.arguments end
83
+
84
+ # Iterate options and commands as name/value pairs. Same as +to_a.each+
85
+ def each(&block) to_a.each(&block) end
86
+
87
+ # Print error messages and usage string and exit with status 1. This method
88
+ # should be called in response to user-errors (eg. specifying an illegal
89
+ # option)
90
+ def error(*msgs) @messenger.error(*msgs) end
91
+
92
+ # Print error message and exit with status 1. This method should called in
93
+ # response to system errors (like disk full)
94
+ def fail(*msgs) @messenger.fail(*msgs) end
95
+ end
96
+ end
97
+
98
+
@@ -1,3 +1,3 @@
1
1
  module Shellopts
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0-1"
3
3
  end
data/rs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/bash
2
+
3
+ PROGRAM=$(basename $0)
4
+ USAGE="SOURCE-FILE"
5
+
6
+ function error() {
7
+ echo "$PROGRAM: $@"
8
+ echo "Usage: $PROGRAM $USAGE"
9
+ exit 1
10
+ } >&2
11
+
12
+ [ $# = 1 ] || error "Illegal number of arguments"
13
+ SOURCE_NAME=${1%.rb}.rb
14
+
15
+ GEM_FILE=$(ls *.gemspec 2>/dev/null)
16
+ [ -n "$GEM_FILE" ] || error "Can't find gemspec file"
17
+ GEM_NAME=${GEM_FILE%.gemspec}
18
+
19
+ if [ -f lib/$SOURCE_NAME ]; then
20
+ SOURCE_FILE=lib/$SOURCE_NAME
21
+ elif [ -f lib/$GEM_NAME/$SOURCE_NAME ]; then
22
+ SOURCE_FILE=lib/$GEM_NAME/$SOURCE_NAME
23
+ else
24
+ SOURCE_FILE=$(find lib/$GEM_NAME -type f -path $SOURCE_NAME | head -1)
25
+ if [ -z "$SOURCE_FILE" ]; then
26
+ SOURCE_FILE=lib/$GEM_NAME/$SOURCE_NAME
27
+ fi
28
+ fi
29
+
30
+ SPEC_FILE=spec/${SOURCE_NAME%.rb}_spec.rb
31
+ [ -f $SPEC_FILE ] || error "Can't find spec file '$SPEC_FILE'"
32
+
33
+ rspec --fail-fast $SPEC_FILE || {
34
+ # rcov forgets a newline when rspec fails
35
+ status=$?; echo; exit $status;
36
+ }
37
+
38
+
39
+
40
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shellopts
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-20 00:00:00.000000000 Z
11
+ date: 2020-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -111,13 +111,19 @@ files:
111
111
  - lib/shellopts/ast/option.rb
112
112
  - lib/shellopts/ast/program.rb
113
113
  - lib/shellopts/compiler.rb
114
+ - lib/shellopts/generator.rb
114
115
  - lib/shellopts/grammar/command.rb
115
116
  - lib/shellopts/grammar/node.rb
116
117
  - lib/shellopts/grammar/option.rb
117
118
  - lib/shellopts/grammar/program.rb
119
+ - lib/shellopts/idr.rb
120
+ - lib/shellopts/messenger.rb
121
+ - lib/shellopts/option_struct.rb
118
122
  - lib/shellopts/parser.rb
123
+ - lib/shellopts/shellopts.rb
119
124
  - lib/shellopts/utils.rb
120
125
  - lib/shellopts/version.rb
126
+ - rs
121
127
  - shellopts.gemspec
122
128
  homepage: http://github.com/clrgit/shellopts
123
129
  licenses: []
@@ -134,9 +140,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
140
  version: '0'
135
141
  required_rubygems_version: !ruby/object:Gem::Requirement
136
142
  requirements:
137
- - - ">="
143
+ - - ">"
138
144
  - !ruby/object:Gem::Version
139
- version: '0'
145
+ version: 1.3.1
140
146
  requirements: []
141
147
  rubygems_version: 3.0.8
142
148
  signing_key: