shellopts 2.0.7 → 2.0.10

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: 6e8933ca7084aea3e51858cd679eec2a6b34b2fc7ae9a55ccce131947ccc7375
4
- data.tar.gz: 125309b75338e9205fb05c5c556fca54cbc1cc77ef890a4ca2d6992c69eff302
3
+ metadata.gz: 812078e64c677a6aff78081e25a04b81be3b3a1fe9496709838e2abb80573b4a
4
+ data.tar.gz: 92c14741c409697f543820d656683b9552d4c6c7ce5c29b01a30677a174549c2
5
5
  SHA512:
6
- metadata.gz: 123b17ded3c4943267612959f3b8ab56a37249e38ea9c338c577ad254ebc1c7c9c305ca9a7a3ee2661d9474e905ddd193c5da8e7a5adc3e711c236349095a636
7
- data.tar.gz: a3115c875274fccf6628d4c839e3eac36804157f7c56e90b599e8201c1bf8f1070b5b92b7e22cbe2c6615e20b81f4de8b81233227aaccdb873f1016e129d99ae
6
+ metadata.gz: 3b1ac7453a534a50cf250e489688aa82ad1928220a615b9e4f7076c27b588d19a7b355e866f3e3cc789fd2259bb2e5de14ac92e905b7b033528c11be26564356
7
+ data.tar.gz: d50433ec49a4bf67fd9947d2028ed81be9c8310e7410d93c691e2cd59ed36ef622937c861ef1f4608e7ef5869804cd0f1a4197abf03fd4c2d96cef4985d5dab4
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
@@ -20,18 +20,17 @@ 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
30
26
 
31
- # Move options before first command
27
+ # Move options before first command or before explicit COMMAND section
32
28
  def reorder_options
33
29
  if commands.any?
34
- if i = children.find_index { |child| child.is_a?(Command) }
30
+ i = children.find_index { |child|
31
+ child.is_a?(Command) || child.is_a?(Section) && child.name == "COMMAND"
32
+ }
33
+ if i
35
34
  options, rest = children[i+1..-1].partition { |child| child.is_a?(OptionGroup) }
36
35
  @children = children[0, i] + options + children[i..i] + rest
37
36
  end
@@ -51,9 +50,9 @@ module ShellOpts
51
50
  }
52
51
  end
53
52
 
53
+ # TODO Check for dash-collision
54
54
  def compute_command_hashes
55
55
  commands.each { |command|
56
- # TODO Check for dash-collision
57
56
  !@commands_hash.key?(command.name) or
58
57
  analyzer_error command.token, "Duplicate command name: #{command.name}"
59
58
  @commands_hash[command.name] = command
@@ -73,41 +72,74 @@ module ShellOpts
73
72
  @grammar = grammar
74
73
  end
75
74
 
76
- # Move commands that are nested within a different command than it belongs to
77
- 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
78
93
  # We can't use Command#[] at this point so we collect the commands here
79
94
  h = {}
80
95
  @grammar.traverse(Grammar::Command) { |command|
81
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
82
113
  }
83
114
 
84
- # Find commands to move
115
+ # Find commands to link
85
116
  #
86
- # Commands are moved in two steps because the behaviour of #traverse is
87
- # not defined when the data structure changes beneath it
88
- 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?)
89
120
  @grammar.traverse(Grammar::Command) { |command|
90
121
  if command.path.size > 1 && command.parent && command.parent.path != command.path[0..-2]
91
- move << command
122
+ # if command.path.size > 1 && command.parent.path != command.path[0..-2]
123
+ link << command
92
124
  else
93
125
  command.instance_variable_set(:@command, command.parent)
94
126
  end
95
127
  }
96
128
 
97
- # Move commands but do not change parent/child relationship
98
- move.each { |command|
99
- supercommand = h[command.path[0..-2]] or analyzer_error "Can't find #{command.ident}!"
100
- 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
101
134
  supercommand.commands << command
102
135
  command.instance_variable_set(:@command, supercommand)
103
136
  }
104
137
  end
105
138
 
106
139
  def analyze()
107
- move_commands
140
+ link_commands
108
141
 
