shellopts 1.0.0 → 2.0.0.pre.7

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: c7ac01e4c6feb1897f74056a36c74db0cf78802d13437908aa18631ead7c97c5
4
+ data.tar.gz: 5443ff421ceb38fcf07aaf87ae7b3740fd804679ccdec377fb195c688c50b57b
5
5
  SHA512:
6
- metadata.gz: d3c501335a899e4b14280cbb14b7f9e8a0d9ef28715760394d8f13ea837d0a5d3d9b91b9d07dcb42a747753e97a10c157ae5c5b20b95804b1746e0ebad7d88c9
7
- data.tar.gz: 387c888158bbbebe127c6efde55c7bbb14436b7dcb8227cac38e2c9dac85e9a3d956c062e04b86101e28fa0bde4977fefadad4d1a643dd48d9218e5c04558ad2
6
+ metadata.gz: 0d8180a2acc6dac8e234567db9409cd9d01cebe8d4551f6cfa24488ce5d1fb848ec8b42b21d40e7d8f6990bf61e4dc8d482370e9cb4ceb409e3f18fdb70a877c
7
+ data.tar.gz: f07d22ab7976d5efa317356fc796f8bbe20b8c6a8ee609eb24106f60e8e56d9874181dcab4e873268b1aea5a1aa27ffd1253bb10499052edd995894c6eec9d88
@@ -1 +1 @@
1
- ruby-2.5.1
1
+ ruby-2.6.6
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,23 @@
1
1
 
2
2
  TODO
3
+ o Rethink #error and #fail <- The use-case is one-file ruby scripts. Idea: Only use in main exe file?
4
+ o Create exceptions: UserError SystemFail and allow them to be used instead of #error and #fail
5
+ ? Remove ! from OptionStruct#subcommand return value. We know we're
6
+ processing commands so there is no need to have a distinct name and it
7
+ feels a lot more intuitive without it
8
+ o Add validation block to ShellOpts class methods
9
+ o Get rid of key_name. Define #name on Grammar::Node instead
10
+ o Define #name to the string name of the option/command without prefixed '--'
11
+ for options. This can cause collisions but they can be avoided using aliases
12
+ o Clean-up
13
+ o Grammar::options -> Grammar::option_multihash
14
+ o Clean-up identifiers etc.
15
+ o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
16
+ o subcommand vs. command consistency
17
+ o Implement ObjectStruct#key! and ObjectStruct#value! (?)
18
+ o Allow command_alias == nil to suppress the method
19
+ o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
20
+ o Use hash_tree
3
21
  o Also allow assignment to usage string for ShellOpts::ShellOpts objects
4
22
  o Create a ShellOpts.args method? It would be useful when processing commands:
5
23
  case opt
@@ -9,7 +27,10 @@ TODO
9
27
  ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
10
28
  Another option would be to create an argument-processing method:
11
29
  shellopts.argv(2) -> call error if not exactly two arguments else return elements
12
-
30
+ o Add a ShellOpts.option method:
31
+ file = ShellOpts.option("--file")
32
+ This will only work for options on the outermost level... maybe:
33
+ file = ShellOpts.option("load! --file")
13
34
  o Check on return value from #process block to see if all options was handled:
14
35
  case opt
15
36
  when '-v'; verbose = true # Return value 'true' is ok
@@ -24,6 +45,7 @@ TODO
24
45
  o Long version usage strings (major release)
25
46
  o Doc: Example of processing of sub-commands and sub-sub-commands
26
47
 
48
+ + Add a 'mandatory' argument to #subcommand
27
49
  + More tests
28
50
  + More doc
29
51
  + Implement value-name-before-flags rule
@@ -2,231 +2,241 @@ require "shellopts/version"
2
2
 
3
3
  require 'shellopts/compiler.rb'
4
4
  require 'shellopts/parser.rb'
