shellopts 2.0.8 → 2.0.11

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: e6c52f313fee54283e1d5e704dd8b75a96239db6cff70584eefe747cf56270c1
4
- data.tar.gz: 67b50b7b3a993bf77533246c9634ce7850d765891381244fedf2b1ecf3f6bc76
3
+ metadata.gz: 485e81c58a890240d2a77e24a5f3a7a291551da968f395f3aaa4f5b2dac3cc9b
4
+ data.tar.gz: 2d6c92ca01ce1207f6311cf9b0e9bab3e555e309142ba6916d589396f215afb8
5
5
  SHA512:
6
- metadata.gz: 3cf5cd91d92bce24b1d04a3a526d6f43f8fe761f760319bf60191e2e638c3301fdbf9c63aa9f465aa8755cd18aa56293f0ae68e01870d6a877928bc1c0eebf5c
7
- data.tar.gz: 0e1fa649cff6a072d676a33dab6a3cbb038cb6799b59088f7ebff1144e6983843fe16658c80fa75308f057caf24e5b232aea15c92f80290d0969d868e653159e
6
+ metadata.gz: 31f3801b98585bed378ff77ae6632d18951ef37a49b3cfde843df9718dc5b46da40e777dff3f7b29e01bf8b32f2ebe0e1e7ab76ddb66744a461acf0061f75b1f
7
+ data.tar.gz: d72bcf1d58e9afdf4f92477af1f2824e01031a46b4cacf5283878ce1045ad91dd930c1c1a15cd588a2552a4ad90fe5ac3add6019b50e099693aa184b7747153f
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
  }
@@ -112,37 +116,34 @@ module ShellOpts
112
116
 
113
117
  section = {
114
118
  Paragraph => "DESCRIPTION",
115
- OptionGroup => "OPTIONS",
116
- Command => "COMMANDS"
119
+ OptionGroup => "OPTION",
120
+ Command => "COMMAND"
117
121
  }
118
122
 
123
+ seen_sections = {}
119
124
  newline = false # True if a newline should be printed before child
120
125
  indent {
121
126
  children.each { |child|
122
- if child.is_a?(Section) # Explicit section
123
- # p :A
124
- puts
125
- indent(-1).puts Ansi.bold child.name
126
- section.delete_if { |_,v| v == child.name }
127
+ klass = child.is_a?(Section) ? section.key(child.name) : child.class
128
+ if s = section[klass] # Implicit section
129
+ section.delete(klass)
127
130
  section.delete(Paragraph)
128
- newline = false
129
- next
130
- elsif s = section[child.class] # Implicit section
131
- # p :B
132
- puts
131
+ if klass <= OptionGroup
132
+ s += "S" if options.size > 1
133
+ elsif klass <= Command
134
+ s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
135
+ end
136
+ puts
133
137
  indent(-1).puts Ansi.bold s
134
- section.delete(child.class)
135
- section.delete(Paragraph)
136
138
  newline = false
137
- else # Any other node add a newline
138
- # p :C
139
+ next if child.is_a?(Section)
140
+ else # Any other node adds a newline
139
141
  puts if newline
140
142
  newline = true
141
143
  end
142
144
 
143
145
  if child.is_a?(Command)
144
- # prefix = child.parent != self ? nil : child.supercommand&.name
145
- prefix = child.supercommand == self ? nil : child.supercommand&.name
146
+ prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
146
147
  child.puts_descr(prefix, brief: false, name: :path)
147
148
  newline = true
148
149
  else
@@ -153,9 +154,10 @@ module ShellOpts
153
154
 
154
155
  # Also emit commands not declared in nested scope
155
156
  (commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
157
+ next if cmd.parent.nil? # Skip implicit commands
156
158
  puts if newline
157
159
  newline = true
158
- prefix = cmd.supercommand == self ? nil : cmd.supercommand&.name
160
+ prefix = cmd.command == self ? nil : cmd.command&.name
159
161
  cmd.puts_descr(prefix, brief: false, name: path)
160
162
  }
161
163
  }
@@ -207,7 +209,10 @@ module ShellOpts
207
209
  BRIEF_COL1_MAX_WIDTH = 40
208
210
 
209
211
  # Minimum width of second column in brief option and command lists
210
- 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
211
216
 
212
217
  # Indent to use in help output
213
218
  HELP_INDENT = 4
@@ -217,29 +222,21 @@ module ShellOpts
217
222
 
218
223
  # Usage string in error messages
219
224
  def self.usage(subject)
220
- subject = Grammar::Command.command(subject)
221
- @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
222
227
  setup_indent(1) {
223
228
  print lead = "#{USAGE_STRING}: "
224
- indent(lead.size, ' ', bol: false) { subject.puts_usage }
229
+ indent(lead.size, ' ', bol: false) { command.puts_usage }
225
230
  }
226
231
  end
227
232
 
228
- # # TODO
229
- # def self.usage=(usage_lambda)
230
- # end
231
-
232
233
  # When the user gives a -h option
233
- def self.brief(command)
234
- command = Grammar::Command.command(command)
234
+ def self.brief(subject)
235
+ command = Grammar::Command.command(subject)
235
236
  @command_prefix = command.ancestors.map { |node| node.name + " " }.join
236
237
  setup_indent(BRIEF_INDENT) { command.puts_brief }
237
238
  end
238
239
 
239
- # # TODO
240
- # def self.brief=(brief_lambda)
241
- # end
242
-
243
240
  # When the user gives a --help option
244
241
  def self.help(subject)
245
242
  subject = Grammar::Command.command(subject)
@@ -253,18 +250,6 @@ module ShellOpts
253
250
  obj.is_a?(Grammar::Command) ? obj : obj.__grammar__
254
251
  end
255
252
 
256
- # # TODO
257
- # def self.help_w_lambda(program)
258
- # if @help_lambda
259
- # #
260
- # else
261
- # program = Grammar::Command.command(program)
262
- # setup_indent(HELP_INDENT) { program.puts_descr }
263
- # end
264
- # end
265
- #
266
- # def self.help=(help_lambda) @help_lambda end
267
-
268
253
  def self.puts_columns(widths, fields)
269
254
  l = []
270
255
  first_width, second_width = *widths
@@ -275,40 +260,47 @@ module ShellOpts
275
260
  puts first
276
261
  indent(first_width + BRIEF_COL_SEP, ' ') { puts second.wrap(second_width) } if second
277
262
  elsif second
278
- printf "%-#{first_width + BRIEF_COL_SEP}s", first
279
- 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) }
280
266
  else
