shellopts 2.0.0.pre.4 → 2.0.0.pre.13

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: 188e8a420eb3e7ead116f7daffaff32761f12be4efa9d8aa583cfb8050ee4a8b
4
- data.tar.gz: 474ae6c949005b53201a7d62a03b53d6f57b263746c2bfeac681a93fb05d880e
3
+ metadata.gz: 8ac8ce6815e283630cb66195b0069a0d1772d8234dd580833e49b38cb0717fe8
4
+ data.tar.gz: e6fc6e96d47db9078edad643ce9ee4fabdeaf3450a5ab61fd330b701bfadf244
5
5
  SHA512:
6
- metadata.gz: 2734bcdec0f1d3b68a26675eee2dd172b776be45b333917e74f7935912410bbc620ab152610820392b73ffe061e8e38d8c6cb8698faf94387655337de4d38d26
7
- data.tar.gz: 7dd0c8dea7d12c259212b72551a769a11cda450df81b17f370cbc242ef5fbee28f2b0ecc739e9b2d4307ee65505e3bc939ebe433797d52b708b30f20a287b454
6
+ metadata.gz: f77988efda7870d95b7693125e0e4b9753dbbe637cfc126d92559654bd171f17f37698ac7588ce9d3fc0521ba00156b862e763f1ae95190d77d89cc5e500bf5a
7
+ data.tar.gz: 98d96929cd577649796e5f699e3c748f7ef3c72cb8ad3dd6036d2e656bae15229a414263a3701a4a027daacb869de2ceb52b1d3095fd1715c064d6befbaedbd0
@@ -1 +1 @@
1
- ruby-2.5.1
1
+ ruby-2.6.6
data/TODO CHANGED
@@ -1,6 +1,8 @@
1
1
 
2
2
  TODO
3
- o Remove ! from OptionStruct#subcommand return value. We know we're
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
4
6
  processing commands so there is no need to have a distinct name and it
5
7
  feels a lot more intuitive without it
6
8
  o Add validation block to ShellOpts class methods
@@ -43,6 +45,7 @@ TODO
43
45
  o Long version usage strings (major release)
44
46
  o Doc: Example of processing of sub-commands and sub-sub-commands
45
47
 
48
+ + Add a 'mandatory' argument to #subcommand
46
49
  + More tests
47
50
  + More doc
48
51
  + Implement value-name-before-flags rule
@@ -0,0 +1 @@
1
+ hej
@@ -4,11 +4,10 @@ require 'shellopts/compiler.rb'
4
4
  require 'shellopts/parser.rb'
5
5
  require 'shellopts/generator.rb'
6
6
  require 'shellopts/option_struct.rb'
7
- require 'shellopts/messenger.rb'
8
- require 'shellopts/utils.rb'
7
+ require 'shellopts/main.rb'
9
8
 
10
9
  # Name of program. Defined as the basename of the program file
11
- PROGRAM = File.basename($PROGRAM_NAME)
10
+ #PROGRAM = File.basename($PROGRAM_NAME)
12
11
 
13
12
  # ShellOpts main Module
14
13
  #
@@ -21,7 +20,7 @@ PROGRAM = File.basename($PROGRAM_NAME)
21
20
  #
22
21
  # For example; the following process and convert a command line into a struct
23
22
  # representation and also sets ShellOpts.shellopts object so that the #error
24
- # method can print a relevant usage string:
23
+ # method can print a relevant spec string:
25
24
  #
26
25
  # USAGE = "a,all f,file=FILE -- ARG1 ARG2"
27
26
  # opts, args = ShellOpts.as_struct(USAGE, ARGV)
@@ -54,18 +53,68 @@ PROGRAM = File.basename($PROGRAM_NAME)
54
53
  # ShellOpts injects the constant PROGRAM into the global scope. It contains the
55
54
  # name of the program
56
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
+ #
57
64
  module ShellOpts