5
- require 'shellopts/utils.rb'
5
+ require 'shellopts/generator.rb'
6
+ require 'shellopts/option_struct.rb'
7
+ require 'shellopts/main.rb'
6
8
 
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}
9
+ # Name of program. Defined as the basename of the program file
10
+ #PROGRAM = File.basename($PROGRAM_NAME)
11
+
12
+ # ShellOpts main Module
13
+ #
14
+ # This module contains methods to process command line options and arguments.
15
+ # ShellOpts keeps a reference in ShellOpts.shellopts to the result of the last
16
+ # command that was processed through its interface and use it as the implicit
17
+ # object of many of its methods. This matches the typical use case where only
18
+ # one command line is ever processed and makes it possible to create class
19
+ # methods that knows about the command like #error and #fail
20
+ #
21
+ # For example; the following process and convert a command line into a struct
22
+ # representation and also sets ShellOpts.shellopts object so that the #error
23
+ # method can print a relevant spec string:
24
+ #
25
+ # USAGE = "a,all f,file=FILE -- ARG1 ARG2"
26
+ # opts, args = ShellOpts.as_struct(USAGE, ARGV)
27
+ # File.exist?(opts.file) or error "Can't find #{opts.file}"
28
+ #
29
+ # The command line is processed through one of the methods #process, #as_array,
30
+ # #as_hash, or #as_struct that returns a [data, args] tuple. The data type
31
+ # depends on the method: #process yields a Idr object that internally serves as
32
+ # the base for the #as_array and #as_hash and #as_struct that converts it into
33
+ # an Array, Hash, or ShellOpts::OptionStruct object. For example:
34
+ #
35
+ # USAGE = "..."
36
+ # ShellOpts.process(USAGE, ARGV)
37
+ # program, args = ShellOpts.as_program(USAGE, ARGV)
38
+ # array, args = ShellOpts.as_array(USAGE, ARGV)
39
+ # hash, args = ShellOpts.as_hash(USAGE, ARGV)
40
+ # struct, args = ShellOpts.as_struct(USAGE, ARGV)
41
+ #
42
+ # +args+ is a ShellOpts::Argv object containing the the remaning command line
43
+ # arguments. Argv is derived from Array
44
+ #
45
+ # ShellOpts can raise the exception CompilerError is there is an error in the
46
+ # USAGE string. If there is an error in the user supplied command line, #error
47
+ # is called instead and the program terminates with exit code 1. ShellOpts
48
+ # raises ConversionError is there is a name collision when converting to the
49
+ # hash or struct representations. Note that CompilerError and ConversionError
50
+ # are caused by misuse of the library and the problem should be corrected by
51
+ # the developer
10
52
  #
11
- # ShellOpts inject the constant PROGRAM into the global scope. It contains the
53
+ # ShellOpts injects the constant PROGRAM into the global scope. It contains the
12
54
  # name of the program
13
55
  #
56
+ # INCLUDING SHELLOPTS
57
+ #
58
+ # ShellOpts can optionally be included in your shell application main file but
59
+ # it is not supposed to be included anywhere else
60
+ #
61
+ # Some behind the scenes magic happen if you include the ShellOpts module in your
62
+ # main exe file
63
+ #
14
64
  module ShellOpts
15
- # Return the hidden +ShellOpts::ShellOpts+ object (see .process)
16
- def self.shellopts()
17
- @shellopts
18
- end
19
-
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
105
- end
65
+ def self.default_name()
66
+ @default_name || defined?(PROGRAM) ? PROGRAM : File.basename($0)
106
67
  end
107
68
 
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
114
-
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
127
-
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
135
-
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
69
+ def self.default_name=(name)
70
+ @default_name = name
71
+ end
174
72
 
175
- # Unroll the AST into a nested array
176
- def to_a
177
- @ast.values
178
- end
73
+ def self.default_usage()
74
+ @default_usage || defined?(USAGE) ? USAGE : nil
75
+ end
179
76
 
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
77
+ def self.default_usage=(usage)
78
+ @default_usage = usage
79
+ end
189
80
 
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
81
+ def self.default_key_type()
82
+ @default_key_type || ::ShellOpts::DEFAULT_KEY_TYPE
83
+ end
196
84
 
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
85
+ def self.default_key_type=(type)
86
+ @default_key_type = type
202
87
  end
203
88
 
204
89
  # Base class for ShellOpts exceptions
205
90
  class Error < RuntimeError; end
206
91
 
207
- # Raised when an error is detected in the usage string
92
+ # Raised when a syntax error is detected in the spec string
208
93
  class CompilerError < Error
209
- def initialize(start, message)
210
- super(message)
94
+ def initialize(start, usage)
95
+ super(usage)
211
96
  set_backtrace(caller(start))
212
97
  end
213
98
  end
214
99
 
100
+ # Raised when an error is detected in the command line
101
+ class ParserError < Error; end
102
+
103
+ # Raised when the command line error is caused by the user. It is raised by
104
+ # the parser but can also be used by the application if the command line
105
+ # fails a semantic check
106
+ class UserError < ParserError; end
107
+
108
+ # Raised when the error is caused by a failed assumption about the system. It
109
+ # is not raised by the ShellOpts library as it only concerns itself with
110
+ # command line syntax but can be used by the application to report a failure
111
+ # through ShellOpts#fail method when the ShellOpts module is included
112
+ class SystemFail < Error; end
113
+
114
+ # Raised when an error is detected during conversion from the Idr to array,
115
+ # hash, or struct
116
+ class ConversionError < Error; end
117
+
215
118
  # Raised when an internal error is detected
