shellopts 2.0.0.pre.1 → 2.0.0.pre.8

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: e60db3cf5de50dcd106cb0fca007d064e18354278a65207bb167bf8aa63d3433
4
- data.tar.gz: 85b0108262357d6e5654e725ab12293f51fd2579beb13fa0d910be2200c32310
3
+ metadata.gz: b967bc79076cae64e2c68c3ef559568a4324d94f7d87800937b746e09755fcfe
4
+ data.tar.gz: 14c6a5866c6b33da7f7a27f050de46d03b7936842c366fb7b32121324f17d7b0
5
5
  SHA512:
6
- metadata.gz: 375fe97651622560d786b288217e1a8581d12bbc497dee55b36848597d9410d393a127e86088cfe4ac8f949270d9f6797d6f135f65492f15bb9d4a84bff1ffbc
7
- data.tar.gz: c69a813e4672d87c6a4da69e0e8086d8acfe54c8506cda64ce6dc36dbc12669cc79c29ffd480c6871937d87491139f4e7448fd35a6d6a7d333451addd6c634a6
6
+ metadata.gz: de339ab12a1ef41f75cbb386a6778adc0dca989f64e9180780c1e00276303824dacca681ccba287f746e0bf7b436b717384be48feb02b4b9faf4be3852c533a5
7
+ data.tar.gz: a4b286f8297d56afc672a8e9f21119580496c820a8377a419a1aca1905a08e67a5763ed1aa14678137a9c739f120d673a1227d7a18233b43a482e8c43c603d92
@@ -1 +1 @@
1
- ruby-2.5.1
1
+ ruby-2.6.6
data/TODO CHANGED
@@ -1,5 +1,10 @@
1
1
 
2
2
  TODO
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
6
+ processing commands so there is no need to have a distinct name and it
7
+ feels a lot more intuitive without it
3
8
  o Add validation block to ShellOpts class methods
4
9
  o Get rid of key_name. Define #name on Grammar::Node instead
5
10
  o Define #name to the string name of the option/command without prefixed '--'
@@ -40,6 +45,7 @@ TODO
40
45
  o Long version usage strings (major release)
41
46
  o Doc: Example of processing of sub-commands and sub-sub-commands
42
47
 
48
+ + Add a 'mandatory' argument to #subcommand
43
49
  + More tests
44
50
  + More doc
45
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)
@@ -40,6 +39,9 @@ PROGRAM = File.basename($PROGRAM_NAME)
40
39
  # hash, args = ShellOpts.as_hash(USAGE, ARGV)
41
40
  # struct, args = ShellOpts.as_struct(USAGE, ARGV)
42
41
  #
42
+ # +args+ is a ShellOpts::Argv object containing the the remaning command line
43
+ # arguments. Argv is derived from Array
44
+ #
43
45
  # ShellOpts can raise the exception CompilerError is there is an error in the
44
46
  # USAGE string. If there is an error in the user supplied command line, #error
45
47
  # is called instead and the program terminates with exit code 1. ShellOpts
@@ -51,18 +53,64 @@ PROGRAM = File.basename($PROGRAM_NAME)
51
53
  # ShellOpts injects the constant PROGRAM into the global scope. It contains the
52
54
  # name of the program
53
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
+ #
54
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
+
55
89
  # Base class for ShellOpts exceptions
56
90
  class Error < RuntimeError; end
57
91
 
58
- # Raised when a syntax error is detected in the usage string
92
+ # Raised when a syntax error is detected in the spec string
59
93
  class CompilerError < Error
60
- def initialize(start, message)
61
- super(message)
94
+ def initialize(start, usage)
95
+ super(usage)
62
96
  set_backtrace(caller(start))
63
97
  end
64
98
  end
65
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
+
66
114
  # Raised when an error is detected during conversion from the Idr to array,
67
115
  # hash, or struct
68
116
  class ConversionError < Error; end
@@ -73,74 +121,122 @@ module ShellOpts
73
121
  # The current compilation object. It is set by #process
74
122
  def self.shellopts() @shellopts end
75
123
 
76
- # Process command line and set and return the shellopts compile object
77
- 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)
78
136
  @shellopts.nil? or reset
