shellopts 2.0.9 → 2.0.12

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: a9d576d24bd0aec0dd43a2a3645ede04751d80ae22a4c88e8f66ed53bbe7e405
4
- data.tar.gz: 9027ac55689a345099f6515e5280808528603dfafe385219e6cce77d1c166d96
3
+ metadata.gz: b2f378c7e7c04297221bfae81065ef06699e93b2dc4fda892b210ef4702c5836
4
+ data.tar.gz: f5d83b09b371aebd7cc07e73905edffb2f9f834ba5e382894277310e984e7ecc
5
5
  SHA512:
6
- metadata.gz: b44a4d95aa0585dd0ec31abccde4942c98ad712d9e3bdf9f0c0b6ead7333ff4c5b555ec5b45786a6f539388b2460c93d12f3755b2e80cbb81fd269c42a9b4955
7
- data.tar.gz: 3c948a743d809df7d22bc142dcfa9aa4e943fed144004c4fbf5d1995c314f4aea3f91b032533222eed953e5344a45c55666339797e5ae2b1be262eea17071cd5
6
+ metadata.gz: 6255a64a99ceefe0dc0a7a3195976d42b2db6b3a28d779cbeefbc9f36380e46a762939e2062ff9f32f4f78bd5bff8122606b5102d9d8473e86c0a742cd7db479
7
+ data.tar.gz: b54f74a5d8fd251d1492667bd8e1340c1f17a3e96936f423efd61e82fe665294936c1edd1ac9ac4ab9df118450ccefafcda117f84ef4e0726d78159196e075cf
data/TODO CHANGED
@@ -1,18 +1,13 @@
1
1
 
2
- o Add brackets to optional option arguments: '--all=FILE?' -> '--all[=FILE]'
3
2
  o Ignore all text after ' # ' (doesn't conflict with option flag)
4
3
  o Command aliases
5
4
  o Add user-defined setions
6
5
  o Add a SOURCE section with link to git repo
7
6
  o Bullet-lists
8
7
  o Allow a USAGE section (and NAME)
9
- o Find source in code an adjust line number in error messages
10
- o Rename line and char to lineno and charno
11
8
  o Client-defined argument types
12
9
  o Rename Expr -> ?
13
10
  o Find clean(er) procedural object model
14
- o Allow assignment to options (this makes practical stuff easier)
15
- o Special handling of --help arguments so that '--help command' is possible
16
11
  o Support for paging of help:
17
12
  begin
18
13
  file = Tempfile.new("prick")
@@ -23,6 +18,11 @@ o Support for paging of help:
23
18
  file.close
24
19
  end
25
20
 
21
+ + Special handling of --help arguments so that '--help command' is possible
22
+ + Allow assignment to options (this makes practical stuff easier)
23
+ + Rename line and char to lineno and charno
24
+ + Find source in code an adjust line number in error messages
25
+ + Add brackets to optional option arguments: '--all=FILE?' -> '--all[=FILE]'
26
26
  + Bold text output
27
27
  + Recursive format of commands
28
28
  + Rename Compiler -> Interpreter
data/lib/ext/array.rb CHANGED
@@ -41,7 +41,9 @@ module Ext
41
41
 
42
42
  module Wrap
43
43
  refine ::Array do
44
- # Concatenate strings into lines that are at most +width+ characters wide
44
+ # Concatenate array of words into lines that are at most +width+
45
+ # characters wide. +curr+ is the initial number of characters already
46
+ # used on the first line
45
47
  def wrap(width, curr = 0)
46
48
  lines = [[]]
47
49
  curr -= 1 # Simplifies conditions below
@@ -20,10 +20,6 @@ module ShellOpts
20
20
  end
21
21
 
22
22
  class Command
23
- def set_supercommand
24
- commands.each { |child| child.instance_variable_set(:@supercommand, self) }
25
- end
26
-
27
23
  def collect_options
28
24
  @options = option_groups.map(&:options).flatten
29
25
  end
@@ -54,9 +50,9 @@ module ShellOpts
54
50
  }
55
51
  end
56
52
 
53
+ # TODO Check for dash-collision
57
54
  def compute_command_hashes
