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

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