shellopts 2.0.0.pre.4 → 2.0.0.pre.7

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: c7ac01e4c6feb1897f74056a36c74db0cf78802d13437908aa18631ead7c97c5
4
+ data.tar.gz: 5443ff421ceb38fcf07aaf87ae7b3740fd804679ccdec377fb195c688c50b57b
5
5
  SHA512:
6
- metadata.gz: 2734bcdec0f1d3b68a26675eee2dd172b776be45b333917e74f7935912410bbc620ab152610820392b73ffe061e8e38d8c6cb8698faf94387655337de4d38d26
7
- data.tar.gz: 7dd0c8dea7d12c259212b72551a769a11cda450df81b17f370cbc242ef5fbee28f2b0ecc739e9b2d4307ee65505e3bc939ebe433797d52b708b30f20a287b454
6
+ metadata.gz: 0d8180a2acc6dac8e234567db9409cd9d01cebe8d4551f6cfa24488ce5d1fb848ec8b42b21d40e7d8f6990bf61e4dc8d482370e9cb4ceb409e3f18fdb70a877c
7
+ data.tar.gz: f07d22ab7976d5efa317356fc796f8bbe20b8c6a8ee609eb24106f60e8e56d9874181dcab4e873268b1aea5a1aa27ffd1253bb10499052edd995894c6eec9d88
@@ -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
@@ -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,122 @@ 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)
84
138
  end
85
139
 
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)
140
+ # Process command line, set current shellopts object, and return a
141
+ # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
142
+ # if called from the main Ruby object (ie. your executable)
143
+ def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
144
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
145
+ process(spec, argv, name: name, usage: usage)
91
146
  [shellopts.idr, shellopts.args]
92
147
  end
93
148
 
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)
149
+ # Process command line, set current shellopts object, and return a [array,
150
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
151
+ # main Ruby object (ie. your executable)
152
+ def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
153
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
154
+ process(spec, argv, name: name, usage: usage)
99
155
  [shellopts.to_a, shellopts.args]
100
156
  end
101
157
 
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]
158
+ # Process command line, set current shellopts object, and return a [hash,
159
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
160
+ # main Ruby object (ie. your executable)
161
+ def self.as_hash(
162
+ spec, argv,
163
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
164
+ key_type: ::ShellOpts.default_key_type,
165
+ aliases: {})
166
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
167
+ process(spec, argv, name: name, usage: usage)
168
+ [shellopts.to_h(key_type: key_type, aliases: aliases), shellopts.args]
108
169
  end
109
170
 
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]
171
+ # Process command line, set current shellopts object, and return a [struct,
172
+ # argv] tuple. Automatically includes the ShellOpts module if called from the
173
+ # main Ruby object (ie. your executable)
174
+ def self.as_struct(
175
+ spec, argv,
176
+ name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
177
+ aliases: {})
178
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
179
+ process(spec, argv, name: name, usage: usage)
180
+ [shellopts.to_struct(aliases: aliases), shellopts.args]
116
181
  end
117
182
 
118
183
  # Process command line, set current shellopts object, and then iterate
119
184
  # options and commands as an array. Returns an enumerator to the array
120
185
  # 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)
186
+ # argument. Automatically includes the ShellOpts module if called from the
187
+ # main Ruby object (ie. your executable)
188
+ def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
189
+ Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
190
+ process(spec, argv, name: name, usage: usage)
124
191
  shellopts.each(&block)
125
192
  end
126
193
 
127
- # Print error message and usage string and exit with status 1. This method
194
+ # Print error usage and spec string and exit with status 1. This method
128
195
  # should be called in response to user-errors (eg. specifying an illegal
129
196
  # option)
130
- def self.error(*msgs)
131
- raise "Oops" if shellopts.nil?
132
- shellopts.error(*msgs)
197
+ def self.error(*msgs, exit: true)
198
+ shellopts!.error(msgs, exit: exit)
133
199
  end
134
200
 
135
- # Print error message and exit with status 1. This method should not be
201
+ # Print error usage and exit with status 1. This method should not be
136
202
  # called in response to system errors (eg. disk full)
137
- def self.fail(*msgs)
138
- raise "Oops" if shellopts.nil?
139
- shellopts.fail(*msgs)
203
+ def self.fail(*msgs, exit: true)
204
+ shellopts!.fail(*msgs, exit: exit)
205
+ end
206
+
207
+ def self.included(base)
208
+ # base.equal?(Object) is only true when included in main (we hope)
209
+ if !@is_included_in_main && base.equal?(Object)
210
+ @is_included_in_main = true
211
+ at_exit do
212
+ case $!
213
+ when ShellOpts::UserError
214
+ ::ShellOpts.error($!.message, exit: false)
215
+ exit!(1)
216
+ when ShellOpts::SystemFail
217
+ ::ShellOpts.fail($!.message)
218
+ exit!(1)
219
+ end
220
+ end
221
+ end
222
+ super
140
223
  end
141
224
 
142
225
  private
226
+ # Default default key type
227
+ DEFAULT_KEY_TYPE = :name
228
+
143
229
  # Reset state variables
144
230
  def self.reset()
145
231
  @shellopts = nil
146
232
  end
147
233
 
234
+ # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
235
+ def self.shellopts!
236
+ ::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
237
+ end
238
+
148
239
  @shellopts = nil
240
+ @is_included_in_main = false
149
241
  end
242
+
@@ -33,28 +33,16 @@ module ShellOpts
33
33
  end
34
34
  end
35
35
 
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
36
+ # As extract except it doesn't allow negative counts and that the array is
37
+ # expect to be emptied by the operation
42
38
  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
39
+ count_or_range === self.size or inoa(message)
40
+ extract(count_or_range) # Can't fail
53
41
  end
54
42
 
55
43
  private
56
44
  def inoa(message = nil)
57
- @shellopts.messenger.error(message || "Illegal number of arguments")
45
+ raise ShellOpts::UserError, message || "Illegal number of arguments"
58
46
  end
59
47
  end
60
48
  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
@@ -3,9 +3,9 @@ require 'shellopts/idr.rb'
3
3
 
4
4
  module ShellOpts
5
5
  module Idr
6
- # Generates an Idr::Program from an Ast::Program object
7
- def self.generate(ast, messenger)
8
- Idr::Program.new(ast, messenger)
6
+ # Generates an Idr::Program from a ShellOpts object
7
+ def self.generate(shellopts)
8
+ Idr::Program.new(shellopts)
9
9
  end
10
10
  end
11
11
  end