shellopts 2.4.3 → 2.5.0

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: 4620f77a96e0e880d03301d91100824186c02ec20b68ffa22ff7032bb3b269d2
4
- data.tar.gz: 628d2c0653ac9d565a860bb5941bc1ca9cf4c13c6d5709e78b355d4183c4d48b
3
+ metadata.gz: dc8c49d6d58b38a3fdff578630ef13abe8f3c6c5939cf5ff8098198eb8a17fbe
4
+ data.tar.gz: 181c71564cd5a755bc3e17a19652748b8a276c2cd910a25c1f8d3d39bd6d27c6
5
5
  SHA512:
6
- metadata.gz: 691e8376f9b28bfdce08cbaf64fb2cf871d09417992433f4e48b77288e1b5ccf2cc049328607e8b94e35222e631fd29c01b96bb71602bc053ab4ce1ab1b435db
7
- data.tar.gz: 44fe34f706f91a8cee7df43a48032a97ed31fd315e04edf7c5f6a2842a61bcb010663fc63ad78da38607342b90d8617c266f0b9e56e2c70fa6d3c7befcf9f10d
6
+ metadata.gz: c6fb75cc8baf4738276d25ebddb0dd7827ff816cd124486e5ac681f3fe2d9f80aee1bd9bc2d409eec4e65d476d897c481505f95ca60eb27b70259aac548657b1
7
+ data.tar.gz: ef3e4129aea6baab3b683b25c51bd77d0df222c0cfb18effc0b26e9d8497e66fc2735c32e53e1b67030030824e78bc4fe59c7fd8f17d6e0c8daffb31949f9434
data/README.md CHANGED
@@ -74,11 +74,11 @@ to check presence and return an optional argument. Given the options "--alpha
74
74
 
75
75
  ```ruby
76
76
  # Returns true if the option is present and false otherwise
77
- opts.alpha?()
77
+ opts.alpha?()
78
78
  opts.beta?()
79
79
 
80
80
  # Returns the argument of the beta option or nil if missing
81
- opts.beta()
81
+ opts.beta()
82
82
  ```
83
83
 
84
84
  Given the commands "cmd1! cmd2!" the following methods are available:
@@ -87,7 +87,7 @@ Given the commands "cmd1! cmd2!" the following methods are available:
87
87
  # Returns the sub-command object or nil if not present
88
88
  opts.cmd1!
89
89
  opts.cmd2!
90
-
90
+
91
91
  opts.subcommand! # Returns the sub-command object or nil if not present
92
92
  opts.subcommand # Returns the sub-command's identifier (eg. :cmd1!)
93
93
  ```
@@ -96,7 +96,7 @@ It is used like this
96
96
 
97
97
  ```ruby
98
98
  case opts.subcommand
99
- when :cmd1
99
+ when :cmd1
100
100
  # opts.cmd1 is defined here
101
101
  when :cmd2
102
102
  # opts.cmd2 is defined here
@@ -160,7 +160,7 @@ is paragraphs
160
160
  -a,alpha @ Brief comment for -a and --alpha options
161
161
  Longer description of the option that is used by `::help`
162
162
 
163
- cmd!
163
+ cmd!
164
164
  @ Alternative style of brief comment
165
165
 
166
166
  Longer description of the command
@@ -179,23 +179,28 @@ error messages:
179
179
  The general syntax for options is
180
180
 
181
181
  ```
182
- <prefix><optionlist>[=argspec][?]
182
+ <prefix><optionlist>[=argspec][,][?]
183
183
  ```
184
184
 
185
+ (TODO: Restructure: prefix,options,argument-spec,argument-spec-modifier,etc.)
186
+
185
187
  The option list is a comma-separated list of option names. It is prefixed with
186
188
  a '-' if the option list starts with a short option name and '--' if the option
187
189
  list starts with a long name. '-' and '--' can be replaced with '+' or '++' to
188
190
  indicate that the option can be repeated
189
191
 
190
192
  ```
191
- -a,alpha @ -a and --alpha
192
- ++beta @ --beta, can be repeated
193
- --gamma=ARG? @ --gamma, takes an optional argument
193
+ -a,alpha @ '-a' and '--alpha'
194
+ ++beta @ '--beta', can be repeated
195
+ --gamma=ARG? @ '--gamma', takes an optional argument
196
+ --delta=ARG, @ '--delta', takes a mandatory comma-separated list of arguments
197
+ --epsilon=ARG,? @ '--delta', takes an optional list
194
198
  ```
195
199
 
196
200
  An option argument has a name and a type. The type can be specified as '#'
197
- (integer), '$' (float), or as a comma-separated list of allowed values. It can
198
- also be defined by a keyword that expects a file or directory argument:
201
+ (integer), '$' (float), ',' (list) or as a comma-separated list of allowed
202
+ values (enum). The name should be in capital letters. Some names are keywords
203
+ with a special meaning:
199
204
 
200
205
  | Keyword | Type |
201
206
  | --------- | ---- |
@@ -219,7 +224,7 @@ explicitly by separating it from the type with a ':'. Examples:
219
224
  -c=red,blue,green @ -c takes one of the listed words
