shellopts 2.0.17 → 2.0.20

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: ebd7911e579af4ddddd064399e7b456b45cef80dc3c6589e404ba5e32811bd6c
4
- data.tar.gz: ea555ad68d0d6c43bad39a4e195e6661f72e1e43b6d3dbe647f4c8ff13d7eea3
3
+ metadata.gz: 83e0ce72f27c72eec2087bee9296378fc0d84fc1124f830af27f21340cefffcc
4
+ data.tar.gz: 6c3b2363c1ca392b0950c792c48e353181f352ca22bfe16e7c42490248511b29
5
5
  SHA512:
6
- metadata.gz: 453d3bd1e4354a56947623232f1fabcdc8d212a91d317ce16026c356c71c77ab53031e4e42d05242d0c04ce309ccd69fd75386d3d40705feac461f58d6a2c86b
7
- data.tar.gz: 282655ab16154e1b50c25b6ddf8a07196a296f8a2bad4086cd78b8962a9003c22e1fb8dfbe2e339fc97973fef95b1cf9965a829bab23b2ad98f63df1aef8c009
6
+ metadata.gz: dcc6e2271a451f494e007c78e9054c49bbb6f65f0f0fcf5be07158488157782c8216fc0f6fce7e4c25dda6bf0e39ac8ae2096f733cb4af7c4b1360b28a978129
7
+ data.tar.gz: e1d60f792447e5884f2ad939fbf408d5c053ef413328e2e2a42a082e34e9672537d8d64916fbc136f11797ac2c87894c148939d4feac9cc43489d3c66faa538d
data/PROBLEMS CHANGED
@@ -39,3 +39,14 @@ SPEC = %(
39
39
  mapping and grants are
40
40
  )
41
41
 
42
+ ###########################
43
+
44
+ # If option is only defined on a subcommand, it doesn't flow before the subcommand. This should be legal:
45
+
46
+ cmd -c sub
47
+
48
+ # Only this is legal today
49
+
50
+ cmd sub -c
51
+
52
+
data/TODO CHANGED
@@ -1,3 +1,19 @@
1
+
2
+ o Make opts[] point at the main object
3
+ opts[].subcommand is the direct subcommand of main
4
+ o Make opts be the aggrated set of options in a subcommand
5
+ opts.subcommand is the concatenated path of subcommands
6
+ o Make opts[:some_command] be the set of options for that subcommand (already done)
7
+ o Sub-command options can overshadow options of outer commands but it is an
8
+ error to access them using opts. Use opts[] or opts[:some_command] instead
9
+
10
+ o Add a default argument to extract:
11
+ def extract(range, *defaults)
12
+ end
13
+
14
+ extract(3..4, 'dbo')
15
+
16
+ o More strict parsing of option and command lines
1
17
  o --option can't be escaped if on the start of a block line?
2
18
  o Use require_relative
3
19
  o In the following list is a command with a mandatory sub-command
@@ -30,7 +30,7 @@ module ShellOpts
30
30
  def ancestors() parents.reverse end
31
31
 
32
32
  def inspect
33
- "#{self.class}"
33
+ self.class.to_s
34
34
  end
35
35
 
36
36
  protected
@@ -72,7 +72,7 @@ module ShellOpts
72
72
  # Note that the analyzer fails with an an error if both --with-separator
73
73
  # and --with_separator are used because they would both map to
74
74
  # :with_separator
75
- attr_reader :ident
75
+ attr_accessor :ident
76
76
 
77
77
  # Canonical name (String) of the object
78
78
  #
@@ -241,7 +241,7 @@ module ShellOpts
241
241
  def [](key)
242
242
  case key
243
243
  when String; lookup(key.split("."))
244
- when Symbol; lookup(key.to_s.sub(".", "!.").split(".").map(&:to_sym))
244
+ when Symbol; lookup(key.to_s.gsub(".", "!.").split(".").map(&:to_sym))
245
245
  when Array; lookup(key)
246
246
  else
247
247
  nil
@@ -23,7 +23,7 @@ module ShellOpts
23
23
  end
24
24
 
25
25
  class Option