109
142
  @grammar.traverse(Grammar::Command) { |command|
110
- command.set_supercommand
111
143
  command.reorder_options
112
144
  command.collect_options
113
145
  command.compute_option_hashes
@@ -5,6 +5,11 @@ module ShellOpts
5
5
  # methods raise a ShellOpts::Error exception in case of errors
6
6
  #
7
7
  class Args < Array
8
+ def initialize(*args, exception: false)
9
+ super(*args)
10
+ @exception = exception
11
+ end
12
+
8
13
  # :call-seq:
9
14
  # extract(count, message = nil)
10
15
  # extract(range, message = nil)
@@ -21,7 +26,7 @@ module ShellOpts
21
26
  # #extract raise a ShellOpts::Error exception if there's is not enough
22
27
  # elements in the array to satisfy the request
23
28
  #
24
- def extract(count_or_range, message = nil)
29
+ def extract(count_or_range, message = nil)
25
30
  case count_or_range
26
31
  when Range
27
32
  range = count_or_range
@@ -59,8 +64,10 @@ module ShellOpts
59
64
  end
60
65
 
61
66
  private
62
- def inoa(message = nil)
63
- raise ShellOpts::Error, message || "Illegal number of arguments"
67
+ def inoa(message = nil)
68
+ message ||= "Illegal number of arguments"
69
+ raise Error, message if @exception
70
+ ::ShellOpts.error(message)
64
71
  end
65
72
  end
66
73
  end
@@ -81,7 +81,15 @@ module ShellOpts
81
81
  end
82
82
 
83
83
  def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
84
- puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
84
+ # Use one-line mode if all options are declared on one line
85
+ if options.all? { |option| option.token.lineno == token.lineno }
86
+ puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
87
+ puts_options = false
88
+ else
89
+ puts Ansi.bold([prefix, render(:abbr, Formatter.rest)].flatten.compact.join(" "))
90
+ puts_options = true
91
+ end
92
+
85
93
  indent {
86
94
  if brief
87
95
  puts self.brief.words.wrap(Formatter.rest)
@@ -93,7 +101,10 @@ module ShellOpts
93
101
 
94
102
  if child.is_a?(Command)
95
103
  child.puts_descr(prefix, name: :path)
96
- else
104
+ elsif child.is_a?(OptionGroup)
105
+ child.puts_descr if puts_options
106
+ newline = false
107
+ else
97
108
  child.puts_descr
98
109
  end
99
110
  }
@@ -112,37 +123,34 @@ module ShellOpts
112
123
 
113
124
  section = {
114
125
  Paragraph => "DESCRIPTION",
115
- OptionGroup => "OPTIONS",
116
- Command => "COMMANDS"
126
+ OptionGroup => "OPTION",
127
+ Command => "COMMAND"
117
128
  }
118
129
 
130
+ seen_sections = {}
119
131
  newline = false # True if a newline should be printed before child
120
132
  indent {
121
133
  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 }
134
+ klass = child.is_a?(Section) ? section.key(child.name) : child.class
135
+ if s = section[klass] # Implicit section
136
+ section.delete(klass)
127
137
  section.delete(Paragraph)
128
- newline = false
129
- next
130
- elsif s = section[child.class] # Implicit section
131
- # p :B
132
- puts
138
+ if klass <= OptionGroup
139
+ s += "S" if options.size > 1
140
+ elsif klass <= Command
141
+ s += "S" if commands.size > 1 || commands.size == 1 && commands.first.commands.size > 1
142
+ end
143
+ puts
133
144
  indent(-1).puts Ansi.bold s
134
- section.delete(child.class)
135
- section.delete(Paragraph)
136
145
  newline = false
137
- else # Any other node add a newline
138
- # p :C
146
+ next if child.is_a?(Section)
147
+ else # Any other node adds a newline
139
148
  puts if newline
140
149
  newline = true
141
150
  end
142
151
 
143
152
  if child.is_a?(Command)
144
- # prefix = child.parent != self ? nil : child.supercommand&.name
145
- prefix = child.supercommand == self ? nil : child.supercommand&.name
153
+ prefix = child.path[path.size..-2].map { |sym| sym.to_s.sub(/!/, "") }
146
154
  child.puts_descr(prefix, brief: false, name: :path)
147
155
  newline = true
148
156
  else
@@ -153,9 +161,10 @@ module ShellOpts
153
161
 
154
162
  # Also emit commands not declared in nested scope
155
163
  (commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
164
+ next if cmd.parent.nil? # Skip implicit commands
156
165
  puts if newline
157
166
  newline = true
158
- prefix = cmd.supercommand == self ? nil : cmd.supercommand&.name
167
+ prefix = cmd.command == self ? nil : cmd.command&.name
159
168
  cmd.puts_descr(prefix, brief: false, name: path)
160
169
  }
161
170
  }
@@ -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"
@@ -151,6 +160,10 @@ module ShellOpts
151
160
  class OptionGroup < Node
152
161
  alias_method :command, :parent
153
162
 
163
+ # Duck typing for compatibility with IdrNode (TODO: maybe just make
164
+ # OptionGroup an IdrNode and be over with it)
165
+ def name() options.first&.name end
166
+
154
167
  # Array of options in declaration order
155
168
  attr_reader :options
156
169
 
@@ -185,10 +198,6 @@ module ShellOpts
185
198
  # methods are initialized by the analyzer
186
199
  #
187
200
  class Command < IdrNode
188
- # Supercommand or nil if this is the top-level Program object.
189
- # Initialized by the analyzer
190
- attr_reader :supercommand
191
-
192
201
  # Brief description of command
193
202
  attr_accessor :brief
194
203
 
@@ -352,6 +361,10 @@ module ShellOpts
352
361
  end
353
362
 
354
363
  class Section < Node
