shellopts 2.0.0.pre.3 → 2.0.0.pre.11

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: 34b7edfee1092f53f5c5c75d678c69b2fe753ecbcd6caa4876c923dd56dfbf68
4
- data.tar.gz: f7dfa30cd0ec280eb3f36e6a4779e01eeea1d14fd904fe2b42d1b7d7d9e40b63
3
+ metadata.gz: bf9123d8aaeb0abd253cbbb879763200762cbed326eff55afa1bddc5bc6825d8
4
+ data.tar.gz: 78de60189d46a9d9c36d36ac79f754b9cedc820495b69e77171e8839033fda1a
5
5
  SHA512:
6
- metadata.gz: e3d9b271783e3f9c5b42aa2511c36a78814f239e848829fd347ad74fc5289b769da7264defe8af985bce500485d7e9480a240b3f9e261d70943b1ebc69cd671d
7
- data.tar.gz: 3f91000787d5fa80570d378604a7cc28ef991585030bd1ce327b88e67dca3e4e647428d48863fc302165bcece5130cc2f140efb05fa478c2ff9443a7197a8b4e
6
+ metadata.gz: 6012864adf0539064c11b700263692cec91a39d8519ec0d11030d586b7ab039f393c5595f07930f0a3e2b27a4418d18e6ee01c01deae0b7f056c9f7162106ae8
7
+ data.tar.gz: a740ce82c8134cfba058df3c0d8abed3cada1abdef0f3b640f75a8af73efee800eb650d70ca41416af9730f55bf317cf3977683d80c69b28aea2578132def4be
@@ -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,64 @@ 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
+
58
89
  # Base class for ShellOpts exceptions
59
90
  class Error < RuntimeError; end
60
91
 
61
- # Raised when a syntax error is detected in the usage string
92
+ # Raised when a syntax error is detected in the spec string
62
93
  class CompilerError < Error
63
- def initialize(start, message)
64
- super(message)
94
+ def initialize(start, usage)
95
+ super(usage)
65
96
  set_backtrace(caller(start))
66
97
  end
67
98
  end
68
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
+
69
114
  # Raised when an error is detected during conversion from the Idr to array,
70
115
  # hash, or struct
71
116
  class ConversionError < Error; end
@@ -76,74 +121,123 @@ module ShellOpts
76
121
  # The current compilation object. It is set by #process
77
122
  def self.shellopts() @shellopts end
78
123
 
79
- # Process command line and set and return the shellopts compile object
80
- def self.process(usage, argv, name: self.name, message: nil)
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)
81
136
  @shellopts.nil? or reset
82
- messenger = message && Messenger.new(name, message, format: :custom)
83
- @shellopts = ShellOpts.new(usage, argv, name: name, messenger: messenger)
137
+ @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
138
+ @shellopts.process
84
139
  end
85
140
 
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)
141
+ # Process command line, set current shellopts object, and return a
142
+ # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
143
+ # if called from the main Ruby object (ie. your executable)
144
+ def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
145
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
146
+ process(spec, argv, name: name, usage: usage)
91
147
  [shellopts.idr, shellopts.args]
92
148
  end
93
149
 
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)
150
+ # Process command line, set current shellopts object, and return a [array,
151
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
152
+ # main Ruby object (ie. your executable)
153
+ def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
154
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
155
+ process(spec, argv, name: name, usage: usage)
99
156
  [shellopts.to_a, shellopts.args]
100
157
  end
101
158
 
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]
159
+ # Process command line, set current shellopts object, and return a [hash,
160
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
161
+ # main Ruby object (ie. your executable)
162
+ def self.as_hash(
163
+ spec, argv,
164
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
165
+ key_type: ::ShellOpts.default_key_type,
166
+ aliases: {})
167
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
168
+ process(spec, argv, name: name, usage: usage)
169
+ [shellopts.to_h(key_type: key_type, aliases: aliases), shellopts.args]
108
170
  end
109
171
 
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]
172
+ # Process command line, set current shellopts object, and return a [struct,
173
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
174
+ # main Ruby object (ie. your executable)
175
+ def self.as_struct(
176
+ spec, argv,
177
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
178
+ aliases: {})
179
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
180
+ process(spec, argv, name: name, usage: usage)
181
+ [shellopts.to_struct(aliases: aliases), shellopts.args]
116
182
  end
117
183
 
118
184
  # Process command line, set current shellopts object, and then iterate
119
185
  # options and commands as an array. Returns an enumerator to the array
120
186
  # 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)