58
55
  commands.each { |command|
59
- # TODO Check for dash-collision
60
56
  !@commands_hash.key?(command.name) or
61
57
  analyzer_error command.token, "Duplicate command name: #{command.name}"
62
58
  @commands_hash[command.name] = command
@@ -76,41 +72,74 @@ module ShellOpts
76
72
  @grammar = grammar
77
73
  end
78
74
 
79
- # Move commands that are nested within a different command than it belongs to
80
- def move_commands
75
+ def create_implicit_commands(cmd)
76
+ path = cmd.path[0..-2]
77
+
78
+
79
+ end
80
+
81
+ # Link up commands with supercommands. This is only done for commands that
82
+ # are nested within a different command than it belongs to. The
83
+ # parent/child relationship is not changed Example:
84
+ #
85
+ # cmd!
86
+ # cmd.subcmd!
87
+ #
88
+ # Here subcmd is added to cmd's list of commands. It keeps its position in
89
+ # the program's parent/child relationship so that documentation will print the
90
+ # commands in the given order and with the given indentation level
91
+ #
92
+ def link_commands
81
93
  # We can't use Command#[] at this point so we collect the commands here
82
94
  h = {}
83
95
  @grammar.traverse(Grammar::Command) { |command|
84
96
  h[command.path] = command
97
+ # TODO: Pick up parent-less commands
98
+ }
99
+
100
+ # Command to link
101
+ link = []
102
+
103
+ # Create implicit commands
104
+ h.sort { |l,r| l.size <=> r.size }.each { |path, command|
105
+ path = path[0..-2]
106
+ while !h.key?(path)
107
+ cmd = Grammar::Command.new(nil, command.token)
108
+ cmd.set_name(path.last.to_s.sub(/!/, ""), path.dup)
109
+ link << cmd
110
+ h[cmd.path] = cmd
111
+ path.pop
112
+ end
85
113
  }
86
114
 
87
- # Find commands to move
115
+ # Find commands to link
88
116
  #
89
- # Commands are moved in two steps because the behaviour of #traverse is
90
- # not defined when the data structure changes beneath it
91
- move = []
117
+ # Commands are linked in two steps because the behaviour of #traverse is
118
+ # not defined when the data structure changes beneath it. (FIXME: Does it
119
+ # change when we don't touch the parent/child relationship?)
92
120
  @grammar.traverse(Grammar::Command) { |command|
93
121
  if command.path.size > 1 && command.parent && command.parent.path != command.path[0..-2]
94
- move << command
122
+ # if command.path.size > 1 && command.parent.path != command.path[0..-2]
123
+ link << command
95
124
  else
96
125
  command.instance_variable_set(:@command, command.parent)
97
126
  end
98
127
  }
99
128
 
100
- # Move commands but do not change parent/child relationship
101
- move.each { |command|
102
- supercommand = h[command.path[0..-2]] or analyzer_error "Can't find #{command.ident}!"
103
- command.parent.commands.delete(command)
129
+ # Link commands but do not change parent/child relationship
130
+ link.each { |command|
131
+ path = command.path[0..-2]
132
+ path.pop while (supercommand = h[path]).nil?
133
+ command.parent.commands.delete(command) if command.parent
104
134
  supercommand.commands << command
105
135
  command.instance_variable_set(:@command, supercommand)
106
136
  }
107
137
  end
108
138
 
109
139
  def analyze()
110
- move_commands
140
+ link_commands
111
141
 
112
142
  @grammar.traverse(Grammar::Command) { |command|
113
- command.set_supercommand
114
143
  command.reorder_options
115
144
  command.collect_options
116
145
  command.compute_option_hashes
@@ -1,12 +1,5 @@
1
1
  require 'terminfo'
2
2
 
3
- # TODO: Move to ext/indented_io.rb
4
- module IndentedIO
5
- class IndentedIO
6
- def margin() combined_indent.size end
7
- end
8
- end
9
-
10
3
  module ShellOpts
11
4
  module Grammar
12
5
  class Node
@@ -81,7 +74,15 @@ module ShellOpts
81
74
  end
82
75
 
83
76
  def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
84
- puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
77
+ # Use one-line mode if all options are declared on one line
78
+ if options.all? { |option| option.token.lineno == token.lineno }
79
+ puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
80
+ puts_options = false
81
+ else
82
+ puts Ansi.bold([prefix, render(:abbr, Formatter.rest)].flatten.compact.join(" "))
83
+ puts_options = true
84
+ end
85
+
85
86
  indent {
86
87
  if brief
87
88
  puts self.brief.words.wrap(Formatter.rest)
@@ -93,7 +94,10 @@ module ShellOpts
93
94
 
94
95
  if child.is_a?(Command)
95
96
  child.puts_descr(prefix, name: :path)
96
- else
97
+ elsif child.is_a?(OptionGroup)
98
+ child.puts_descr if puts_options
99
+ newline = false
100
+ else
97
101
  child.puts_descr
98
102
  end
99
103
  }
@@ -125,9 +129,9 @@ module ShellOpts
125
129
  section.delete(klass)
126
130
  section.delete(Paragraph)
127
131
  if klass <= OptionGroup
128
- s = s + "S" if options.size > 1
132
+ s += "S" if options.size > 1
129
133
  elsif klass <= Command
130
- s = s + "S" if commands.size > 1 || commands.first&.commands&.size != 0
134
+ s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
131
135
  end
132
136
  puts
133
137
  indent(-1).puts Ansi.bold s
@@ -139,7 +143,7 @@ module ShellOpts
139
143
  end
140
144
 
141
145
  if child.is_a?(Command)
142
- prefix = child.supercommand == self ? nil : child.supercommand&.name
146
+ prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
143
147
  child.puts_descr(prefix, brief: false, name: :path)
144
148
  newline = true
145
149
  else
@@ -150,9 +154,10 @@ module ShellOpts
150
154
 
151
155
  # Also emit commands not declared in nested scope
152
156
  (commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
157
+ next if cmd.parent.nil? # Skip implicit commands
153
158
  puts if newline
154
159
  newline = true
155
- prefix = cmd.supercommand == self ? nil : cmd.supercommand&.name
160
+ prefix = cmd.command == self ? nil : cmd.command&.name
156
161
  cmd.puts_descr(prefix, brief: false, name: path)
157
162
  }
158
163
  }
@@ -204,7 +209,10 @@ module ShellOpts
204
209
  BRIEF_COL1_MAX_WIDTH = 40
205
210
 
206
211
  # Minimum width of second column in brief option and command lists
207
- BRIEF_COL2_MAX_WIDTH = 50
212
+ BRIEF_COL2_MIN_WIDTH = 30
213
+
214
+ # Maximum width of second column in brief option and command lists
215
+ BRIEF_COL2_MAX_WIDTH = 70
208
216
 
209
217
  # Indent to use in help output
210
218
  HELP_INDENT = 4
@@ -214,29 +222,21 @@ module ShellOpts
214
222
 
215
223
  # Usage string in error messages
216
224
  def self.usage(subject)
217
- subject = Grammar::Command.command(subject)
218
- @command_prefix = subject.ancestors.map { |node| node.name + " " }.join
225
+ command = Grammar::Command.command(subject)
226
+ @command_prefix = command.ancestors.map { |node| node.name + " " }.join
219
227
  setup_indent(1) {
220
228
  print lead = "#{USAGE_STRING}: "
221
- indent(lead.size, ' ', bol: false) { subject.puts_usage }
229
+ indent(lead.size, ' ', bol: false) { command.puts_usage }
222
230
  }
223
231
  end
224
232
 
225
- # # TODO
226
- # def self.usage=(usage_lambda)
227
- # end
228
-
229
233
  # When the user gives a -h option
230
- def self.brief(command)
231
- command = Grammar::Command.command(command)
234
+ def self.brief(subject)
235
+ command = Grammar::Command.command(subject)
232
236
  @command_prefix = command.ancestors.map { |node| node.name + " " }.join
233
237
  setup_indent(BRIEF_INDENT) { command.puts_brief }
234
238
  end
235
239
 
236
- # # TODO
237
- # def self.brief=(brief_lambda)
238
- # end
239
-
240
240
  # When the user gives a --help option
241
241
  def self.help(subject)
242
242
  subject = Grammar::Command.command(subject)
@@ -250,18 +250,6 @@ module ShellOpts
250
250
  obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
251
251
  end
252
252
 
253
- # # TODO
254
- # def self.help_w_lambda(program)
255
- # if @help_lambda
256
- # #
257
- # else
258
- # program = Grammar::Command.command(program)
259
- # setup_indent(HELP_INDENT) { program.puts_descr }
260
- # end
261
- # end
262
- #
263
- # def self.help=(help_lambda) @help_lambda end
264
-
265
253
  def self.puts_columns(widths, fields)
266
254
  l = []
267
255
  first_width, second_width = *widths
@@ -272,40 +260,47 @@ module ShellOpts
272
260
  puts first
273
261
  indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
274
262
  elsif second
275
- printf "%-#{first_width + BRIEF_COL_SEP}s", first
276
- indent(first_width, bol: false) { puts second.wrap(second_width) }
263
+ indent_size = first_width + BRIEF_COL_SEP
264
+ printf "%-#{indent_size}s", first
265
+ indent(indent_size, ' ', bol: false) { puts second.wrap(second_width) }
277
266
  else
278
267
  puts first
279
268
  end
280
269
  end
281
270
  end
282
271
 
272
+ # Returns a tuple of [first-column-width, second-column-width]. +width+ is
273
+ # the maximum width of the colunms and the BRIEF_COL_SEP separator.
274
+ # +fields+ is an array of [subject-string, descr-text] tuples where the
275
+ # descr is an array of words
283
276
  def self.compute_columns(width, fields)
284
- first_max = [
285
- (fields.map { |first, _| first.size } + [BRIEF_COL1_MIN_WIDTH]).max,
286
- BRIEF_COL1_MAX_WIDTH
287
- ].min
288
- second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1: 0 }.max
289
-
290
- if first_max + BRIEF_COL_SEP + second_max <= width
291
- first_width = first_max
292
- second_width = second_max
293
- elsif first_max + BRIEF_COL_SEP + BRIEF_COL2_MAX_WIDTH <= width
294
- first_width = first_max
295
- second_width = width - first_width - BRIEF_COL_SEP
277
+ first_max =
278
+ fields.map { |first, _| first.size }.select { |size| size <= BRIEF_COL1_MAX_WIDTH }.max ||
279
+ BRIEF_COL1_MIN_WIDTH
280
+ second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1 : 0 }.max
281
+ first_width = [[first_max, BRIEF_COL1_MIN_WIDTH].max, BRIEF_COL1_MAX_WIDTH].min
282
+ rest = width - first_width - BRIEF_COL_SEP
283
+ second_min = [BRIEF_COL2_MIN_WIDTH, second_max].min
284
+ if rest < second_min
285
+ first_width = [first_max, width - second_min - BRIEF_COL_SEP].max
286
+ second_width = [width - first_width - BRIEF_COL_SEP, BRIEF_COL2_MIN_WIDTH].max
296
287
  else
