shellopts 2.4.3 → 2.5.0

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