shellopts 0.9.7 → 2.0.0.pre.4

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: 4a081efe12e3a4e2f4b38eab4d2f6776c5dc3c748fb06178a4b42dbbba4e9612
4
- data.tar.gz: 27a8d76c4de24c440bd330aa84d2e17014456c927af3c4a12807573df798c01a
3
+ metadata.gz: 188e8a420eb3e7ead116f7daffaff32761f12be4efa9d8aa583cfb8050ee4a8b
4
+ data.tar.gz: 474ae6c949005b53201a7d62a03b53d6f57b263746c2bfeac681a93fb05d880e
5
5
  SHA512:
6
- metadata.gz: 4f21b1f020dd04219272d5772cf0483b88c108af9991450e2532fcdab5322635a80a1cbac512c284358d10f271c03b4adef65ffb379d3df8a1d6635b9ca9f518
7
- data.tar.gz: e88c6c0abcb49a7ba6568aa66c62267a63b92cfb9969387522a52b045fc34d4121cddb4b8c11ffe68d8ed3f13f925391e393e5b4ac292732696de019f7ed4217
6
+ metadata.gz: 2734bcdec0f1d3b68a26675eee2dd172b776be45b333917e74f7935912410bbc620ab152610820392b73ffe061e8e38d8c6cb8698faf94387655337de4d38d26
7
+ data.tar.gz: 7dd0c8dea7d12c259212b72551a769a11cda450df81b17f370cbc242ef5fbee28f2b0ecc739e9b2d4307ee65505e3bc939ebe433797d52b708b30f20a287b454
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
@@ -320,12 +319,12 @@ preserve_root = true
320
319
  verbose = false
321
320
 
322
321
  # Process command line
323
- args = ShellOpts.process(USAGE, ARGV) { |opt, val|
322
+ args = ShellOpts.process(USAGE, ARGV) { |opt, arg|
324
323
  case opt
325
324
  when '-f', '--force'; force = true
326
325
  when '-i'; prompt = true
327
326
  when '-I'; prompt_once = true
328
- when '--interactive'; interactive = true; interactive_when = val
327
+ when '--interactive'; interactive = true; interactive_when = arg
329
328
  when '-r', '-R', '--recursive'; recursive = true
330
329
  when '-d', '--dir'; remove_empty_dirs = true
331
330
  when '--one-file-system'; one_file_system = true
@@ -379,6 +378,16 @@ release a new version, update the version number in `version.rb`, and then run
379
378
  git commits and tags, and push the `.gem` file to
380
379
  [rubygems.org](https://rubygems.org).
381
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
+
382
391
  ## Contributing
383
392
 
384
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,231 +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.nil?, 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, false, 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, true, 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, false, 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, 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
143
+ # Reset state variables
144
+ def self.reset()
145
+ @shellopts = nil
229
146
  end
230
- end
231
147
 
232
- PROGRAM = File.basename($PROGRAM_NAME)
148
+ @shellopts = nil
149
+ end