79
- messenger = message && Messenger.new(name, message, format: :custom)
80
- @shellopts = ShellOpts.new(usage, argv, name: name, messenger: messenger)
137
+ @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
81
138
  end
82
139
 
83
- # Return the internal data representation of the command line (Idr::Program).
84
- # Note that #as_program that the remaning arguments are accessible through
85
- # the returned object
86
- def self.as_program(usage, argv, name: self.name, message: nil)
87
- 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)
88
146
  [shellopts.idr, shellopts.args]
89
147
  end
90
148
 
91
- # Process command line, set current shellopts object, and return a [array, argv]
92
- # tuple. Returns the representation of the current object if not given any
93
- # arguments
94
- def self.as_array(usage, argv, name: self.name, message: nil)
95
- 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)
96
155
  [shellopts.to_a, shellopts.args]
97
156
  end
98
157
 
99
- # Process command line, set current shellopts object, and return a [hash, argv]
100
- # tuple. Returns the representation of the current object if not given any
101
- # arguments
102
- def self.as_hash(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
103
- process(usage, argv, name: name, message: message)
104
- [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]
105
169
  end
106
170
 
107
- # Process command line, set current shellopts object, and return a [struct, argv]
108
- # tuple. Returns the representation of the current object if not given any
109
- # arguments
110
- def self.as_struct(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
111
- process(usage, argv, name: name, message: message)
112
- [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]
113
181
  end
114
182
 
115
183
  # Process command line, set current shellopts object, and then iterate
116
184
  # options and commands as an array. Returns an enumerator to the array
117
185
  # representation of the current shellopts object if not given a block
118
- # argument
119
- def self.each(usage = nil, argv = nil, name: self.name, message: nil, &block)
120
- 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)
121
191
  shellopts.each(&block)
122
192
  end
123
193
 
124
- # 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
125
195
  # should be called in response to user-errors (eg. specifying an illegal
126
196
  # option)
127
- def self.error(*msgs)
128
- raise "Oops" if shellopts.nil?
129
- shellopts.error(*msgs)
197
+ def self.error(*msgs, exit: true)
198
+ shellopts!.error(msgs, exit: exit)
130
199
  end
131
200
 
132
- # 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
133
202
  # called in response to system errors (eg. disk full)
134
- def self.fail(*msgs)
135
- raise "Oops" if shellopts.nil?
136
- 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
137
223
  end
138
224
 
139
225
  private
226
+ # Default default key type
227
+ DEFAULT_KEY_TYPE = :name
228
+
140
229
  # Reset state variables
141
230
  def self.reset()
142
231
  @shellopts = nil
143
232
  end
144
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
+
145
239
  @shellopts = nil
240
+ @is_included_in_main = false
146
241
  end
242
+
@@ -0,0 +1,48 @@
1
+
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 call #error() in response to errors
6
+ class Args < Array
7
+ def initialize(shellopts, *args)
8
+ @shellopts = shellopts
9
+ super(*args)
10
+ end
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
20
+ def extract(count_or_range, message = nil)
21
+ if count_or_range.is_a?(Range)
22
+ range = count_or_range
23
+ range.min <= self.size or inoa(message)
24
+ n_extract = [self.size, range.max].min
25
+ n_extend = range.max > self.size ? range.max - self.size : 0
26
+ r = self.shift(n_extract) + Array.new(n_extend)
27
+ else
28
+ count = count_or_range
29
+ self.size >= count.abs or inoa(message)
30
+ start = count >= 0 ? 0 : size + count
31
+ r = slice!(start, count.abs)
32
+ r.size == 0 ? nil : (r.size == 1 ? r.first : r)
33
+ end
34
+ end
35
+
36
+ # As extract except it doesn't allow negative counts and that the array is
37
+ # expect to be emptied by the operation
38
+ def expect(count_or_range, message = nil)
39
+ count_or_range === self.size or inoa(message)
40
+ extract(count_or_range) # Can't fail
41
+ end
42
+
43
+ private
44
+ def inoa(message = nil)
45
+ raise ShellOpts::UserError, message || "Illegal number of arguments"
46
+ end
47
+ end
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