shellopts 1.0.0 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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: