shellopts 0.9.6 → 2.0.0.pre.3

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: 943b9d0c44ba0291937be24cbc79d0155dfc22ac788c69a76639850d78af3303
4
- data.tar.gz: f38e038f613c577b06e9d2facc6096b39eb18f28266ba8c58a8280adcf75fe47
3
+ metadata.gz: 34b7edfee1092f53f5c5c75d678c69b2fe753ecbcd6caa4876c923dd56dfbf68
4
+ data.tar.gz: f7dfa30cd0ec280eb3f36e6a4779e01eeea1d14fd904fe2b42d1b7d7d9e40b63
5
5
  SHA512:
6
- metadata.gz: 42cc6102c0c66dcffda0b03f9c584c32abc0e62a65dbe79e1389753b74af11f189e27a456e79fb2cb082595622ddbce0840dd22419204e276a48134ac8afd610
7
- data.tar.gz: 6a3b2e35718a5ee1f52e5d56a50ea067da90fef35a4ac3322738e24d5ba80f4aba4ac4a692caace4bb094ccfb84432e9211fef2b15dfef2e194a95569ca80686
6
+ metadata.gz: e3d9b271783e3f9c5b42aa2511c36a78814f239e848829fd347ad74fc5289b769da7264defe8af985bce500485d7e9480a240b3f9e261d70943b1ebc69cd671d
7
+ data.tar.gz: 3f91000787d5fa80570d378604a7cc28ef991585030bd1ce327b88e67dca3e4e647428d48863fc302165bcece5130cc2f140efb05fa478c2ff9443a7197a8b4e
data/README.md CHANGED
@@ -7,11 +7,10 @@ line
7
7
 
8
8
  ## Usage
9
9
 
10
- Program that accepts the options -a or --all, --count, --file, and -v or
11
- --verbose. The usage definition expects `--count` to have an optional integer
12
- argument, `--file` to have a mandatory argument, and allows `-v` and
13
- `--verbose` to be repeated:
14
-
10
+ The following program accepts the options -a or --all, --count, --file, and -v
11
+ or --verbose. It expects `--count` to have an optional integer argument,
12
+ `--file` to have a mandatory argument, and allows `-v` and `--verbose` to be
13
+ repeated:
15
14
 
16
15
  ```ruby
17
16
 
@@ -74,7 +73,7 @@ line at a time and to inspect the grammar and AST
74
73
 
75
74
  ```ruby
76
75
  shellopts = ShellOpts.process(USAGE, ARGV) # Returns a ShellOpts::ShellOpts object
77
- shellopts.each { |opt, val| ... } # Access options
76
+ shellopts.each { |opt, arg| ... } # Access options
78
77
  args = shellopts.args # Access remaining arguments
79
78
  shellopts.error "Something went wrong" # Emit an error message and exit
80
79
  ```
@@ -196,11 +195,11 @@ sub-commands) to the command:
196
195
  ```ruby
197
196
  USAGE = "a cmd! b c"
198
197
 
