shellopts 2.0.17 → 2.0.20

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