shellopts 2.0.0.pre.2 → 2.0.0.pre.9

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: 79a36712cf9c41a500726cd042d9932f1c6ed7f7e7a334d5d245e0ad31185c75
4
- data.tar.gz: ddeec0f65a4d8e4250cbc9d1332626395c135d0c4e34d879fafcfb89b492699a
3
+ metadata.gz: abf197d1a00cb875a6ac80ba5dd672bfdf035778f2e281baa151b61b4ffb4644
4
+ data.tar.gz: 2e4f6a91002acbdd0ad2f528305e559606accb5f9b6f2b6610f88851a0cbd981
5
5
  SHA512:
6
- metadata.gz: 2890ba5f277385eb8cc30a9e168d618f3633278cc41066c302ed82d7be46eeefb5a30f41a2471947bc78a0e55e8219cc472887a91b9d753a6687dfda605231e6
7
- data.tar.gz: a02ecb6e6613163f2915125ea0cd71cab30c7fd3c11f840a99bad0cb27b6a14dad0b3c806d55f8ab752e4f7921731cab0753c562499b7c88eeb8e5e8400efaab
6
+ metadata.gz: 6297e1adbf0a1a6f9a6c3b1057ae2a9c904cc34f4dfad26ee9ae25f48400476ea128b3c834e23cbe0ed746a1c11a69001c74dcd422ec06eec04b3138b855cbf3
7
+ data.tar.gz: bc9b0549d18774cdfac16150693e30b707bae6eb718ee36f8964405a78794335aea39386cb67e5457b7652e276fd244e9f6ec88d73577756e87d7f9490fd80b4
@@ -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
+
@@ -1,36 +1,53 @@
1
1
 
2
2
  module ShellOpts
3
+ # Specialization of Array for arguments lists. Args extends Array with a
4
+ # #extract and an #expect method to extract elements from the array. The
5
+ # methods raise a ShellOpts::UserError exception in case of errors
3
6
  class Args < Array
4
7
  def initialize(shellopts, *args)
5
8
  @shellopts = shellopts
6
9
  super(*args)
7
10
  end
8
11
 
9
- # Remove and return +count+ elements from the beginning of the array.
10
- # Elements are removed from the end of the array if +count+ is less than 0.
11
- # Expects at least +count.abs+ elements in the array
12
- def extract(count, message = nil)
13
- self.size >= count.abs or inoa(message)
14
- start = count >= 0 ? 0 : size + count
15
- slice!(start, count.abs)
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)
23
+ if count_or_range.is_a?(Range)
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
+ else
30
+ count = count_or_range
31
+ self.size >= count.abs or inoa(message)
32
+ start = count >= 0 ? 0 : size + count
33
+ r = slice!(start, count.abs)
34
+ r.size == 0 ? nil : (r.size == 1 ? r.first : r)
35
+ end
16
36
  end
17
37
 
18
- # Shifts +count+ elements from the array. Expects exactly +count+ elements
19
- # in the array
20
- def expect(count, message = nil)
21
- self.size == count or inoa(message)
22
- self
23
- end
24
-
25
- # Eats rest of the elements. Expects at least +min+ elements
26
- def consume(count, message = nil)
27
- self.size >= count or inoa(message)
28
- self
38
+ # As #extract except it doesn't allow negative counts and that the array is
39
+ # expect to be emptied by the operation
40
+ #
41
+ # #expect raise a ShellOpts::UserError exception if the array is not emptied
42
+ # by the operation
43
+ def expect(count_or_range, message = nil)
44
+ count_or_range === self.size or inoa(message)
45
+ extract(count_or_range) # Can't fail
29
46
  end
30
47
 
31
48
  private
32
49
  def inoa(message = nil)
33
- @shellopts.messenger.error(message || "Illegal number of arguments")
50
+ raise ShellOpts::UserError, message || "Illegal number of arguments"
34
51
  end
35
52
  end
36
53
  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