65
+ def self.default_name()
66
+ @default_name || defined?(PROGRAM) ? PROGRAM : File.basename($0)
67
+ end
68
+
69
+ def self.default_name=(name)
70
+ @default_name = name
71
+ end
72
+
73
+ def self.default_usage()
74
+ @default_usage || defined?(USAGE) ? USAGE : nil
75
+ end
76
+
77
+ def self.default_usage=(usage)
78
+ @default_usage = usage
79
+ end
80
+
81
+ def self.default_key_type()
82
+ @default_key_type || ::ShellOpts::DEFAULT_KEY_TYPE
83
+ end
84
+
85
+ def self.default_key_type=(type)
86
+ @default_key_type = type
87
+ end
88
+
89
+ # Result of the last as_* command
90
+ def self.opts() @opts end
91
+ def self.args() @args end
92
+
58
93
  # Base class for ShellOpts exceptions
59
94
  class Error < RuntimeError; end
60
95
 
61
- # Raised when a syntax error is detected in the usage string
96
+ # Raised when a syntax error is detected in the spec string
62
97
  class CompilerError < Error
63
- def initialize(start, message)
64
- super(message)
98
+ def initialize(start, usage)
99
+ super(usage)
65
100
  set_backtrace(caller(start))
66
101
  end
67
102
  end
68
103
 
104
+ # Raised when an error is detected in the command line
105
+ class ParserError < Error; end
106
+
107
+ # Raised when the command line error is caused by the user. It is raised by
108
+ # the parser but can also be used by the application if the command line
109
+ # fails a semantic check
110
+ class UserError < ParserError; end
111
+
112
+ # Raised when the error is caused by a failed assumption about the system. It
113
+ # is not raised by the ShellOpts library as it only concerns itself with
114
+ # command line syntax but can be used by the application to report a failure
115
+ # through ShellOpts#fail method when the ShellOpts module is included
116
+ class SystemFail < Error; end
117
+
69
118
  # Raised when an error is detected during conversion from the Idr to array,
70
119
  # hash, or struct
71
120
  class ConversionError < Error; end
@@ -76,74 +125,133 @@ module ShellOpts
76
125
  # The current compilation object. It is set by #process
77
126
  def self.shellopts() @shellopts end
78
127
 
79
- # Process command line and set and return the shellopts compile object
80
- def self.process(usage, argv, name: self.name, message: nil)
128
+ # Name of program
129
+ def program_name() shellopts!.name end
130
+ def program_name=(name) shellopts!.name = name end
131
+
132
+ # Usage string
133
+ def usage() shellopts!.spec end
134
+ def usage=(spec) shellopts!.spec = spec end
135
+
136
+ # Process command line, set current shellopts object, and return it.
137
+ # Remaining arguments from the command line can be accessed through
138
+ # +shellopts.args+
139
+ def self.process(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
81
140
  @shellopts.nil? or reset
82
- messenger = message && Messenger.new(name, message, format: :custom)
83
- @shellopts = ShellOpts.new(usage, argv, name: name, messenger: messenger)
141
+ @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
142
+ @shellopts.process
84
143
  end
85
144
 
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]
145
+ # Process command line, set current shellopts object, and return a
146
+ # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
147
+ # if called from the main Ruby object (ie. your executable)
148
+ def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
149
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
150
+ process(spec, argv, name: name, usage: usage)
151
+ @opts = shellopts.idr
152
+ @args = shellopts.args
153
+ [@opts, @args]
92
154
  end
93
155
 
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]
156
+ # Process command line, set current shellopts object, and return a [array,
157
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
158
+ # main Ruby object (ie. your executable)
159
+ def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
160
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
161
+ process(spec, argv, name: name, usage: usage)
162
+ @opts = shellopts.to_a
163
+ @args = shellopts.args
164
+ [@opts, @args]
100
165
  end
101
166
 
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]
167
+ # Process command line, set current shellopts object, and return a [hash,
168
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
169
+ # main Ruby object (ie. your executable)
170
+ def self.as_hash(
171
+ spec, argv,
172
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
173
+ key_type: ::ShellOpts.default_key_type,
174
+ aliases: {})
175
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
176
+ process(spec, argv, name: name, usage: usage)
177
+ @opts = shellopts.to_h(key_type: key_type, aliases: aliases)
178
+ @args = shellopts.args
179
+ [@opts, @args]
108
180
  end