199
- args = ShellOpts.process(USAGE, ARGV) { |opt,val|
198
+ args = ShellOpts.process(USAGE, ARGV) { |opt, arg|
200
199
  case opt
201
200
  when '-a'; # Handle -a
202
201
  when 'cmd'
203
- opt.each { |opt, val|
202
+ arg.each { |opt, arg|
204
203
  case opt
205
204
  when '-b'; # Handle -b
206
205
  when '-c'; # Handle -c
@@ -275,20 +274,24 @@ class methods on `ShellOpts`. They can also be included in the global scope by
275
274
  #### Usage string
276
275
 
277
276
  The error handling methods prints a prettified version of the usage string
278
- given to `ShellOpts.parse`. It can be overridden by assigning to
279
- `ShellOpts.usage`. You'll often assign to the usage string when it needs to be
280
- split over several lines:
277
+ given to `ShellOpts.parse`. The usage string can be overridden by assigning to
278
+ `ShellOpts.usage`. A typical use case is when you want to split the usage
279
+ description over multiple lines:
281
280
 
282
281
  ```ruby
283
282
 
284
283
  USAGE="long-and-complex-usage-string"
285
- ShellOpts.usage = %(
284
+ ShellOpts.usage = <<~EOD
286
285
  usage explanation
287
286
  split over
288
287
  multiple lines
289
- )
288
+ EOD
290
289
  ```
291
290
 
291
+ Note that this only affects the module-level `ShellOpts.error` method and not
292
+ object-level `ShellOpts::ShellOpts#error` method. This is considered a bug and
293
+ will fixed at some point
294
+
292
295
  ## Example
293
296
 
294
297
  The rm(1) command could be implemented like this
@@ -316,12 +319,12 @@ preserve_root = true
316
319
  verbose = false
317
320
 
318
321
  # Process command line
319
- args = ShellOpts.process(USAGE, ARGV) { |opt, val|
322
+ args = ShellOpts.process(USAGE, ARGV) { |opt, arg|
320
323
  case opt
321
324
  when '-f', '--force'; force = true
322
325
  when '-i'; prompt = true
323
326
  when '-I'; prompt_once = true
324
- when '--interactive'; interactive = true; interactive_when = val
327
+ when '--interactive'; interactive = true; interactive_when = arg
325
328
  when '-r', '-R', '--recursive'; recursive = true
326
329
  when '-d', '--dir'; remove_empty_dirs = true
327
330
  when '--one-file-system'; one_file_system = true
@@ -375,6 +378,16 @@ release a new version, update the version number in `version.rb`, and then run
375
378
  git commits and tags, and push the `.gem` file to
376
379
  [rubygems.org](https://rubygems.org).
377
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
+
378
391
  ## Contributing
379
392
 
380
393
  Bug reports and pull requests are welcome on GitHub at
data/TODO CHANGED
@@ -1,5 +1,34 @@
1
1
 
2
2
  TODO
3
+ o Remove ! from OptionStruct#subcommand return value. We know we're
4
+ processing commands so there is no need to have a distinct name and it
5
+ feels a lot more intuitive without it
6
+ o Add validation block to ShellOpts class methods
7
+ o Get rid of key_name. Define #name on Grammar::Node instead
8
+ o Define #name to the string name of the option/command without prefixed '--'
9
+ for options. This can cause collisions but they can be avoided using aliases
10
+ o Clean-up
11
+ o Grammar::options -> Grammar::option_multihash
12
+ o Clean-up identifiers etc.
13
+ o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
14
+ o subcommand vs. command consistency
15
+ o Implement ObjectStruct#key! and ObjectStruct#value! (?)
16
+ o Allow command_alias == nil to suppress the method
17
+ o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
18
+ o Use hash_tree
19
+ o Also allow assignment to usage string for ShellOpts::ShellOpts objects
20
+ o Create a ShellOpts.args method? It would be useful when processing commands:
21
+ case opt
22
+ when "command"
23
+ call_command_method(ShellOpts.args[1], ShellOpts.args[2])
24
+ end
25
+ ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
26
+ Another option would be to create an argument-processing method:
27
+ shellopts.argv(2) -> call error if not exactly two arguments else return elements
28
+ o Add a ShellOpts.option method:
29
+ file = ShellOpts.option("--file")
30
+ This will only work for options on the outermost level... maybe:
31
+ file = ShellOpts.option("load! --file")
3
32
  o Check on return value from #process block to see if all options was handled:
4
33
  case opt
5
34
  when '-v'; verbose = true # Return value 'true' is ok
@@ -11,7 +40,7 @@ TODO
11
40
  o Make an official dump method for debug
12
41
  o Make a note that all options are processed at once and not as-you-go
13
42
  o Test that arguments with spaces work
14
- o Long version usage strings
43
+ o Long version usage strings (major release)
15
44
  o Doc: Example of processing of sub-commands and sub-sub-commands
16
45
 
17
46
  + More tests
@@ -2,227 +2,148 @@ 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
+ # +args+ is a ShellOpts::Argv object containing the the remaning command line
44
+ # arguments. Argv is derived from Array
45
+ #
46
+ # ShellOpts can raise the exception CompilerError is there is an error in the
47
+ # USAGE string. If there is an error in the user supplied command line, #error
48
+ # is called instead and the program terminates with exit code 1. ShellOpts
49
+ # raises ConversionError is there is a name collision when converting to the
50
+ # hash or struct representations. Note that CompilerError and ConversionError
51
+ # are caused by misuse of the library and the problem should be corrected by
52
+ # the developer
53
+ #
54
+ # ShellOpts injects the constant PROGRAM into the global scope. It contains the
12
55
  # name of the program
13
56
  #
14
57
  module ShellOpts
15
- # Return the hidden +ShellOpts::ShellOpts+ object (see .process)
16
- def self.shellopts()
17
- @shellopts
18
- end
58
+ # Base class for ShellOpts exceptions
59
+ class Error < RuntimeError; end
19
60
 
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
61
+ # Raised when a syntax error is detected in the usage string
62
+ class CompilerError < Error
63
+ def initialize(start, message)
64
+ super(message)
65
+ set_backtrace(caller(start))
105
66
  end
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
69
+ # Raised when an error is detected during conversion from the Idr to array,
70
+ # hash, or struct
71
+ class ConversionError < Error; end
114
72
 
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_string, *msgs)
126
- end
73
+ # Raised when an internal error is detected
74
+ class InternalError < Error; end
127
75
 
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, nil, *msgs)
134
- end
76
+ # The current compilation object. It is set by #process
77
+ def self.shellopts() @shellopts end
135
78
 
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
79
+ # Process command line and set and return the shellopts compile object
80
+ def self.process(usage, argv, name: self.name, message: nil)
81
+ @shellopts.nil? or reset
82
+ messenger = message && Messenger.new(name, message, format: :custom)
83
+ @shellopts = ShellOpts.new(usage, argv, name: name, messenger: messenger)
84
+ end
174
85
 
175
- # Unroll the AST into a nested array
176
- def to_a
177
- @ast.values
178
- end
86
+ # Return the internal data representation of the command line (Idr::Program).
87
+ # Note that #as_program that the remaning arguments are accessible through
88
+ # the returned object
89
+ def self.as_program(usage, argv, name: self.name, message: nil)
90
+ process(usage, argv, name: name, message: message)
91
+ [shellopts.idr, shellopts.args]
92
+ end
179
93
 
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
94
+ # Process command line, set current shellopts object, and return a [array, argv]
95
+ # tuple. Returns the representation of the current object if not given any
96
+ # arguments
97
+ def self.as_array(usage, argv, name: self.name, message: nil)
98
+ process(usage, argv, name: name, message: message)
99
+ [shellopts.to_a, shellopts.args]
100
+ end
189
101
 
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, usage, msgs)
195
- end
102
+ # Process command line, set current shellopts object, and return a [hash, argv]
103
+ # tuple. Returns the representation of the current object if not given any
104
+ # arguments
105
+ def self.as_hash(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
106
+ process(usage, argv, name: name, message: message)
107
+ [shellopts.to_hash(use: use, aliases: aliases), shellopts.args]
108
+ end
196
109
 
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, nil, msgs)
201
- end
110
+ # Process command line, set current shellopts object, and return a [struct, argv]
111
+ # tuple. Returns the representation of the current object if not given any
112
+ # arguments
113
+ def self.as_struct(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
114
+ process(usage, argv, name: name, message: message)
115
+ [shellopts.to_struct(use: use, aliases: aliases), shellopts.args]
202
116
  end
203
117
 
204
- # Base class for ShellOpts exceptions
205
- class Error < RuntimeError; end
118
+ # Process command line, set current shellopts object, and then iterate
119
+ # options and commands as an array. Returns an enumerator to the array
120
+ # representation of the current shellopts object if not given a block
121
+ # argument
122
+ def self.each(usage = nil, argv = nil, name: self.name, message: nil, &block)
123
+ process(usage, argv, name: name, message: message)
124
+ shellopts.each(&block)
125
+ end
206
126
 
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
127
+ # Print error message and usage string and exit with status 1. This method
128
+ # should be called in response to user-errors (eg. specifying an illegal
129
+ # option)
130
+ def self.error(*msgs)
131
+ raise "Oops" if shellopts.nil?
132
+ shellopts.error(*msgs)
213
133
  end
214
134
 
215
- # Raised when an internal error is detected
216
- class InternalError < Error; end
135
+ # Print error message and exit with status 1. This method should not be
136
+ # called in response to system errors (eg. disk full)
137
+ def self.fail(*msgs)
138
+ raise "Oops" if shellopts.nil?
139
+ shellopts.fail(*msgs)
140
+ end
217
141
 
218
142
  private
219
- @shellopts = nil
220
-
221
- def self.emit_and_exit(program, usage, *msgs)
222
- $stderr.puts "#{program}: #{msgs.join}"
223
- $stderr.puts "Usage: #{program} #{usage}" if usage
224
- exit 1
143
+ # Reset state variables
144
+ def self.reset()
145
+ @shellopts = nil
225
146
  end
226
- end
227
147
 
228
- PROGRAM = File.basename($PROGRAM_NAME)
148
+ @shellopts = nil
149
+ end