297
- first_width = [width - BRIEF_COL_SEP - BRIEF_COL2_MAX_WIDTH, BRIEF_COL1_MAX_WIDTH].min
298
- second_width = BRIEF_COL2_MAX_WIDTH
288
+ second_width = [[rest, BRIEF_COL2_MIN_WIDTH].max, BRIEF_COL2_MAX_WIDTH].min
299
289
  end
300
-
301
290
  [first_width, second_width]
302
291
  end
303
292
 
304
293
  def self.width()
305
294
  @width ||= TermInfo.screen_width - MARGIN_RIGHT
295
+ @width
296
+ end
297
+
298
+ # Used in rspec
299
+ def self.width=(width)
300
+ @width = width
306
301
  end
307
302
 
308
- def self.rest() width - $stdout.margin end
303
+ def self.rest() width - $stdout.tab end
309
304
 
310
305
  private
311
306
  # TODO Get rid of?
@@ -45,16 +45,18 @@ module ShellOpts
45
45
  end
46
46
 
47
47
  class IdrNode < Node
48
- # Command of this object. This is different from #parent when a
49
- # subcommand is nested on a higher level than its supercommand.
50
- # Initialized by the analyzer
48
+ # Command of this object (nil for the top-level Program object). This is
49
+ # different from #parent when a subcommand is nested textually on a
50
+ # higher level than its supercommand. Initialized by the analyzer
51
51
  attr_reader :command