109
181
 
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]
182
+ # Process command line, set current shellopts object, and return a [struct,
183
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
184
+ # main Ruby object (ie. your executable)
185
+ def self.as_struct(
186
+ spec, argv,
187
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
188
+ aliases: {})
189
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
190
+ process(spec, argv, name: name, usage: usage)
191
+ @opts = shellopts.to_struct(aliases: aliases)
192
+ @args = shellopts.args
193
+ [@opts, @args]
116
194
  end
117
195
 
118
196
  # Process command line, set current shellopts object, and then iterate
119
197
  # options and commands as an array. Returns an enumerator to the array
120
198
  # 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)
199
+ # argument. Automatically includes the ShellOpts module if called from the
200
+ # main Ruby object (ie. your executable)
201
+ def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
202
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
203
+ process(spec, argv, name: name, usage: usage)
204
+ @opts = shellopts.to_a
205
+ @args = shellopts.args
124
206
  shellopts.each(&block)
125
207
  end
126
208
 
127
- # Print error message and usage string and exit with status 1. This method
209
+ # Print error usage and spec string and exit with status 1. This method
128
210
  # should be called in response to user-errors (eg. specifying an illegal
129
211
  # option)
130
- def self.error(*msgs)
131
- raise "Oops" if shellopts.nil?
132
- shellopts.error(*msgs)
212
+ def self.error(*msgs, exit: true)
213
+ shellopts!.error(msgs, exit: exit)
133
214
  end
134
215
 
135
- # Print error message and exit with status 1. This method should not be
216
+ # Print error usage and exit with status 1. This method should not be
136
217
  # called in response to system errors (eg. disk full)
137
- def self.fail(*msgs)
138
- raise "Oops" if shellopts.nil?
139
- shellopts.fail(*msgs)
218
+ def self.fail(*msgs, exit: true)
219
+ shellopts!.fail(*msgs, exit: exit)
220
+ end
221
+
222
+ def self.included(base)
223
+ # base.equal?(Object) is only true when included in main (we hope)
224
+ if !@is_included_in_main && base.equal?(Object)
225
+ @is_included_in_main = true
226
+ at_exit do
227
+ case $!
228
+ when ShellOpts::UserError
229
+ ::ShellOpts.error($!.message, exit: false)
230
+ exit!(1)
231
+ when ShellOpts::SystemFail
232
+ ::ShellOpts.fail($!.message)
233
+ exit!(1)
234
+ end
235
+ end
236
+ end
237
+ super
140
238
  end
141
239
 
142
240
  private
241
+ # Default default key type
242
+ DEFAULT_KEY_TYPE = :name
243
+
143
244
  # Reset state variables
144
245
  def self.reset()
145
246
  @shellopts = nil
146
247
  end
147
248
 
249
+ # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
250
+ def self.shellopts!
251
+ ::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
252
+ end
253
+
148
254
  @shellopts = nil
255
+ @is_included_in_main = false
149
256
  end
257
+
@@ -2,21 +2,23 @@
2
2
  module ShellOpts
3
3
  # Specialization of Array for arguments lists. Args extends Array with a
4
4
  # #extract and an #expect method to extract elements from the array. The
5
- # methods call #error() in response to errors
5
+ # methods raise a ShellOpts::UserError exception in case of errors
6
6
  class Args < Array
7
7
  def initialize(shellopts, *args)
8
8
  @shellopts = shellopts
9
9
  super(*args)
10
10
  end
11
11
 
12
- # Remove and return elements from beginning of the array. If
13
- # +count_or_range+ is a number, that number of elements will be returned.
14
- # If the count is one, a simple value is returned instead of an array. If
15
- # the count is negative, the elements will be removed from the end of the
16
- # array. If +count_or_range+ is a range, the number of elements returned
17
- # will be in that range. The range can't contain negative numbers #expect
18
- # calls #error() if there's is not enough elements in the array to satisfy
19
- # the request
12
+ # Remove and return elements from beginning of the array
13
+ #
14
+ # If +count_or_range+ is a number, that number of elements will be
15
+ # returned. If the count is one, a simple value is returned instead of an
16
+ # array. If the count is negative, the elements will be removed from the
17
+ # end of the array. If +count_or_range+ is a range, the number of elements
18
+ # returned will be in that range. The range can't contain negative numbers
19
+ #
20
+ # #extract raise a ShellOpts::UserError exception if there's is not enough
21
+ # elements in the array to satisfy the request
20
22
  def extract(count_or_range, message = nil)