216
119
  class InternalError < Error; end
217
120
 
218
- private
219
- @shellopts = nil
121
+ # The current compilation object. It is set by #process
122
+ def self.shellopts() @shellopts end
220
123
 
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
124
+ # Name of program
125
+ def program_name() shellopts!.name end
126
+ def program_name=(name) shellopts!.name = name end
127
+
128
+ # Usage string
129
+ def usage() shellopts!.spec end
130
+ def usage=(spec) shellopts!.spec = spec end
131
+
132
+ # Process command line, set current shellopts object, and return it.
133
+ # Remaining arguments from the command line can be accessed through
134
+ # +shellopts.args+
135
+ def self.process(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
136
+ @shellopts.nil? or reset
137
+ @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
138
+ end
139
+
140
+ # Process command line, set current shellopts object, and return a
141
+ # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
142
+ # if called from the main Ruby object (ie. your executable)
143
+ def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
144
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
145
+ process(spec, argv, name: name, usage: usage)
146
+ [shellopts.idr, shellopts.args]
147
+ end
148
+
149
+ # Process command line, set current shellopts object, and return a [array,
150
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
151
+ # main Ruby object (ie. your executable)
152
+ def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
153
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
154
+ process(spec, argv, name: name, usage: usage)
155
+ [shellopts.to_a, shellopts.args]
156
+ end
157
+
158
+ # Process command line, set current shellopts object, and return a [hash,
159
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
160
+ # main Ruby object (ie. your executable)
161
+ def self.as_hash(
162
+ spec, argv,
163
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
164
+ key_type: ::ShellOpts.default_key_type,
165
+ aliases: {})
166
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
167
+ process(spec, argv, name: name, usage: usage)
168
+ [shellopts.to_h(key_type: key_type, aliases: aliases), shellopts.args]
169
+ end
170
+
171
+ # Process command line, set current shellopts object, and return a [struct,
172
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
173
+ # main Ruby object (ie. your executable)
174
+ def self.as_struct(
175
+ spec, argv,
176
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
177
+ aliases: {})
178
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
179
+ process(spec, argv, name: name, usage: usage)
180
+ [shellopts.to_struct(aliases: aliases), shellopts.args]
181
+ end
182
+
183
+ # Process command line, set current shellopts object, and then iterate
184
+ # options and commands as an array. Returns an enumerator to the array
185
+ # representation of the current shellopts object if not given a block
186
+ # argument. Automatically includes the ShellOpts module if called from the
187
+ # main Ruby object (ie. your executable)
188
+ def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
189
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
190
+ process(spec, argv, name: name, usage: usage)
191
+ shellopts.each(&block)
192
+ end
193
+
194
+ # Print error usage and spec string and exit with status 1. This method
195
+ # should be called in response to user-errors (eg. specifying an illegal
196
+ # option)
197
+ def self.error(*msgs, exit: true)
198
+ shellopts!.error(msgs, exit: exit)
199
+ end
200
+
201
+ # Print error usage and exit with status 1. This method should not be
202
+ # called in response to system errors (eg. disk full)
203
+ def self.fail(*msgs, exit: true)
204
+ shellopts!.fail(*msgs, exit: exit)
205
+ end
206
+
207
+ def self.included(base)
208
+ # base.equal?(Object) is only true when included in main (we hope)
209
+ if !@is_included_in_main && base.equal?(Object)
210
+ @is_included_in_main = true
211
+ at_exit do
212
+ case $!
213
+ when ShellOpts::UserError
214
+ ::ShellOpts.error($!.message, exit: false)
215
+ exit!(1)
216
+ when ShellOpts::SystemFail
217
+ ::ShellOpts.fail($!.message)
218
+ exit!(1)
219
+ end
220
+ end
227
221
  end
228
- exit 1
222
+ super
223
+ end
224
+
225
+ private
226
+ # Default default key type
227
+ DEFAULT_KEY_TYPE = :name
228
+
229
+ # Reset state variables
230
+ def self.reset()
231
+ @shellopts = nil
229
232
  end
233
+
234
+ # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
235
+ def self.shellopts!
236
+ ::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
237
+ end
238
+
239
+ @shellopts = nil
240
+ @is_included_in_main = false
230
241
  end
231
242
 
232
- PROGRAM = File.basename($PROGRAM_NAME)