52
52
 
53
53
  # Unique identifier of node (String) within the context of a program. nil
54
- # for the Program object. It is the list of path elements concatenated
55
- # with '.' and with internal '!' removed (eg. "cmd.opt" or "cmd.cmd!").
56
- # Initialized by the parser
57
- attr_reader :uid
54
+ # for the Program object. It is the dot-joined elements of path with
55
+ # internal exclamation marks removed (eg. "cmd.opt" or "cmd.cmd!").
56
+ # Initialize by the analyzer
57
+ def uid()
58
+ @uid ||= command && [command.uid, ident].compact.join(".").sub(/!\./, ".")
59
+ end
58
60
 
59
61
  # Path from Program object and down to this node. Array of identifiers.
60
62
  # Empty for the Program object. Initialized by the parser
@@ -84,6 +86,13 @@ module ShellOpts
84
86
  # #ident is a reserved word. Initialized by the parser
85
87
  attr_reader :attr
86
88
 
89
+ def set_name(name, path)
90
+ @name = name.to_s
91
+ @path = path
92
+ @ident = @path.last || :!
93
+ @attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(@ident.to_s) ? nil : @ident
94
+ end
95
+
87
96
  protected
88
97
  def lookup(path)
89
98
  path.empty? or raise ArgumentError, "Argument should be empty"
@@ -189,10 +198,6 @@ module ShellOpts
189
198
  # methods are initialized by the analyzer
190
199
  #
191
200
  class Command < IdrNode
192
- # Supercommand or nil if this is the top-level Program object.
193
- # Initialized by the analyzer
194
- attr_reader :supercommand
195
-
196
201
  # Brief description of command
197
202
  attr_accessor :brief
198
203
 
@@ -16,9 +16,9 @@ module ShellOpts
16
16
  class IdrNode
17
17
  # Assumes that @name and @path has been defined
18
18
  def parse
19
- @ident = @path.last || :!
20
- @attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(ident.to_s) ? nil : ident
21
- @uid = parent && @path.join(".").sub(/!\./, ".") # uid is nil for the Program object
19
+ # @ident = @path.last || :!
20
+ # @attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(ident.to_s) ? nil : ident
21
+ # @uid = parent && @path.join(".").sub(/!\./, ".") # uid is nil for the Program object
22
22
  end
23
23
  end
24
24
 
@@ -56,8 +56,9 @@ module ShellOpts
56
56
  @long_names = names.map { |name| "--#{name}" }
57
57
  @long_idents = names.map { |name| name.tr("-", "_").to_sym }
58
58
 
59
- @name = @long_names.first || @short_names.first
60
- @path = command.path + [@long_idents.first || @short_idents.first]
59
+ set_name(
60
+ @long_names.first || @short_names.first,
61
+ command.path + [@long_idents.first || @short_idents.first])
61
62
 
62
63
  @argument = !arg.nil?