21
23
  if count_or_range.is_a?(Range)
22
24
  range = count_or_range
@@ -24,37 +26,29 @@ module ShellOpts
24
26
  n_extract = [self.size, range.max].min
25
27
  n_extend = range.max > self.size ? range.max - self.size : 0
26
28
  r = self.shift(n_extract) + Array.new(n_extend)
29
+ range.max <= 1 ? r.first : r
27
30
  else
28
31
  count = count_or_range
29
32
  self.size >= count.abs or inoa(message)
30
33
  start = count >= 0 ? 0 : size + count
31
34
  r = slice!(start, count.abs)
32
- r.size == 0 ? nil : (r.size == 1 ? r.first : r)
35
+ r.size <= 0 ? nil : (r.size == 1 ? r.first : r)
33
36
  end
34
37
  end
35
38
 
36
- # Remove and returns elements from the array. If +count_or_range+ is a
37
- # number, that number of elements will be returned. If the count is one, a
38
- # simple value is returned instead of an array. If +count_or_range+ is a
39
- # range, the number of elements returned will be in that range. The range
40
- # can't contain negative numbers. #expect calls #error() if the array has
41
- # remaning elemens after removal satisfy the request
39
+ # As #extract except it doesn't allow negative counts and that the array is
40
+ # expect to be emptied by the operation
41
+ #
42
+ # #expect raise a ShellOpts::UserError exception if the array is not emptied
43
+ # by the operation
42
44
  def expect(count_or_range, message = nil)
43
- if count_or_range.is_a?(Range)
44
- range = count_or_range
45
- range.cover?(self.size) or inoa(message)
46
- self.shift(self.size)
47
- else
48
- count = count_or_range
49
- count == self.size or inoa(message)
50
- r = self.shift(count)
51
- r.size == 0 ? nil : (r.size == 1 ? r.first : r)
52
- end
45
+ count_or_range === self.size or inoa(message)
46
+ extract(count_or_range) # Can't fail
53
47
  end
54
48
 
55
49
  private
56
50
  def inoa(message = nil)
57
- @shellopts.messenger.error(message || "Illegal number of arguments")
51
+ raise ShellOpts::UserError, message || "Illegal number of arguments"
58
52
  end
59
53
  end
60
54
  end
@@ -7,17 +7,17 @@ module ShellOpts
7
7
 
8
8
  # Optional sub-command (Ast::Command). Initially nil but assigned by the
9
9
  # parser
10
- attr_accessor :command
10
+ attr_accessor :subcommand
11
11
 
12
12
  def initialize(grammar, name)
13
13
  super(grammar, name)
14
14
  @options = []
15
- @command = nil
15
+ @subcommand = nil
16
16
  end
17
17
 
18
18
  # Array of option or command tuples
19
19
  def values
20
- (options + (Array(command || []))).map { |node| node.to_tuple }
20
+ (options + (Array(subcommand || []))).map { |node| node.to_tuple }
21
21
  end
22
22
 
23
23
  # :nocov:
@@ -26,10 +26,10 @@ module ShellOpts
26
26
  yield if block_given?
27
27
  puts "options:"
28
28
  indent { options.each { |opt| opt.dump } }
29
- print "command:"
30
- if command
29
+ print "subcommand:"
30
+ if subcommand
31
31
  puts
32
- indent { command.dump }
32
+ indent { subcommand.dump }
33
33
  else
34
34
  puts "nil"
35
35
  end
@@ -29,11 +29,11 @@ module ShellOpts
29
29
  def initialize(name, source)
30
30
  @name, @tokens = name, source.split(/\s+/).reject(&:empty?)
31
31
 