281
267
  puts first
282
268
  end
283
269
  end
284
270
  end
285
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
286
276
  def self.compute_columns(width, fields)
287
- first_max = [
288
- (fields.map { |first, _| first.size } + [BRIEF_COL1_MIN_WIDTH]).max,
289
- BRIEF_COL1_MAX_WIDTH
290
- ].min
291
- second_max = fields.map { |_, second| second ? second&.map(&:size).sum + second.size - 1: 0 }.max
292
-
293
- if first_max + BRIEF_COL_SEP + second_max <= width
294
- first_width = first_max
295
- second_width = second_max
296
- elsif first_max + BRIEF_COL_SEP + BRIEF_COL2_MAX_WIDTH <= width
297
- first_width = first_max
298
- 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
299
287
  else
300
- first_width = [width - BRIEF_COL_SEP - BRIEF_COL2_MAX_WIDTH, BRIEF_COL1_MAX_WIDTH].min
301
- second_width = BRIEF_COL2_MAX_WIDTH
288
+ second_width = [[rest, BRIEF_COL2_MIN_WIDTH].max, BRIEF_COL2_MAX_WIDTH].min
302
289
  end
303
-
304
290
  [first_width, second_width]
305
291
  end
306
292
 
307
293
  def self.width()
308
294
  @width ||= TermInfo.screen_width - MARGIN_RIGHT
295
+ @width
296
+ end
297
+
298
+ # Used in rspec
299
+ def self.width=(width)
300
+ @width = width
309
301
  end
310
302
 
311
- def self.rest() width - $stdout.margin end
303
+ def self.rest() width - $stdout.tab end
312
304
 
313
305
  private
314
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,
@@ -163,14 +166,6 @@ module ShellOpts
163
166
  @nodes = {}
164
167
  end
165
168
 
166
- # def add_stdopts
167
- # version_token = Token.new(:option, 1, 1, "--version")
168
- # version_brief = Token.new(:brief, 1, 1, "Gryf gryf")
169
- # group = Grammar::OptionGroup.new(@program, version_token)
170
- # option = Grammar::Option.parse(group, version_token)
171
- # brief = Grammr::Brief.parse(option, version_brief)
172
- # end
173
-
174
169
  def parse()
175
170
  @program = Grammar::Program.parse(@tokens.shift)
176
171
  oneline = @tokens.first.lineno == @tokens.last.lineno
@@ -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.8"
2
+ VERSION = "2.0.11"
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
104
97
 
105
- # Version of client program. This is only used if +stdopts+ is true
106
- attr_reader :version
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
106
+
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
@@ -364,132 +367,3 @@ module ShellOpts
364
367
  end
365
368
  end
366
369
 
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.8
4
+ version: 2.0.11
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-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: forward_to