shellopts 2.0.8 → 2.0.11

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