63
64
 
@@ -108,11 +109,11 @@ module ShellOpts
108
109
  def parse
109
110
  if parent
110
111
  path_names = token.source.sub("!", "").split(".")
111
- @name = path_names.last
112
- @path = path_names.map { |cmd| "#{cmd}!".to_sym }
112
+ set_name(
113
+ path_names.last,
114
+ path_names.map { |cmd| "#{cmd}!".to_sym })
113
115
  else
114
- @path = []
115
- @name = token.source
116
+ set_name(token.source, [])
116
117
  end
117
118
  super
118
119
  end
@@ -123,13 +124,15 @@ module ShellOpts
123
124
  super(nil, token)
124
125
  end
125
126
 
126
- def add_stdopts
127
+ def add_version_option
127
128
  option_token = Token.new(:option, 1, 1, "--version")
128
129
  brief_token = Token.new(:brief, 1, 1, "Write version number and exit")
129
130
  group = OptionGroup.new(self, option_token)
130
131
  option = Option.parse(group, option_token)
131
132
  brief = Brief.parse(group, brief_token)
133
+ end
132
134
 
135
+ def add_help_options
133
136
  option_token = Token.new(:option, 1, 1, "-h,help")
134
137
  brief_token = Token.new(:brief, 1, 1, "Write help text and exit")
135
138
  paragraph_token = Token.new(:text, 1, 1,
@@ -49,10 +49,10 @@ module ShellOpts
49
49
  singleton_method_removed singleton_method_undefined
50
50
  )
51
51
 
52
- # These methods can be overridden by an option (the value is not used -
52
+ # These methods can be overridden by an option or a command (the value is not used -
53
53
  # this is just for informational purposes)
