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

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