220
225
  -d=FILE @ Fails if file exists and is not a file
221
226
  -d=EDIR @ Fails if directory doesn't exist or is not a directory
222
- -d=INPUT:EFILE @ Takes and existing file. Shown as '-d=INPUT' in messages
227
+ -d=INPUT:EFILE @ Expects an existing file. Shown as '-d=INPUT' in messages
223
228
  ```
224
229
 
225
230
  ## Commands
@@ -234,7 +239,7 @@ command line like this
234
239
  cmd!
235
240
  -b @ Command level option
236
241
  subcmd!
237
- -c @ Sub-command level option
242
+ -c @ Sub-command level option
238
243
  ```
239
244
 
240
245
  In single-line format, subcommands are specified by prefixing the supercommand's name:
@@ -256,7 +261,7 @@ SPEC = %(
256
261
  -f,force @ ignore nonexisten files and arguments, never prompt
257
262
  -i @ prompt before every removal
258
263
 
259
- -I
264
+ -I
260
265
  @ prompt once
261
266
 
262
267
  prompt once before removing more than three files, or when removing
data/TODO CHANGED
@@ -1,3 +1,25 @@
1
+ # TODO
2
+ o A program framework to make it easier to handle commands:
3
+ class Program
4
+ def check!() end
5
+ def fix!() end
6
+ def list!() end
7
+
8
+ def initialize(spec, argv)
9
+ end
10
+
11
+ def command(cmd)
12
+ self.send(cmd.subcommand)
13
+ end
14
+ end
15
+
16
+ o We really need a global #indent/#outdent set of methods
17
+ o Make it possible to override the documentation of built-in options (ex. --verbose)
18
+ o Create global IO-like objects for each output channel: $mesg, $verb, $notice,
19
+ $warn, $error, $failure so that they can be used together with the usual
20
+ output functions #puts, #print, #printf, #indent etc.
21
+
22
+ ShellOpts.mesg "string" -> $mesg.puts "string"
1
23
 
2
24
  o Remove IFILE and OFILE. Alternatively document them
3
25
  o Document special-case file argument '-'
@@ -14,8 +14,8 @@ module ShellOpts
14
14
  children.delete_if { |node| node.is_a?(ArgSpec) }
15
15
  end
16
16
 
17
- def analyzer_error(token, message)
18
- raise AnalyzerError.new(token), message
17
+ def analyzer_error(token, message)
18
+ raise AnalyzerError.new(token), message
19
19
  end
20
20
  end
21
21
 
@@ -27,7 +27,7 @@ module ShellOpts
27
27
  # Move options before first command or before explicit COMMAND section
28
28
  def reorder_options
29
29
  if commands.any?
30
- i = children.find_index { |child|
30
+ i = children.find_index { |child|
31
31
  child.is_a?(Command) || child.is_a?(Section) && child.name == "COMMAND"
32
32
  }
33
33
  if i
@@ -40,10 +40,10 @@ module ShellOpts
40
40
  def compute_option_hashes
41
41
  options.each { |option|
42
42
  option.idents.zip(option.names).each { |ident, name|
43
- !@options_hash.key?(name) or
43
+ !@options_hash.key?(name) or
44
44
  analyzer_error option.token, "Duplicate option name: #{name}"
45
45
  @options_hash[name] = option
46
- !@options_hash.key?(ident) or
46
+ !@options_hash.key?(ident) or
47
47
  analyzer_error option.token, "Can't use both #{@options_hash[ident].name} and #{name}"
48
48
  @options_hash[ident] = option
49
49
  }
@@ -53,7 +53,7 @@ module ShellOpts
53
53
  # TODO Check for dash-collision
54
54
  def compute_command_hashes
55
55
  commands.each { |command|
56
- !@commands_hash.key?(command.name) or
56
+ !@commands_hash.key?(command.name) or
57
57
  analyzer_error command.token, "Duplicate command name: #{command.name}"
58
58
  @commands_hash[command.name] = command
59
59
  @commands_hash[command.ident] = command
@@ -75,7 +75,7 @@ module ShellOpts
75
75
  def create_implicit_commands(cmd)
76
76
  path = cmd.path[0..-2]
77
77
 
78
-
78
+
79
79
  end
80
80
 
81
81
  # Link up commands with supercommands. This is only done for commands that
@@ -83,7 +83,7 @@ module ShellOpts
83
83
  # parent/child relationship is not changed Example:
84
84
  #
85
85
  # cmd!
86
- # cmd.subcmd!
86
+ # cmd.subcmd!
87
87
  #
88
88
  # Here subcmd is added to cmd's list of commands. It keeps its position in
89
89
  # the program's parent/child relationship so that documentation will print the
@@ -147,8 +147,8 @@ module ShellOpts
147
147
 
148
148
  @grammar.compute_command_hashes
149
149
 
150
- @grammar.traverse { |node|
151
- node.remove_brief_nodes
150
+ @grammar.traverse { |node|
151
+ node.remove_brief_nodes
152
152
  node.remove_arg_descr_nodes
153
153
  node.remove_arg_spec_nodes
154
154
  }
@@ -21,7 +21,7 @@ module ShellOpts
21
21
  # array. If the count is negative, the elements will be removed from the
22
22
  # end of the array. If +count_or_range+ is a range, the number of elements
23
23
  # returned will be in that range. Note that the range can't contain
24
- # negative numbers
24
+ # negative numbers
25
25
  #
26
26
  # #extract raise a ShellOpts::Error exception if there's is not enough
27
27
  # elements in the array to satisfy the request
@@ -51,7 +51,7 @@ module ShellOpts
51
51
  # As #extract except the array is expected to be emptied by the operation.
52
52
  # Raise a #inoa exception if count is negative
53
53
  #
54
- # #expect raise a ShellOpts::Error exception if the array is not emptied
54
+ # #expect raise a ShellOpts::Error exception if the array is not emptied
55
55
  # by the operation
56
56
  #
57
57
  # TODO: Better handling of ranges. Allow: 2..-1, -2..-4, etc.
@@ -37,9 +37,9 @@ module ShellOpts
37
37
  end
38
38
 
39
39
  class IntegerArgument < ArgumentType
40
- def match?(name, literal)
41
- literal =~ /^-?\d+$/ or
42
- set_message "Illegal integer value in #{name}: #{literal}"
40
+ def match?(name, literal)
41
+ literal =~ /^-?\d+$/ or
42
+ set_message "Illegal integer value in #{name}: #{literal}"
43
43
  end
44
44
 
45
45
  def value?(value) value.is_a?(Integer) end
@@ -47,9 +47,9 @@ module ShellOpts
47
47
  end
48
48
 
49
49
  class FloatArgument < ArgumentType
50
- def match?(name, literal)
50
+ def match?(name, literal)
51
51
  # https://stackoverflow.com/a/21891705/2130986
52
- literal =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
52
+ literal =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
53
53
  set_message "Illegal decimal value in #{name}: #{literal}"
54
54
  end
55
55
 
@@ -61,7 +61,7 @@ module ShellOpts
61
61
  attr_reader :kind
62
62
 
63
63
  def subject # Used in error messages
64
- @subject ||=
64
+ @subject ||=
65
65
  case kind
66
66
  when :file, :efile; "regular file"
67
67
  when :nfile, :ifile, :ofile; "file"
@@ -74,7 +74,7 @@ module ShellOpts
74
74
 
75
75
  def initialize(kind)
76
76
  constrain kind, :file, :dir, :path, :efile, :edir, :epath, :nfile, :ndir, :npath, :ifile, :ofile
77
- @kind = kind
77
+ @kind = kind
78
78
  end
79
79
 
80
80
  def match?(name, literal)
@@ -168,7 +168,7 @@ module ShellOpts
168
168
  end
169
169
 
170
170
  # file does not exist
171
- else
171
+ else
172
172
  if [:default, :new].include? mode
173
173
  dir = File.dirname(literal)
174
174
  if !File.directory?(dir)
@@ -79,9 +79,9 @@ module ShellOpts
79
79
  def dump_idr(short = false)
80
80
  if short
81
81
  s = [
82
- name,
83
- argument? ? argument_type.name : nil,
84
- optional? ? "?" : nil,
82
+ name,
83
+ argument? ? argument_type.name : nil,
84
+ optional? ? "?" : nil,
85
85
  repeatable? ? "*" : nil
86
86
  ].compact.join(" ")
87
87
  puts s
@@ -89,9 +89,9 @@ module ShellOpts
89
89
  puts "#{name}: #{classname}"
90
90
  dump_attrs(
91
91
  :uid, :path, :attr, :ident, :name, :idents, :names,
92
- :repeatable?,
93
- :argument?, argument? && :argument_name, argument? && :argument_type,
94
- :enum?, enum? && :argument_enum,
92
+ :repeatable?,
93
+ :argument?, argument? && :argument_name, argument? && :argument_type,
94
+ :enum?, enum? && :argument_enum,
95
95
  :optional?)
96
96
  indent { puts "brief: #{group.brief}" }
97
97
  end
@@ -102,11 +102,11 @@ module ShellOpts
102
102
  def dump_idr(short = false)
103
103
  if short
104
104
  puts name
105
- indent {
105
+ indent {
106
106
  options.each { |option| option.dump_idr(short) }
107
107
  commands.each { |command| command.dump_idr(short) }
108
108
  descrs.each { |descr| descr.dump_idr(short) }
109
- }
109
+ }
110
110
  else
111
111
  puts "#{name}: #{classname}"
112
112
  dump_attrs :uid, :path, :ident, :name, :options, :commands, :specs, :descrs, :brief
@@ -34,15 +34,15 @@ module ShellOpts
34
34
  width = [Formatter.rest, max_width].min
35
35
  if descrs.size == 0
36
36
  print (lead = Formatter.command_prefix || "")
37
- indent(lead.size, ' ', bol: bol && lead == "") {
38
- puts render(:multi, width)
37
+ indent(lead.size, ' ', bol: bol && lead == "") {
38
+ puts render(:multi, width)
39
39
  }
40
40
  else
41
41
  lead = Formatter.command_prefix || ""
42
42
  descrs.each { |descr|
43
43
  print lead
44
- puts render(:multi, width, args: descr.text.split(' '))
45
- }
44
+ puts render(:multi, width, args: descr.text.split(' '))
45
+ }
46
46
  end
47
47
  end
48
48
 
@@ -120,7 +120,7 @@ module ShellOpts
120
120
  Command => "COMMAND"
121
121
  }
122
122
  seen_sections = {}
123
- newline = false # True if a newline should be printed before child
123
+ newline = false # True if a newline should be printed before child
124
124
  indent {
125
125
  children.each { |child|
126
126
  klass = child.is_a?(Section) ? section.key(child.name) : child.class
@@ -174,7 +174,7 @@ module ShellOpts
174
174
  module WrappedNode
175
175
  def puts_descr
176
176
  width = [Formatter.rest, Formatter::HELP_MAX_WIDTH].min
177
- puts lines(width)
177
+ puts lines(width)
178
178
  end
179
179
  end
180
180
 
@@ -220,7 +220,7 @@ module ShellOpts
220
220
  HELP_INDENT = 4
221
221
 
222
222
  # Max. width of help text (not including indent)
223
- HELP_MAX_WIDTH = 85
223
+ HELP_MAX_WIDTH = 85
224
224
 
225
225
  # Command prefix when subject is a sub-command
226
226
  def self.command_prefix() @command_prefix end
@@ -279,7 +279,7 @@ module ShellOpts
279
279
  # +fields+ is an array of [subject-string, descr-text] tuples where the
280
280
  # descr is an array of words
281
281
  def self.compute_columns(width, fields)
282
- first_max =
282
+ first_max =
283
283
  fields.map { |first, _| first.size }.select { |size| size <= BRIEF_COL1_MAX_WIDTH }.max ||
284
284
  BRIEF_COL1_MIN_WIDTH
285
285
  second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1 : 0 }.max
@@ -54,8 +54,8 @@ module ShellOpts
54
54
  # for the Program object. It is the dot-joined elements of path with
55
55
  # internal exclamation marks removed (eg. "cmd.opt" or "cmd.cmd!").
56
56
  # Initialize by the analyzer
57
- def uid()
58
- @uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
57
+ def uid()
58
+ @uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
59
59
  end
60
60
 
61
61
  # Path from Program object and down to this node. Array of identifiers.
@@ -142,6 +142,7 @@ module ShellOpts
142
142
  def repeatable?() @repeatable end
143
143
  def argument?() @argument end
144
144
  def optional?() @optional end
145
+ def list?() @list end
145
146
 
146
147
  def integer?() @argument_type.is_a? IntegerArgument end
147
148
  def float?() @argument_type.is_a? FloatArgument end
@@ -250,7 +251,7 @@ module ShellOpts
250
251
 
251
252
  def key?(key) !self.[](key).nil? end
252
253
 
253
- # Mostly for debug. Has questional semantics because it only lists local keys
254
+ # Mostly for debug. Has questional semantics because it only lists local keys
254
255
  def keys() @options_hash.keys + @commands_hash.keys end
255
256
 
256
257
  # Shorthand to get the associated Grammar::Command object from a Program
@@ -312,7 +313,7 @@ module ShellOpts
312
313
  alias_method :spec, :parent
313
314
  end
314
315
 
315
- # DocNode object has no children but lines.
316
+ # DocNode object has no children but lines.
316
317
  #
317
318
  class DocNode < Node
318
319
  # Array of :text tokens. Assigned by the parser
@@ -80,15 +80,23 @@ module ShellOpts
80
80
  elsif !value.nil?
81
81
  error "No argument allowed for option '#{opt_name}'"
82
82
  end
83
-
83
+
84
84
  Command.add_option(option_command, Option.new(option, name, value))
85
85
  end
86
86
 
87
87
  def interpret_option_value(option, name, value)
88
+ if option.list?
89
+ value.split(",").map { |elem| interpret_option_value_element(option, name, elem) }
90
+ else
91
+ interpret_option_value_element(option, name, value)
92
+ end
93
+ end
94
+
95
+ def interpret_option_value_element(option, name, elem)
88
96
  type = option.argument_type
89
- if type.match?(name, value)
90
- type.convert(value)
91
- elsif value == ""
97
+ if type.match?(name, elem)
98
+ type.convert(elem)
99
+ elsif elem == ""
92
100
  nil
93
101
  else
94
102
  error type.message
@@ -2,7 +2,7 @@
2
2
  module ShellOpts
3
3
  class Line
4
4
  attr_reader :source
5
- attr_reader :lineno
5
+ attr_reader :lineno
6
6
  attr_reader :charno
7
7
  attr_reader :text
8
8
 
@@ -52,7 +52,7 @@ module ShellOpts
52
52
  attr_reader :name # Name of program
53
53
  attr_reader :source
54
54
  attr_reader :tokens
55
-
55
+
56
56
  def oneline?() @oneline end
57
57
 
58
58
  def initialize(name, source, oneline)
@@ -88,7 +88,7 @@ module ShellOpts
88
88
  @tokens << Token.new(:blank, line.lineno, line.charno, "")
89
89
  next
90
90
  end
91
-
91
+
92
92
  # Ignore meta comments
93
93
  if line.charno < initial_indent
94
94
  next if line =~ /^#/
@@ -129,8 +129,8 @@ module ShellOpts
129
129
  @tokens << Token.new(:usage_string, line.lineno, charno, source)
130
130
  when "++" # FIXME Rename argspec
131
131
  @tokens << Token.new(:spec, line.lineno, charno, "++")
132
- words.shift_while { |c,w|
133
- w =~ SPEC_RE and @tokens << Token.new(:argument, line.lineno, c, w)
132
+ words.shift_while { |c,w|
133
+ w =~ SPEC_RE and @tokens << Token.new(:argument, line.lineno, c, w)
134
134
  }
135
135
  when /^-|\+/
136
136
  @tokens << Token.new(:option, line.lineno, charno, word)
@@ -143,7 +143,7 @@ module ShellOpts
143
143
  end
144
144
 
145
145
  # TODO: Move to parser and remove @oneline from Lexer
146
- (token = @tokens.last).kind != :brief || !oneline? or
146
+ (token = @tokens.last).kind != :brief || !oneline? or
147
147
  lexer_error token, "Briefs are only allowed in multi-line specifications"
148
148
 
149
149
  # Paragraph lines
@@ -151,13 +151,13 @@ module ShellOpts
151
151
  @tokens << Token.new(:text, line.lineno, line.charno, source)
152
152
  end
153
153
  # FIXME Not sure about this
154
- # last_nonblank = @tokens.last
155
- last_nonblank = @tokens.last if ![:blank, :usage_string, :argument].include? @tokens.last.kind
154
+ # last_nonblank = @tokens.last
155
+ last_nonblank = @tokens.last if ![:blank, :usage_string, :argument].include? @tokens.last.kind
156
156
  end
157
157
 
158
158
  # Move arguments and briefs before first command if one-line source
159
159
  # if oneline? && cmd_index = @tokens.index { |token| token.kind == :command }
160
- # @tokens =
160
+ # @tokens =
161
161
  # @tokens[0...cmd_index] +
162
162
  # @tokens[cmd_index..-1].partition { |token| ![:command, :option].include?(token.kind) }.flatten
163
163
  # end
@@ -0,0 +1,27 @@
1
+ module ShellOpts
2
+ # Option models an option as given by the user on the subcommand line.
3
+ # Compiled options (and possibly aggregated) options are stored in the
4
+ # Command#__option_values__ array
5
+ class Option
6
+ # Associated Grammar::Option object
7
+ attr_reader :grammar
8
+
9
+ # The actual name used on the shell command-line (String)
10
+ attr_reader :name
11
+
12
+ # Argument value or nil if not present. The value is a String, Integer,
13
+ # or Float depending the on the type of the option
14
+ attr_accessor :argument
15
+
16
+ forward_to :grammar,
17
+ :uid, :ident,
18
+ :repeatable?, :argument?, :integer?, :float?,
19
+ :file?, :enum?, :string?, :optional?, :list?,
20
+ :argument_name, :argument_type, :argument_enum,
21
+ :short_idents, :long_idents
22
+
23
+ def initialize(grammar, name, argument)
24
+ @grammar, @name, @argument = grammar, name, argument
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,6 @@
1
1
 
2
2
  module ShellOpts
3
+ # Extend Grammar classes with parse methods
3
4
  module Grammar
4
5
  class Node
5
6
  def parse() end
@@ -28,13 +29,13 @@ module ShellOpts
28
29
  NAME_RE = /(?:#{SHORT_NAME_RE}|#{LONG_NAME_RE})(?:,#{LONG_NAME_RE})*/
29
30
 
30
31
  def parse
31
- token.source =~ /^(-|--|\+|\+\+)(#{NAME_RE})(?:=(.+?)(\?)?)?$/ or
32
+ token.source =~ /^(-|--|\+|\+\+)(#{NAME_RE})(?:=(.+?)(,\??|\?,?)?)?$/ or
32
33
  parser_error token, "Illegal option: #{token.source.inspect}"
33
34
  initial = $1
34
35
  name_list = $2
35
36
  arg = $3
36
- optional = $4
37
-
37
+ @optional = $4&.include?(??) || false
38
+ @list = $4&.include?(?,) || false
38
39
  @repeatable = %w(+ ++).include?(initial)
39
40
 
40
41
  @short_idents = []
@@ -48,8 +49,8 @@ module ShellOpts
48
49
  end
49
50
  end
50
51
 
51
- names.each { |name|
52
- name.size > 1 or
52
+ names.each { |name|
53
+ name.size > 1 or
53
54
  parser_error token, "Long names should be at least two characters long: '#{name}'"
54
55
  }
55
56
 
@@ -94,7 +95,6 @@ module ShellOpts
94
95
  @argument_name = arg
95
96
  @argument_type = StringType.new
96
97
  end
97
- @optional = !optional.nil?
98
98
  else
99
99
  @argument_type = StringType.new
100
100
  end
@@ -182,7 +182,7 @@ module ShellOpts
182
182
  when :option
183
183
  # Collect options into option groups if on the same line and not in
184
184
  # oneline mode
185
- options = [token] + @tokens.shift_while { |follow|
185
+ options = [token] + @tokens.shift_while { |follow|
186
186
  !oneline && follow.kind == :option && follow.lineno == token.lineno
187
187
  }
188
188
  group = Grammar::OptionGroup.new(cmds.top, token)
@@ -266,7 +266,7 @@ module ShellOpts
266
266
  else
267
267
  if nodes.top.is_a?(Grammar::Command) || nodes.top.is_a?(Grammar::OptionGroup)
268
268
  Grammar::Brief.new(nodes.top, token, token.source.sub(/\..*/, "")) if !nodes.top.brief
269
- parent = nodes.top
269
+ parent = nodes.top
270
270
  else
271
271
  parent = nodes.top.parent
272
272
  end
@@ -174,7 +174,7 @@ module ShellOpts
174
174
 
175
175
  # The parent command or nil. Initialized by #add_command
176
176
  attr_accessor :__supercommand__
177
-
177
+
178
178
  # The subcommand identifier (a Symbol incl. the exclamation mark) or nil
179
179
  # if not present. Use #subcommand!, or the dynamically generated
180
180
  # '#<identifier>!' method to get the actual subcommand object
@@ -197,7 +197,7 @@ module ShellOpts
197
197
  def __initialize__(grammar)
198
198
  @__grammar__ = grammar
199
199
  @__option_values__ = {}
200
- @__option_list__ = []
200
+ @__option_list__ = []
201
201
  @__option_hash__ = {}
202
202
  @__option_values__ = {}
203
203
  @__subcommand__ = nil
@@ -209,22 +209,22 @@ module ShellOpts
209
209
  @__grammar__.options.each { |opt|
210
210
  if !opt.repeatable?
211
211
  self.instance_eval %(
212
- def #{opt.attr}?()
213
- @__option_values__.key?(:#{opt.attr})
212
+ def #{opt.attr}?()
213
+ @__option_values__.key?(:#{opt.attr})
214
214
  end
215
215
  )
216
216
  end
217
-
217
+
218
218
  if opt.repeatable?
219
219
  if opt.argument?
220
220
  self.instance_eval %(
221
- def #{opt.attr}?()
222
- (@__option_values__[:#{opt.attr}]&.size || 0) > 0
221
+ def #{opt.attr}?()
222
+ (@__option_values__[:#{opt.attr}]&.size || 0) > 0
223
223
  end
224
224
  )
225
225
  self.instance_eval %(
226
226
  def #{opt.attr}(default = [])
227
- if @__option_values__.key?(:#{opt.attr})
227
+ if @__option_values__.key?(:#{opt.attr})
228
228
  @__option_values__[:#{opt.attr}]
229
229
  else
230
230
  default
@@ -233,12 +233,12 @@ module ShellOpts
233
233
  )
234
234
  else
235
235
  self.instance_eval %(
236
- def #{opt.attr}?()
237
- (@__option_values__[:#{opt.attr}] || 0) > 0
236
+ def #{opt.attr}?()
237
+ (@__option_values__[:#{opt.attr}] || 0) > 0
238
238
  end
239
239
  )
240
240
  self.instance_eval %(
241
- def #{opt.attr}(default = 0)
241
+ def #{opt.attr}(default = 0)
242
242
  if default > 0 && (@__option_values__[:#{opt.attr}] || 0) == 0
243
243
  default
244
244
  else
@@ -251,7 +251,7 @@ module ShellOpts
251
251
  elsif opt.argument?
252
252
  self.instance_eval %(
253
253
  def #{opt.attr}(default = nil)
254
- if @__option_values__.key?(:#{opt.attr})
254
+ if @__option_values__.key?(:#{opt.attr})
255
255
  @__option_values__[:#{opt.attr}]
256
256
  else
257
257
  default
@@ -261,8 +261,8 @@ module ShellOpts
261
261
 
262
262
  else
263
263
  self.instance_eval %(
264
- def #{opt.attr}()
265
- @__option_values__.key?(:#{opt.attr})
264
+ def #{opt.attr}()
265
+ @__option_values__.key?(:#{opt.attr})
266
266
  end
267
267
  )
268
268
  end
@@ -279,7 +279,7 @@ module ShellOpts
279
279
  @__grammar__.commands.each { |cmd|
280
280
  next if cmd.attr.nil?
281
281
  self.instance_eval %(
282
- def #{cmd.attr}()
282
+ def #{cmd.attr}()
283
283
  :#{cmd.attr} == __subcommand__ ? __subcommand__! : nil
284
284
  end
285
285
  )
@@ -328,29 +328,4 @@ module ShellOpts
328
328
  @__debug__ = false
329
329
  end
330
330
  end
331
-
332
- # Option models an option as given by the user on the subcommand line.
333
- # Compiled options (and possibly aggregated) options are stored in the
334
- # Command#__option_values__ array
335
- class Option
336
- # Associated Grammar::Option object
337
- attr_reader :grammar
338
-
339
- # The actual name used on the shell command-line (String)
340
- attr_reader :name
341
-
342
- # Argument value or nil if not present. The value is a String, Integer,
343
- # or Float depending the on the type of the option
344
- attr_accessor :argument
345
-
346
- forward_to :grammar,
347
- :uid, :ident,
348
- :repeatable?, :argument?, :integer?, :float?,
349
- :file?, :enum?, :string?, :optional?,
350
- :argument_name, :argument_type, :argument_enum
351
-
352
- def initialize(grammar, name, argument)
353
- @grammar, @name, @argument = grammar, name, argument
354
- end
355
- end
356
331
  end
@@ -26,7 +26,7 @@ require 'terminfo'
26
26
  # [cmd1|cmd2] ARG1 ARG2
27
27
  # cmd --all --beta
28
28
  # <commands> ARGS
29
- #
29
+ #
30
30
  module ShellOpts
31
31
  module Grammar
32
32
  class Option
@@ -38,16 +38,23 @@ module ShellOpts
38
38
  #
39
39
  def render(format)
40
40
  constrain format, :enum, :long, :short
41
- s =
41
+ s =
42
42
  case format
43
43
  when :enum; names.join(", ")
44
44
  when :long; name
45
45
  when :short; short_names.first || name
46
46
  else
47
47
  raise ArgumentError, "Illegal format: #{format.inspect}"
48
- end
48
+ end
49
49
  if argument?
50
- s + (optional? ? "[=#{argument_name}]" : "=#{argument_name}")
50
+ short = long_idents.empty? || format == :short
51
+ arg = ""
52
+ arg += "=" if !short
53
+ arg += argument_name
54
+ arg += "..." if list?
55
+ arg = "[#{arg}]" if optional?
56
+ arg = " " + arg if short
57
+ s += arg
51
58
  else
52
59
  s
53
60
  end
@@ -56,7 +63,7 @@ module ShellOpts
56
63
 
57
64
  class OptionGroup
58
65
  # Formats:
59
- #
66
+ #
60
67
  # :enum -a, --all -r, --recursive
61
68
  # :long --all --recursive
62
69
  # :short -a -r
@@ -115,7 +122,7 @@ module ShellOpts
115
122
 
116
123
  # Force one line and compact options to "[OPTIONS]"
117
124
  def render_abbr
118
- args = get_args
125
+ args = get_args
119
126
  ([name] + [options.empty? ? nil : "[OPTIONS]"] + args).compact.join(" ")
120
127
  end
121
128
 
@@ -4,7 +4,7 @@ module ShellOpts
4
4
  # Each kind should have a corresponding Grammar class with the same name
5
5
  KINDS = [
6
6
  :program, :section, :option, :command, :spec, :argument, :usage,
7
- :usage_string, :brief, :text, :blank
7
+ :usage_string, :brief, :text, :blank
8
8
  ]
9
9
 
10
10
  # Kind of token
@@ -27,13 +27,13 @@ module ShellOpts
27
27
 
28
28
  forward_to :source, :to_s, :empty?
29
29
 
30
- def pos(start_lineno = 1, start_charno = 1)
31
- "#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
30
+ def pos(start_lineno = 1, start_charno = 1)
31
+ "#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
32
32
  end
33
33
 
34
34
  def to_s() source end
35
35
 
36
- def inspect()
36
+ def inspect()
37
37
  "<#{self.class.to_s.sub(/.*::/, "")} #{pos} #{kind.inspect} #{source.inspect}>"
38
38
  end
39
39
 
@@ -1,3 +1,3 @@
1
1
  module ShellOpts
2
- VERSION = "2.4.3"
2
+ VERSION = "2.5.0"
3
3
  end
data/lib/shellopts.rb CHANGED
@@ -16,6 +16,7 @@ require_relative 'shellopts/stack.rb'
16
16
  require_relative 'shellopts/token.rb'
17
17
  require_relative 'shellopts/grammar.rb'
18
18
  require_relative 'shellopts/program.rb'
19
+ require_relative 'shellopts/option.rb'
19
20
  require_relative 'shellopts/args.rb'
20
21
  require_relative 'shellopts/lexer.rb'
21
22
  require_relative 'shellopts/argument_type.rb'
@@ -32,7 +33,7 @@ require_relative 'shellopts/dump.rb'
32
33
  # Notes
33
34
  # * Two kinds of exceptions: Expected & unexpected. Expected exceptions are
34
35
  # RuntimeError or IOError. Unexpected exceptions are the rest. Both results
35
- # in shellopts.failure messages if shellopts error handling is enabled
36
+ # in shellopts.failure messages if shellopts error handling is enabled
36
37
  # * Describe the difference between StandardError, RuntimeError, and IOError
37
38
  # * Add an #internal error handling for the production environment that
38
39
  # prints an intelligble error message and prettyfies stack dump. This
@@ -60,7 +61,7 @@ module ShellOpts
60
61
  # <program>: <message>
61
62
  # Usage: <program> ...
62
63
  #
63
- class Error < ShellOptsError; end
64
+ class Error < ShellOptsError; end
64
65
 
65
66
  # Default class for program failures. Failures are raised on missing files or
66
67
  # illegal paths. When ShellOpts handles the exception a message with the
@@ -74,7 +75,7 @@ module ShellOpts
74
75
  # source. Messages are formatted as '<file> <lineno>:<charno> <message>' when
75
76
  # handled by ShellOpts
76
77
  class CompilerError < ShellOptsError; end
77
- class LexerError < CompilerError; end
78
+ class LexerError < CompilerError; end
78
79
  class ParserError < CompilerError; end
79
80
  class AnalyzerError < CompilerError; end
80
81
 
@@ -92,7 +93,7 @@ module ShellOpts
92
93
  attr_reader :spec
93
94
 
94
95
  # Array of arguments. Initialized by #interpret
95
- attr_reader :argv
96
+ attr_reader :argv
96
97
 
97
98
  # Grammar. Grammar::Program object. Initialized by #compile
98
99
  attr_reader :grammar
@@ -139,10 +140,10 @@ module ShellOpts
139
140
  attr_reader :tokens
140
141
  alias_method :ast, :grammar
141
142
 
142
- def initialize(name: nil,
143
+ def initialize(name: nil,
143
144
  # Options
144
- help: true,
145
- version: true,
145
+ help: true,
146
+ version: true,
146
147
  silent: nil,
147
148
  quiet: nil,
148
149
  verbose: nil,
@@ -157,7 +158,7 @@ module ShellOpts
157
158
  # Let exceptions through
158
159
  exception: false
159
160
  )
160
-
161
+
161
162
  @name = name || File.basename($PROGRAM_NAME)
162
163
  @help = help
163
164
  @version = version || (version.nil? && !version_number.nil?)
@@ -187,24 +188,24 @@ module ShellOpts
187
188
  verbose_spec = (@verbose == true ? "+v,verbose" : @verbose)
188
189
  debug_spec = (@debug == true ? "--debug" : @debug)
189
190
 
190
- @silent_option =
191
+ @silent_option =
191
192
  ast.inject_option(silent_spec, "Quiet", "Do not write anything to standard output") if @silent
192
- @quiet_option =
193
+ @quiet_option =
193
194
  ast.inject_option(quiet_spec, "Quiet", "Do not write anything to standard output") if @quiet
194
- @verbose_option =
195
+ @verbose_option =
195
196
  ast.inject_option(verbose_spec, "Increase verbosity", "Write verbose output") if @verbose
196
- @debug_option =
197
+ @debug_option =
197
198
  ast.inject_option(debug_spec, "Write debug information") if @debug
198
- @help_option =
199
+ @help_option =
199
200
  ast.inject_option(help_spec, "Write short or long help") { |option|
200
- short_option = option.short_names.first
201
+ short_option = option.short_names.first
201
202
  long_option = option.long_names.first
202
203
  [
203
204
  short_option && "#{short_option} prints a brief help text",
204
205
  long_option && "#{long_option} prints a longer man-style description of the command"
205
206
  ].compact.join(", ")
206
207
  } if @help
207
- @version_option =
208
+ @version_option =
208
209
  ast.inject_option(version_spec, "Write version number and exit") if @version
209
210
 
210
211
  @grammar = Analyzer.analyze(ast)
@@ -216,7 +217,7 @@ module ShellOpts
216
217
  # ShellOpts::Args tuple
217
218
  #
218
219
  def interpret(argv)
219
- handle_exceptions {
220
+ handle_exceptions {
220
221
  @argv = argv.dup
221
222
  @program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
222
223
 
@@ -356,7 +357,7 @@ module ShellOpts
356
357
  char_z = 0
357
358
 
358
359
  (0 ... text_lines.size).each { |text_i|
359
- curr_char_i, curr_char_z =
360
+ curr_char_i, curr_char_z =
360
361
  LCS.find_longest_common_substring_index(text_lines[text_i], spec_lines.first.strip)
361
362
  if curr_char_z > char_z
362
363
  line_i = text_i
@@ -372,7 +373,7 @@ module ShellOpts
372
373
  compare_lines(text_lines[text_i + spec_i], spec_lines[spec_i])
373
374
  }
374
375
  } or return [nil, nil]
375
- char_i, char_z =
376
+ char_i, char_z =
376
377
  LCS.find_longest_common_substring_index(text_lines[line_i], spec_lines.first.strip)
377
378
  [line_i, char_i || 0]
378
379
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shellopts
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.3
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-23 00:00:00.000000000 Z
11
+ date: 2024-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: forward_to
@@ -147,6 +147,7 @@ files:
147
147
  - lib/shellopts/grammar.rb
148
148
  - lib/shellopts/interpreter.rb
149
149
  - lib/shellopts/lexer.rb
150
+ - lib/shellopts/option.rb
150
151
  - lib/shellopts/parser.rb
151
152
  - lib/shellopts/program.rb
152
153
  - lib/shellopts/renderer.rb