54
- OVERRIDEABLE_METHODS = %w(
55
- subcommand
54
+ OVERRIDEABLE_METHOD_NAMES = %w(
55
+ subcommand subcommand! supercommand!
56
56
  )
57
57
 
58
58
  # Redefine ::new to call #__initialize__
@@ -107,6 +107,7 @@ module ShellOpts
107
107
  #
108
108
  # Note: Can be overridden by option, in that case use #__subcommand__ or
109
109
  # ShellOpts.subcommand(object) instead
110
+ #
110
111
  def subcommand() __subcommand__ end
111
112
 
112
113
  # The subcommand object or nil if not present. Per-subcommand methods
@@ -120,7 +121,12 @@ module ShellOpts
120
121
  def subcommand!() __subcommand__! end
121
122
 
122
123
  # The parent command or nil. Initialized by #add_command
123
- attr_accessor :__supercommand__
124
+ #
125
+ # Note: Can be overridden by a subcommand declaration (but not an
126
+ # option), in that case use #__supercommand__! or
127
+ # ShellOpts.supercommand!(object) instead
128
+ #
129
+ def supercommand!() __supercommand__ end
124
130
 
125
131
  # UID of command/program
126
132
  def __uid__() @__grammar__.uid end
@@ -149,6 +155,9 @@ module ShellOpts
149
155
  # Map from identifier to option object or to a list of option objects if
150
156
  # the option is repeatable
151
157
  attr_reader :__option_hash__
158
+
159
+ # The parent command or nil. Initialized by #add_command
160
+ attr_accessor :__supercommand__
152
161
 
153
162
  # The subcommand identifier (a Symbol incl. the exclamation mark) or nil
154
163
  # if not present. Use #subcommand!, or the dynamically generated
@@ -38,13 +38,19 @@ module ShellOpts
38
38
  #
39
39
  def render(format)
40
40
  constrain format, :enum, :long, :short
41
- case format
42
- when :enum; names.join(", ")
43
- when :long; name
44
- when :short; short_names.first || name
41
+ s =
42
+ case format
43
+ when :enum; names.join(", ")
44
+ when :long; name
45
+ when :short; short_names.first || name
46
+ else
47
+ raise ArgumentError, "Illegal format: #{format.inspect}"
48
+ end
49
+ if argument?
50
+ s + (optional? ? "[=#{argument_name}]" : "=#{argument_name}")
45
51
  else
46
- raise ArgumentError, "Illegal format: #{format.inspect}"
47
- end + (argument? ? "=#{argument_name}" : "")
52
+ s
53
+ end
48
54
  end
49
55
  end
50
56
 
@@ -75,17 +81,19 @@ module ShellOpts
75
81
  COMMANDS_ABBR = "[COMMANDS]"
76
82
  DESCRS_ABBR = "ARGS..."
77
83
 
78
- # Format can be one of :single, :enum, or :multi. :single force one-line
79
- # output and compacts options and commands if needed. :enum outputs a
80
- # :single line for each argument specification/description, :multi tries
81
- # one-line output but wrap options if needed. Multiple argument
82
- # specifications/descriptions are always compacted
84
+ # Format can be one of :abbr, :single, :enum, or :multi. :abbr
85
+ # lists the command on one line with options abbreviated. :single force
86
+ # one-line output and compacts options and commands if needed. :enum
87
+ # outputs a :single line for each argument specification/description,
88
+ # :multi tries one-line output but wrap options if needed. Multiple
89
+ # argument specifications/descriptions are always compacted
83
90
  #
84
91
  def render(format, width, root: false, **opts)
85
92
  case format
93
+ when :abbr; render_abbr
86
94
  when :single; render_single(width, **opts)
87
95
  when :enum; render_enum(width, **opts)
88
- when :multi; render_multi2(width, **opts)
96
+ when :multi; render_multi(width, **opts)
89
97
  else
90
98
  raise ArgumentError, "Illegal format: #{format.inspect}"
91
99
  end
@@ -96,6 +104,21 @@ module ShellOpts
96
104
  end
97
105
 
98
106
  protected
107
+ # TODO: Refactor and implement recursive detection of any argument
108
+ def get_args(args: nil)
109
+ case descrs.size
110
+ when 0; []
111
+ when 1; [descrs.first.text]
112
+ else [DESCRS_ABBR]
113
+ end
114
+ end
115
+
116
+ # Force one line and compact options to "[OPTIONS]"
117
+ def render_abbr
118
+ args = get_args
119
+ ([name] + [options.empty? ? nil : "[OPTIONS]"] + args).compact.join(" ")
120
+ end
121
+
99
122
  # Force one line. Compact options, commands, arguments if needed
100
123
  def render_single(width, args: nil)
101
124
  long_options = options.map { |option| option.render(:long) }
@@ -104,13 +127,7 @@ module ShellOpts
104
127
  short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
105
128
  compact_commands = commands.empty? ? [] : [COMMANDS_ABBR]
106
129
 
107
- # TODO: Refactor and implement recursive detection of any argument
108
- args ||=
109
- case descrs.size
110
- when 0; args = []
111
- when 1; [descrs.first.text]
112
- else [DESCRS_ABBR]
113
- end
130
+ args ||= get_args
114
131
 
115
132
  begin # to be able to use 'break' below
116
133
  words = [name] + long_options + short_commands + args
@@ -149,36 +166,8 @@ module ShellOpts
149
166
  short_options = options.map { |option| option.render(:short) }
150
167
  short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
151
168
  compact_commands = [COMMANDS_ABBR]
152
- args ||= self.descrs.size != 1 ? [DESCRS_ABBR] : descrs.map(&:text)
153
-
154
- # On one line
155
- words = long_options + short_commands + args
156
- return [words.join(" ")] if pass?(words, width)
157
- words = short_options + short_commands + args
158
- return [words.join(" ")] if pass?(words, width)
159
-
160
- # On multiple lines
161
- options = long_options.wrap(width)
162
- commands = [[short_commands, args].join(" ")]
163
- return options + commands if pass?(commands, width)
164
- options + [[compact_commands, args].join(" ")]
165
- end
166
-
167
- # Try to keep on one line but wrap options if needed. Multiple argument
168
- # specifications/descriptions are always compacted
169
- def render_multi2(width, args: nil)
170
- long_options = options.map { |option| option.render(:long) }
171
- short_options = options.map { |option| option.render(:short) }
172
- short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
173
- compact_commands = [COMMANDS_ABBR]
174
169
 
175
- # TODO: Refactor and implement recursive detection of any argument
176
- args ||=
177
- case descrs.size
178
- when 0; args = []
179
- when 1; [descrs.first.text]
180
- else [DESCRS_ABBR]
181
- end
170
+ args ||= get_args
182
171
 
183
172
  # On one line
184
173
  words = [name] + long_options + short_commands + args
@@ -1,3 +1,3 @@
1
1
  module ShellOpts
2
- VERSION = "2.0.9"
2
+ VERSION = "2.0.12"
3
3
  end
data/lib/shellopts.rb CHANGED
@@ -1,12 +1,6 @@
1
1
 
2
- $quiet = nil
3
- $verb = nil
4
- $debug = nil
5
- $shellopts = nil
6
-
7
2
  require 'indented_io'
8
3
 
9
- #$LOAD_PATH.unshift "../constrain/lib"
10
4
  require 'constrain'
11
5
  include Constrain
12
6
 
@@ -98,14 +92,22 @@ module ShellOpts
98
92
  # Array of remaining arguments. Initialized by #interpret
99
93
  attr_reader :args
100
94
 
101
- # Compiler flags
102
- attr_accessor :stdopts
103
- attr_accessor :msgopts
95
+ # Automatically add a -h and a --help option if true
96
+ attr_reader :help
97
+
98
+ # Version of client program. If not nil a --version option is added to the program
99
+ def version
100
+ return @version if @version
101
+ exe = caller.find { |line| line =~ /`<top \(required\)>'$/ }&.sub(/:.*/, "")
102
+ file = Dir.glob(File.dirname(exe) + "/../lib/*/version.rb").first
103
+ @version = IO.read(file).sub(/^.*VERSION\s*=\s*"(.*?)".*$/m, '\1') or
104
+ raise ArgumentError, "ShellOpts needs an explicit version"
105
+ end
104
106
 
105
- # Version of client program. This is only used if +stdopts+ is true
106
- attr_reader :version
107
+ # Add message options (TODO)
108
+ attr_accessor :msgopts
107
109
 
108
- # Interpreter flags
110
+ # Floating options
109
111
  attr_accessor :float
110
112
 
111
113
  # True if ShellOpts lets exceptions through instead of writing an error
@@ -117,11 +119,14 @@ module ShellOpts
117
119
 
118
120
  # Debug: Internal variables made public
119
121
  attr_reader :tokens
120
- alias_method :ast, :grammar # Oops - defined earlier FIXME
122
+ alias_method :ast, :grammar
121
123
 
122
- def initialize(name: nil, stdopts: true, version: nil, msgopts: false, float: true, exception: false)
124
+ def initialize(name: nil, help: true, version: true, msgopts: false, float: true, exception: false)
123
125
  @name = name || File.basename($PROGRAM_NAME)
124
- @stdopts, @version, @msgopts, @float, @exception = stdopts, version, msgopts, float, exception
126
+ @help = help
127
+ @use_version = version ? true : false
128
+ @version = @use_version && @version != true ? @version : nil
129
+ @msgopts, @float, @exception = msgopts, float, exception
125
130
  end
126
131
 
127
132
  # Compile source and return grammar object. Also sets #spec and #grammar.
@@ -133,7 +138,8 @@ module ShellOpts
133
138
  @file = find_caller_file
134
139
  @tokens = Lexer.lex(name, @spec, @oneline)
135
140
  ast = Parser.parse(tokens)
136
- ast.add_stdopts if stdopts
141
+ ast.add_version_option if @use_version
142
+ ast.add_help_options if @help
137
143
  @grammar = Analyzer.analyze(ast)
138
144
  }
139
145
  self
@@ -146,19 +152,16 @@ module ShellOpts
146
152
  handle_exceptions {
147
153
  @argv = argv.dup
148
154
  @program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
149
- if stdopts
150
- if @program.version?
151
- version or raise ArgumentError, "Version not specified"
152
- puts version
153
- exit
154
- elsif @program.help?
155
- if @program[:help].name == "-h"
156
- ShellOpts.brief
157
- else
158
- ShellOpts.help
159
- end
160
- exit
155
+ if @program.version?
156
+ puts version
157
+ exit
158
+ elsif @program.help?
159
+ if @program[:help].name == "-h"
160
+ ShellOpts.brief
161
+ else
162
+ ShellOpts.help
161
163
  end
164
+ exit
162
165
  end
163
166
  }
164
167
  self
@@ -189,23 +192,23 @@ module ShellOpts
189
192
  #
190
193
  # #error is supposed to be used when the user made an error and the usage
191
194
  # is written to help correcting the error
192
- #
193
195
  def error(subject = nil, message)
194
- saved = $stdout
195
- $stdout = $stderr
196
196
  $stderr.puts "#{name}: #{message}"
197
- Formatter.usage(grammar)
198
- exit 1
199
- ensure
200
- $stdout = saved
197
+ saved = $stdout
198
+ begin
199
+ $stdout = $stderr
200
+ Formatter.usage(grammar)
201
+ exit 1
202
+ ensure
203
+ $stdout = saved
204
+ end
201
205
  end
202
206
 
203
207
  # Write error message to standard error and terminate program with status 1
204
208
  #
205
- # #failure is supposed to be used the used specified the correct arguments
206
- # but something went wrong during processing. Since the used didn't cause
207
- # the problem, only the error message is written
208
- #
209
+ # #failure doesn't print the program usage because is supposed to be used
210
+ # when the user specified the correct arguments but something else went
211
+ # wrong during processing
209
212
  def failure(message)
210
213
  $stderr.puts "#{name}: #{message}"
211
214
  exit 1
@@ -219,7 +222,6 @@ module ShellOpts
219
222
 
220
223
  # Print help for the given subject or the full documentation if +subject+
221
224
  # is nil. Clears the screen beforehand if :clear is true
222
- #
223
225
  def help(subject = nil, clear: true)
224
226
  node = (subject ? @grammar[subject] : @grammar) or
225
227
  raise ArgumentError, "No such command: '#{subject&.sub(".", " ")}'"
@@ -330,7 +332,17 @@ module ShellOpts
330
332
  def self.instance=(instance) @instance = instance end
331
333
  def self.shellopts() instance end
332
334
 
333
- forward_self_to :instance, :error, :failure
335
+ def self.error(subject = nil, message)
336
+ instance.error(subject, message) if instance? # Never returns
337
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{message}"
338
+ exit 1
339
+ end
340
+
341
+ def self.failure(message)
342
+ instance.failure(message) if instance?
343
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{message}"
344
+ exit 1
345
+ end
334
346
 
335
347
  # The Include module brings the reporting methods into the namespace when
336
348
  # included
@@ -364,132 +376,3 @@ module ShellOpts
364
376
  end
365
377
  end
366
378
 
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
- __END__
375
-
376
- require "shellopts/version"
377
-
378
- require "ext/algorithm.rb"
379
- require "ext/ruby_env.rb"
380
-
381
- require "shellopts/constants.rb"
382
- require "shellopts/exceptions.rb"
383
-
384
- require "shellopts/grammar/analyzer.rb"
385
- require "shellopts/grammar/lexer.rb"
386
- require "shellopts/grammar/parser.rb"
387
- require "shellopts/grammar/command.rb"
388
- require "shellopts/grammar/option.rb"
389
-
390
- require "shellopts/ast/parser.rb"
391
- require "shellopts/ast/command.rb"
392
- require "shellopts/ast/option.rb"
393
-
394
- require "shellopts/args.rb"
395
- require "shellopts/formatter.rb"
396
-
397
- if RUBY_ENV == "development"
398
- require "shellopts/grammar/dump.rb"
399
- require "shellopts/ast/dump.rb"
400
- end
401
-
402
- $verb = nil
403
- $quiet = nil
404
- $shellopts = nil
405
-
406
- module ShellOpts
407
- class ShellOpts
408
- attr_reader :name # Name of program. Defaults to the name of the executable
409
- attr_reader :spec
410
- attr_reader :argv
411
-
412
- attr_reader :grammar
413
- attr_reader :program
414
- attr_reader :arguments
415
-
416
- def initialize(spec, argv, name: nil, exception: false)
417
- @name = name || File.basename($PROGRAM_NAME)
418
- @spec, @argv = spec, argv.dup
419
- exprs = Grammar::Lexer.lex(@spec)
420
- commands = Grammar::Parser.parse(@name, exprs)
421
- @grammar = Grammar::Analyzer.analyze(commands)
422
-
423
- begin
424
- @program, @arguments = Ast::Parser.parse(@grammar, @argv)
425
- rescue Error => ex
426
- raise if exception
427
- error(ex.subject, ex.message)
428
- end
429
- end
430
-
431
- def error(subject = nil, message)
432
- $stderr.puts "#{name}: #{message}"
433
- usage(subject, device: $stderr)
434
- exit 1
435
- end
436
-
437
- def fail(message)
438
- $stderr.puts "#{name}: #{message}"
439
- exit 1
440
- end
441
-
442
- def usage(subject = nil, device: $stdout, levels: 1, margin: "")
443
- subject = find_subject(subject)
444
- device.puts Formatter.usage_string(subject, levels: levels, margin: margin)
445
- end
446
-
447
- def help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
448
- subject = find_subject(subject)
449
- device.puts Formatter.help_string(subject, levels: levels, margin: margin, tab: tab)
450
- end
451
-
452
- private
453
- def lookup(name)
454
- a = name.split(".")
455
- cmd = grammar
456
- while element = a.shift
457
- cmd = cmd.commands[element]
458
- end
459
- cmd
460
- end
461
-
462
- def find_subject(obj)
463
- case obj
464
- when String; lookup(obj)
465
- when Ast::Command; Command.grammar(obj)
466
- when Grammar::Command; obj
467
- when NilClass; grammar
468
- else
469
- raise Internal, "Illegal object: #{obj.class}"
470
- end
471
- end
472
- end
473
-
474
- def self.process(spec, argv, name: nil, exception: false)
475
- $shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
476
- [$shellopts.program, $shellopts.arguments]
477
- end
478
-
479
- def self.error(subject = nil, message)
480
- $shellopts.error(subject, message)
481
- end
482
-
483
- def self.fail(message)
484
- $shellopts.fail(message)
485
- end
486
-
487
- def self.help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
488
- $shellopts.help(subject, device: device, levels: levels, margin: margin, tab: tab)
489
- end
490
-
491
- def self.usage(subject = nil, device: $stdout, levels: 1, margin: "")
492
- $shellopts.usage(subject, device: device, levels: levels, margin: margin)
493
- end
494
- end
495
-
data/main CHANGED
@@ -7,12 +7,15 @@ require 'shellopts'
7
7
 
8
8
  include ShellOpts
9
9
 
10
+ p ShellOpts::ShellOpts.default_version
11
+ exit
12
+
10
13
  VERSION = "1.2.3"
11
14
 
12
15
  SPEC = %(
13
16
  -a @ An option
14
17
  )
15
- opts, args = ShellOpts::process(SPEC, ARGV, version: VERSION)
18
+ opts, args = ShellOpts.process(SPEC, ARGV, version: VERSION)
16
19
  #ShellOpts::ShellOpts.help
17
20
 
18
21
 
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.0.9
4
+ version: 2.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-06 00:00:00.000000000 Z
11
+ date: 2022-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: forward_to