32
- # @commands_by_path is an hash from command-path to Command or Program
32
+ # @subcommands_by_path is an hash from subcommand-path to Command or Program
33
33
  # object. The top level Program object has nil as its path.
34
- # @commands_by_path is used to check for uniqueness of commands and to
35
- # link sub-commands to their parents
36
- @commands_by_path = {}
34
+ # @subcommands_by_path is used to check for uniqueness of subcommands and to
35
+ # link sub-subcommands to their parents
36
+ @subcommands_by_path = {}
37
37
  end
38
38
 
39
39
  def call
@@ -49,30 +49,26 @@ module ShellOpts
49
49
  # Returns the current token and advance to the next token
50
50
  def next_token() @tokens.shift end
51
51
 
52
- def error(msg) # Just a shorthand. Unrelated to ShellOpts.error
53
- raise Compiler::Error.new(msg)
54
- end
55
-
56
52
  def compile_program
57
- program = @commands_by_path[nil] = Grammar::Program.new(@name, compile_options)
53
+ program = @subcommands_by_path[nil] = Grammar::Program.new(@name, compile_options)
58
54
  while curr_token && curr_token != "--"
59
- compile_command
55
+ compile_subcommand
60
56
  end
61
57
  program.args.concat(@tokens[1..-1]) if curr_token
62
58
  program
63
59
  end
64
60
 
65
- def compile_command
61
+ def compile_subcommand
66
62
  path = curr_token[0..-2]
67
63
  ident_list = compile_ident_list(path, ".")
68
64
  parent_path = ident_list.size > 1 ? ident_list[0..-2].join(".") : nil
69
65
  name = ident_list[-1]
70
66
 
71
- parent = @commands_by_path[parent_path] or
72
- error "No such command: #{parent_path.inspect}"
73
- !@commands_by_path.key?(path) or error "Duplicate command: #{path.inspect}"
67
+ parent = @subcommands_by_path[parent_path] or
68
+ raise Compiler::Error, "No such subcommand: #{parent_path.inspect}"
69
+ !@subcommands_by_path.key?(path) or raise Compiler::Error, "Duplicate subcommand: #{path.inspect}"
74
70
  next_token
75
- @commands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
71
+ @subcommands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
76
72
  end
77
73
 
78
74
  def compile_options
@@ -81,7 +77,7 @@ module ShellOpts
81
77
  option_list << compile_option
82
78
  end
83
79
  dup = option_list.map(&:names).flatten.find_dup and
84
- error "Duplicate option name: #{dup.inspect}"
80
+ raise Compiler::Error, "Duplicate option name: #{dup.inspect}"
85
81
  option_list
86
82
  end
87
83
 
@@ -102,7 +98,7 @@ module ShellOpts
102
98
  long_names = []
103
99
  ident_list = compile_ident_list(names, ",")
104
100
  (dup = ident_list.find_dup).nil? or
105
- error "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
101
+ raise Compiler::Error, "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
106
102
  ident_list.each { |ident|
107
103
  if ident.size == 1
108
104
  short_names << "-#{ident}"
@@ -115,13 +111,15 @@ module ShellOpts
115
111
  Grammar::Option.new(short_names, long_names, flags, label)
116
112
  end
117
113
 
118
- # Compile list of option names or a command path
114
+ # Compile list of option names or a subcommand path
119
115
  def compile_ident_list(ident_list_str, sep)
120
116
  ident_list_str.split(sep, -1).map { |str|
121
- !str.empty? or error "Empty identifier in #{curr_token.inspect}"
122
- !str.start_with?("-") or error "Identifier can't start with '-' in #{curr_token.inspect}"
117
+ !str.empty? or
118
+ raise Compiler::Error, "Empty identifier in #{curr_token.inspect}"
119
+ !str.start_with?("-") or
120
+ raise Compiler::Error, "Identifier can't start with '-' in #{curr_token.inspect}"
123
121
  str !~ /([^\w\d#{sep}-])/ or
124
- error "Illegal character #{$1.inspect} in #{curr_token.inspect}"
122
+ raise Compiler::Error, "Illegal character #{$1.inspect} in #{curr_token.inspect}"
125
123
  str
126
124
  }
127
125
  end