shellopts 1.0.0 → 2.0.0.pre.7

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: 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)