187
+ # argument. Automatically includes the ShellOpts module if called from the
188
+ # main Ruby object (ie. your executable)
189
+ def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
190
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
191
+ process(spec, argv, name: name, usage: usage)
124
192
  shellopts.each(&block)
125
193
  end
126
194
 
127
- # Print error message and usage string and exit with status 1. This method
195
+ # Print error usage and spec string and exit with status 1. This method
128
196
  # should be called in response to user-errors (eg. specifying an illegal
129
197
  # option)
130
- def self.error(*msgs)
131
- raise "Oops" if shellopts.nil?
132
- shellopts.error(*msgs)
198
+ def self.error(*msgs, exit: true)
199
+ shellopts!.error(msgs, exit: exit)
133
200
  end
134
201
 
135
- # Print error message and exit with status 1. This method should not be
202
+ # Print error usage and exit with status 1. This method should not be
136
203
  # called in response to system errors (eg. disk full)
137
- def self.fail(*msgs)
138
- raise "Oops" if shellopts.nil?
139
- shellopts.fail(*msgs)
204
+ def self.fail(*msgs, exit: true)
205
+ shellopts!.fail(*msgs, exit: exit)
206
+ end
207
+
208
+ def self.included(base)
209
+ # base.equal?(Object) is only true when included in main (we hope)
210
+ if !@is_included_in_main && base.equal?(Object)
211
+ @is_included_in_main = true
212
+ at_exit do
213
+ case $!
214
+ when ShellOpts::UserError
215
+ ::ShellOpts.error($!.message, exit: false)
216
+ exit!(1)
217
+ when ShellOpts::SystemFail
218
+ ::ShellOpts.fail($!.message)
219
+ exit!(1)
220
+ end
221
+ end
222
+ end
223
+ super
140
224
  end
141
225
 
142
226
  private
227
+ # Default default key type
228
+ DEFAULT_KEY_TYPE = :name
229
+
143
230
  # Reset state variables
144
231
  def self.reset()
145
232
  @shellopts = nil
146
233
  end
147
234
 
235
+ # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
236
+ def self.shellopts!
237
+ ::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
238
+ end
239
+
148
240
  @shellopts = nil
241
+ @is_included_in_main = false
149
242
  end
243
+
@@ -2,48 +2,53 @@
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 negative, the elements will be removed from the end of
15
- # the array. If +count_or_range+ is a range, the number of elements
16
- # returned will be in that range. The range can't contain negative numbers.
17
- # #expect calls #error() if there's is not enough elements in the array to
18
- # satisfy the request
19
- def extract(count, message = nil)
20
- self.size >= count.abs or inoa(message)
21
- start = count >= 0 ? 0 : size + count
22
- r = slice!(start, count.abs)
23
- r.size == 0 ? nil : (r.size == 1 ? r.first : r)
24
- end
25
-
26
- # Remove and returns elements from the array. If +count_or_range+ is a
27
- # number, that number of elements will be returned. If the count is
28
- # negative, the elements will be removed from the end of the array. If
29
- # +count_or_range+ is a range, the number of elements returned will be in
30
- # that range. The range can't contain negative numbers. #expect calls
31
- # #error() if the array has remaning elemens after removal satisfy the
32
- # request
33
- def expect(count_or_range, message = nil)
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
22
+ def extract(count_or_range, message = nil)
34
23
  if count_or_range.is_a?(Range)
35
- count_or_range.cover?(self.size) or inoa(message)
36
- self.shift(self.size)
24
+ range = count_or_range
25
+ range.min <= self.size or inoa(message)
26
+ n_extract = [self.size, range.max].min
27
+ n_extend = range.max > self.size ? range.max - self.size : 0
28
+ r = self.shift(n_extract) + Array.new(n_extend)
29
+ range.max <= 1 ? r.first : r
37
30
  else
38
- count_or_range == self.size or inoa(message)
39
- r = self.shift(count)
40
- r.size == 0 ? nil : (r.size == 1 ? r.first : r)
31
+ count = count_or_range
32
+ self.size >= count.abs or inoa(message)
33
+ start = count >= 0 ? 0 : size + count
34
+ r = slice!(start, count.abs)
35
+ r.size <= 0 ? nil : (r.size == 1 ? r.first : r)
41
36
  end
42
37
  end
43
38
 
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
44
+ def expect(count_or_range, message = nil)
45
+ count_or_range === self.size or inoa(message)
46
+ extract(count_or_range) # Can't fail
47
+ end
48
+
44
49
  private
45
50
  def inoa(message = nil)
46
- @shellopts.messenger.error(message || "Illegal number of arguments")
51
+ raise ShellOpts::UserError, message || "Illegal number of arguments"
47
52
  end
48
53
  end
49
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