26
- SHORT_NAME_RE = /[a-zA-Z0-9]/
26
+ SHORT_NAME_RE = /[a-zA-Z0-9?]/
27
27
  LONG_NAME_RE = /[a-zA-Z0-9][a-zA-Z0-9_-]*/
28
28
  NAME_RE = /(?:#{SHORT_NAME_RE}|#{LONG_NAME_RE})(?:,#{LONG_NAME_RE})*/
29
29
 
@@ -124,23 +124,18 @@ module ShellOpts
124
124
  super(nil, token)
125
125
  end
126
126
 
127
- def add_version_option
128
- option_token = Token.new(:option, 1, 1, "--version")
129
- brief_token = Token.new(:brief, 1, 1, "Write version number and exit")
127
+ def inject_option(decl, brief, paragraph = nil, &block)
128
+ option_token = Token.new(:option, 1, 1, decl)
129
+ brief_token = Token.new(:brief, 1, 1, brief)
130
130
  group = OptionGroup.new(self, option_token)
131
131
  option = Option.parse(group, option_token)
132
132
  brief = Brief.parse(group, brief_token)
133
- end
134
-
135
- def add_help_options
136
- option_token = Token.new(:option, 1, 1, "-h,help")
137
- brief_token = Token.new(:brief, 1, 1, "Write help text and exit")
138
- paragraph_token = Token.new(:text, 1, 1,
139
- "-h prints a brief help text, --help prints a longer man-style description of the command")
140
- group = OptionGroup.new(self, option_token)
141
- option = Option.parse(group, option_token)
142
- brief = Brief.parse(group, brief_token)
143
- paragraph = Paragraph.parse(group, paragraph_token)
133
+ paragraph ||= yield(option) if block_given?
134
+ if paragraph
135
+ paragraph_token = Token.new(:text, 1, 1, paragraph)
136
+ paragraph = Paragraph.parse(group, paragraph_token)
137
+ end
138
+ option
144
139
  end
145
140
  end
146
141
 
@@ -62,53 +62,49 @@ module ShellOpts
62
62
  object
63
63
  end
64
64
 
65
- # Return command or option object if present, otherwise nil. Returns a
66
- # possibly empty array of option objects if the option is repeatable
65
+ # Returns the command or option object identified by the UID if present and
66
+ # otherwise nil. Returns a possibly empty array of option objects if the
67
+ # option is repeatable. Raise an ArgumentError if the key doesn't exists
67
68
  #
68
- # The key is the name or identifier of the object or any any option
69
- # alias. Eg. :f, '-f', :file, or '--file' are all usable as option keys
70
- # and :cmd! or 'cmd' as command keys
69
+ # The +key+ is the symbolic UID of the object. Eg. :command.option or
70
+ # :command.subcommand!
71
71
  #
72
- def [](key)
73
- case object = __grammar__[key]
74
- when ::ShellOpts::Grammar::Command
75
- object.ident == __subcommand__!.__ident__ ? __subcommand__! : nil
76
- when ::ShellOpts::Grammar::Option
77
- if object.repeatable?
78
- __option_hash__[object.ident] || []
72
+ def [](uid)
73
+ __grammar__.key?(uid) or ::Kernel.raise ::ArgumentError, "'#{uid}' is not a valid UID"
74
+ idents = uid.to_s.gsub(/\./, "!.").split(/\./).map(&:to_sym)
75
+ idents.inject(self) { |cmd, ident|
76
+ case ident.to_s
77
+ when /!$/
78
+ return nil if cmd.__subcommand__ != ident
79
+ cmd = cmd.__subcommand__!
79
80
  else
80
- __option_hash__[object.ident]
81
- end
82
- else
83
- ::Kernel.raise ::ArgumentError, "Unknown command or option: '#{key}'"
84
- end
81
+ opt = cmd.__option_hash__[ident]
82
+ opt.nil? && cmd.__grammar__[ident].repeatable? ? [] : opt
83
+ end
84
+ }
85
85
  end
86
86
 
87
- # Return true if the given command or option is present
88
- def key?(key)
89
- case object = __grammar__[key]
90
- when ::ShellOpts::Grammar::Command
91
- object.ident == __subcommand__
92
- when ::ShellOpts::Grammar::Option
93
- __option_hash__.key?(object.ident)
94
- else
95
- ::Kernel.raise ::ArgumentError, "Unknown command or option: '#{key}'"
96
- end
97
- end
98
-
99
- # Returns a hash of the given options if defined. Returns all options if no
100
- # options are given
87
+ # Returns a hash from option ident to value
88
+ #
89
+ # The value depends on the option type: If it is not repeatable, the value
90
+ # is the argument or nil if not present (or allowed). If the option is
91
+ # repeatable and has an argument, the value is an array of the arguments,
92
+ # if it doesn't have an argument, the value is the number of occurrences
93
+ #
101
94
  def to_h(*keys)
102
95
  keys = ::Kernel::Array(keys).flatten
103
- if keys.empty?
104
- self.to_h(@__grammar__.options.map(&:ident))
105
- else
106
- keys.map { |key|
107
- self.__send__("#{key}?".to_sym) ? [key, self.__send__(key)] : nil
108
- }.compact.to_h
109
- end
96
+ __option_values__.select { |key,_| keys.empty? || keys.include?(key) }
110
97
  end
111
98
 
99
+ # Like #to_h but present options without arguments have a true value
100
+ #
101
+ def to_h?(*keys)
102
+ keys = ::Kernel::Array(keys).flatten
103
+ keys = keys.empty? ? __option_values__.keys : keys
104
+ keys.filter_map { |key| __option_values__.key?(key) && [key, self.__send__(key)] }.to_h
105
+ end
106
+
107
+
112
108
  # Subcommand identifier or nil if not present. #subcommand is often used in
113
109
  # case statement to branch out to code that handles the given subcommand:
114
110
  #
@@ -148,7 +144,7 @@ module ShellOpts
148
144
  #
149
145
  def supercommand!() __supercommand__ end
150
146
 
151
- # UID of command/program
147
+ # UID of command/program (String)
152
148
  def __uid__() @__grammar__.uid end
153
149
 
154
150
  # Identfier including the exclamation mark (Symbol)
@@ -309,6 +305,18 @@ module ShellOpts
309
305
 
310
306
  # The top-level command
311
307
  class Program < Command
308
+ # Accessors for standard options values that are not affected if the option
309
+ # is renamed
310
+ attr_accessor :__quiet__
311
+ attr_accessor :__verbose__
312
+ attr_accessor :__debug__
313
+
314
+ def initialize
315
+ super
316
+ @__quiet__ = false
317
+ @__verbose__ = 0
318
+ @__debug__ = false
319
+ end
312
320
  end
313
321
 
314
322
  # Option models an option as given by the user on the subcommand line.
@@ -1,3 +1,3 @@
1
1
  module ShellOpts
2
- VERSION = "2.0.17"
2
+ VERSION = "2.0.20"
3
3
  end
data/lib/shellopts.rb CHANGED
@@ -96,17 +96,20 @@ module ShellOpts
96
96
  # Automatically add a -h and a --help option if true
97
97
  attr_reader :help
98
98
 
99
- # Version of client program. If not nil a --version option is added to the program
100
- def version
101
- return @version if @version
102
- exe = caller.find { |line| line =~ /`<top \(required\)>'$/ }&.sub(/:.*/, "")
103
- file = Dir.glob(File.dirname(exe) + "/../lib/*/version.rb").first
104
- @version = IO.read(file).sub(/^.*VERSION\s*=\s*"(.*?)".*$/m, '\1') or
105
- raise ArgumentError, "ShellOpts needs an explicit version"
106
- end
99
+ # Version of client program. If not nil, a --version option is added to the program
100
+ attr_reader :version
101
+
102
+ # Automatically add a -q and a --quiet option if true
103
+ attr_reader :quiet
107
104
 
108
- # Add message options (TODO)
109
- attr_accessor :msgopts
105
+ # Automatically add a -v and a --verbose option if true
106
+ attr_reader :verbose
107
+
108
+ # Automatically add a --debug option if true
109
+ attr_reader :debug
110
+
111
+ # Version number (this is usually detected dynamically)
112
+ attr_reader :version_number
110
113
 
111
114
  # Floating options
112
115
  attr_accessor :float
@@ -122,12 +125,33 @@ module ShellOpts
122
125
  attr_reader :tokens
123
126
  alias_method :ast, :grammar
124
127
 
125
- def initialize(name: nil, help: true, version: true, msgopts: false, float: true, exception: false)
128
+ def initialize(name: nil,
129
+ # Options
130
+ help: true,
131
+ version: true,
132
+ quiet: nil,
133
+ verbose: nil,
134
+ debug: nil,
135
+
136
+ # Version number (usually detected)
137
+ version_number: nil,
138
+
139
+ # Floating options
140
+ float: true,
141
+
142
+ # Let exceptions through
143
+ exceptions: false
144
+ )
145
+
126
146
  @name = name || File.basename($PROGRAM_NAME)
127
147
  @help = help
128
- @use_version = version ? true : false
129
- @version = @use_version && @version != true ? @version : nil
130
- @msgopts, @float, @exception = msgopts, float, exception
148
+ @version = version || (version.nil? && !version_number.nil?)
149
+ @quiet = quiet
150
+ @verbose = verbose
151
+ @debug = debug
152
+ @version_number = version_number || find_version_number
153
+ @float = float
154
+ @exception = exception
131
155
  end
132
156
 
133
157
  # Compile source and return grammar object. Also sets #spec and #grammar.
@@ -139,8 +163,31 @@ module ShellOpts
139
163
  @file = find_caller_file
140
164
  @tokens = Lexer.lex(name, @spec, @oneline)
141
165
  ast = Parser.parse(tokens)
142
- ast.add_version_option if @use_version
143
- ast.add_help_options if @help
166
+
167
+ help_spec = (@help == true ? "-h,help" : @help)
168
+ version_spec = (@version == true ? "--version" : @version)
169
+ quiet_spec = (@quiet == true ? "-q,quiet" : @quiet)
170
+ verbose_spec = (@verbose == true ? "+v,verbose" : @verbose)
171
+ debug_spec = (@debug == true ? "--debug" : @debug)
172
+
173
+ @quiet_option =
174
+ ast.inject_option(quiet_spec, "Quiet", "Do not write anything to standard output") if @quiet
175
+ @verbose_option =
176
+ ast.inject_option(verbose_spec, "Increase verbosity", "Write verbose output") if @verbose
177
+ @debug_option =
178
+ ast.inject_option(debug_spec, "Write debug information") if @debug
179
+ @help_option =
180
+ ast.inject_option(help_spec, "Write short or long help") { |option|
181
+ short_option = option.short_names.first
182
+ long_option = option.long_names.first
183
+ [
184
+ short_option && "#{short_option} prints a brief help text",
185
+ long_option && "#{long_option} prints a longer man-style description of the command"
186
+ ].compact.join(", ")
187
+ } if @help
188
+ @version_option =
189
+ ast.inject_option(version_spec, "Write version number and exit") if @version
190
+
144
191
  @grammar = Analyzer.analyze(ast)
145
192
  }
146
193
  self
@@ -153,16 +200,22 @@ module ShellOpts
153
200
  handle_exceptions {
154
201
  @argv = argv.dup
155
202
  @program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
156
- if @program.version?
157
- puts version
158
- exit
159
- elsif @program.help?
160
- if @program[:help].name == "-h"
161
- ShellOpts.brief
162
- else
203
+
204
+ # Process standard options (that may have been renamed)
205
+ if @program.__send__(:"#{@help_option.ident}?")
206
+ if @program[:help].name =~ /^--/
163
207
  ShellOpts.help
208
+ else
209
+ ShellOpts.brief
164
210
  end
165
211
  exit
212
+ elsif @program.__send__(:"#{@version_option.ident}?")
213
+ puts version_number
214
+ exit
215
+ else
216
+ @program.__quiet__ = @program.__send__(:"#{@quiet_option.ident}?") if @quiet
217
+ @program.__verbose__ = @program.__send__(:"#{@verbose_option.ident}") if @verbose
218
+ @program.__debug__ = @program.__send__(:"#{@debug_option.ident}?") if @debug
166
219
  end
167
220
  }
168
221
  self
@@ -235,6 +288,13 @@ module ShellOpts
235
288
  def self.help(subject = nil) ::ShellOpts.instance.help(subject) end
236
289
 
237
290
  private
291
+ def find_version_number
292
+ exe = caller.find { |line| line =~ /`<top \(required\)>'$/ }&.sub(/:.*/, "") or return nil
293
+ file = Dir.glob(File.dirname(exe) + "/../lib/*/version.rb").first or return nil
294
+ IO.read(file).sub(/^.*VERSION\s*=\s*"(.*?)".*$/m, '\1') or
295
+ raise ArgumentError, "ShellOpts needs an explicit version"
296
+ end
297
+
238
298
  def handle_exceptions(&block)
239
299
  return yield if exception
240
300
  begin
@@ -322,9 +382,12 @@ module ShellOpts
322
382
  end
323
383
  end
324
384
 
325
- def self.process(spec, argv, msgopts: false, **opts)
326
- msgopts ||= Messages.is_included?
327
- ShellOpts.process(spec, argv, msgopts: msgopts, **opts)
385
+ def self.process(spec, argv, quiet: nil, verbose: nil, debug: nil, **opts)
386
+ constrain quiet, String, true, false, nil
387
+ quiet = quiet.nil? ? Message.is_included? || Verbose.is_included? : quiet
388
+ verbose = verbose.nil? ? ::ShellOpts::Verbose.is_included? : verbose
389
+ debug = debug.nil? ? Debug.is_included? : debug
390
+ ShellOpts.process(spec, argv, quiet: quiet, verbose: verbose, debug: debug, **opts)
328
391
  end
329
392
 
330
393
  @instance = nil
@@ -345,31 +408,57 @@ module ShellOpts
345
408
  exit 1
346
409
  end
347
410
 
348
- # The Include module brings the reporting methods into the namespace when
349
- # included
350
- module Messages
411
+ def self.notice(message)
412
+ $stderr.puts "#{instance.program.__grammar__.name}: #{message}" \
413
+ if !instance.quiet || !instance.program.quiet?
414
+ end
415
+
416
+ def self.mesg(message)
417
+ $stdout.puts message if !instance.quiet || !instance.program.__quiet__
418
+ end
419
+
420
+ def self.verb(level = 1, message)
421
+ $stdout.puts message if instance.verbose && level <= instance.program.__verbose__
422
+ end
423
+
424
+ def self.debug(message)
425
+ $stdout.puts message if instance.debug && instance.program.__debug__
426
+ end
427
+
428
+ def self.quiet_flag
429
+ end
430
+
431
+ def self.verbose_flag
432
+ end
433
+
434
+ def self.debug_flag
435
+ end
436
+
437
+ module Message
351
438
  @is_included = false
352
439
  def self.is_included?() @is_included end
353
- def self.include(...)
354
- @is_included = true
355
- super
356
- end
440
+ def self.included(...) @is_included = true; super end
357
441
 
358
- def notice(message)
359
- $stderr.puts "#{name}: #{message}" if !quiet?
360
- end
442
+ def notice(message) ::ShellOpts.notice(message) end
443
+ def mesg(message) ::ShellOpts.mesg(message) end
444
+ end
361
445
 
362
- def mesg(message)
363
- $stdout.puts message if !quiet?
364
- end
446
+ module Verbose
447
+ @is_included = false
448
+ def self.is_included?() @is_included end
449
+ def self.included(...) @is_included = true; super end
365
450
 
366
- def verb(level = 1, message)
367
- $stdout.puts message if level <= @verbose
368
- end
451
+ def notice(message) ::ShellOpts.notice(message) end
452
+ def mesg(message) ::ShellOpts.mesg(message) end
453
+ def verb(level = 1, message) ::ShellOpts.verb(level, message) end
454
+ end
369
455
 
370
- def debug(message)
371
- $stdout.puts message if debug?
372
- end
456
+ module Debug
457
+ @is_included = false
458
+ def self.is_included?() @is_included end
459
+ def self.included(...) @is_included = true; super end
460
+
461
+ def debug(message) ::ShellOpts.debug(message) end
373
462
  end
374
463
 
375
464
  module ErrorHandling