364
+ def initialize(parent, token)
365
+ constrain token.source, *%w(DESCRIPTION OPTION COMMAND)
366
+ super
367
+ end
355
368
  def name() token.source end
356
369
  end
357
370
 
@@ -32,7 +32,7 @@ module ShellOpts
32
32
  end
33
33
  end
34
34
  end
35
- [@expr, Args.new(@args + @argv)]
35
+ [@expr, Args.new(@args + @argv, exception: @exception)]
36
36
  end
37
37
 
38
38
  def self.interpret(grammar, argv, **opts)
@@ -45,7 +45,7 @@ module ShellOpts
45
45
  # Match ArgDescr words (should be at least two characters long)
46
46
  DESCR_RE = /^[^a-z]{2,}$/
47
47
 
48
- SECTIONS = %w(DESCRIPTION OPTIONS COMMANDS)
48
+ SECTIONS = %w(DESCRIPTION OPTION OPTIONS COMMAND COMMANDS)
49
49
 
50
50
  using Ext::Array::ShiftWhile
51
51
 
@@ -109,7 +109,7 @@ module ShellOpts
109
109
 
110
110
  # Sections
111
111
  elsif SECTIONS.include?(line.text)
112
- @tokens << Token.new(:section, line.lineno, line.charno, line.text)
112
+ @tokens << Token.new(:section, line.lineno, line.charno, line.text.sub(/S$/, ""))
113
113
 
114
114
  # Options, commands, usage, arguments, and briefs
115
115
  elsif line =~ DECL_RE
@@ -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,16 +124,19 @@ 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
- paragraph_token = Token.new(:text, 1, 1, "-h prints a brief help text, --help prints a longer man-style description of the command")
138
+ paragraph_token = Token.new(:text, 1, 1,
139
+ "-h prints a brief help text, --help prints a longer man-style description of the command")
136
140
  group = OptionGroup.new(self, option_token)
137
141
  option = Option.parse(group, option_token)
138
142
  brief = Brief.parse(group, brief_token)
@@ -162,14 +166,6 @@ module ShellOpts
162
166
  @nodes = {}
163
167
  end
164
168
 
165
- # def add_stdopts
166
- # version_token = Token.new(:option, 1, 1, "--version")
167
- # version_brief = Token.new(:brief, 1, 1, "Gryf gryf")
168
- # group = Grammar::OptionGroup.new(@program, version_token)
169
- # option = Grammar::Option.parse(group, version_token)
170
- # brief = Grammr::Brief.parse(option, version_brief)
171
- # end
172
-
173
169
  def parse()
174
170
  @program = Grammar::Program.parse(@tokens.shift)
175
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.7"
2
+ VERSION = "2.0.10"
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
 
data/shellopts.gemspec CHANGED
@@ -1,7 +1,5 @@
1
1
 
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "shellopts/version"
2
+ require_relative "lib/shellopts/version"
5
3
 
6
4
  Gem::Specification.new do |spec|
7
5
  spec.name = "shellopts"
@@ -15,15 +13,18 @@ Gem::Specification.new do |spec|
15
13
  and has built-in help and error messages}
16
14
  spec.homepage = "http://github.com/clrgit/shellopts"
17
15
 
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
16
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
17
+ `git ls-files -z`.split("\x0").reject do |f|
18
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
19
+ end
20
20
  end
21
+
21
22
  spec.bindir = "exe"
22
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
24
  spec.require_paths = ["lib"]
24
25
 
25
26
  spec.add_dependency "forward_to"
26
- spec.add_dependency "constrain"
27
+ spec.add_dependency "constrain", "~> 0.5.1"
27
28
  spec.add_dependency "ruby-terminfo"
28
29
  spec.add_dependency "indented_io"
29
30
 
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.7
4
+ version: 2.0.10
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-03 00:00:00.000000000 Z
11
+ date: 2022-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: forward_to
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: constrain
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 0.5.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 0.5.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: ruby-terminfo
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -132,10 +132,8 @@ executables: []
132
132
  extensions: []
133
133
  extra_rdoc_files: []
134
134
  files:
135
- - ".gitignore"
136
135
  - ".rspec"
137
136
  - ".ruby-version"
138
- - ".travis.yml"
139
137
  - Gemfile
140
138
  - README.md
141
139
  - Rakefile
data/.gitignore DELETED
@@ -1,30 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /rdoc/
5
- /pkg/
6
- /spec/reports/
7
- /tmp/
8
-
9
- # rspec failure tracking
10
- .rspec_status
11
-
12
- # simplecov
13
- /coverage/
14
-
15
- # Ignore Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
16
- /Gemfile.lock
17
-
18
- # Ignore vim files
19
- .*.swp
20
-
21
- # Ignore t.* files
22
- t
23
- t.*
24
- tt
25
- tt.*
26
- s
27
- s.*
28
-
29
- # Ignore temporary directory
30
- /spec/tmpdir/
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.5.1
5
- before_install: gem install bundler -v 1.16.1