shellopts 1.0.1 → 2.0.0.pre.1

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: 3096c35af16a39ffc98b52f02799aa25efa311b20916309496ee33b2a50dcf47
4
- data.tar.gz: ebc336b7a5595412d1f705378f24dc6136c17e3257db7ca5ddec021349ec2a88
3
+ metadata.gz: e60db3cf5de50dcd106cb0fca007d064e18354278a65207bb167bf8aa63d3433
4
+ data.tar.gz: 85b0108262357d6e5654e725ab12293f51fd2579beb13fa0d910be2200c32310
5
5
  SHA512:
6
- metadata.gz: d44d80cc028fa9c9a7add7206b6a0f096cc3deb1c7e3f756075171c4ebfb20fdb6246215ec02d8920e9b450ebfcd5355a0c3b4179cc03d7fb4f6f89eea6f3a15
7
- data.tar.gz: 5bb562a9806196a6b6594897d3a09b8a1dc70abebf9f51194a6d31c031e86c54e69d1531acbb5ba6d675afb97c12d86ad2da4216af06bdcac8e056430787a97a
6
+ metadata.gz: 375fe97651622560d786b288217e1a8581d12bbc497dee55b36848597d9410d393a127e86088cfe4ac8f949270d9f6797d6f135f65492f15bb9d4a84bff1ffbc
7
+ data.tar.gz: c69a813e4672d87c6a4da69e0e8086d8acfe54c8506cda64ce6dc36dbc12669cc79c29ffd480c6871937d87491139f4e7448fd35a6d6a7d333451addd6c634a6
data/README.md CHANGED
@@ -378,6 +378,16 @@ release a new version, update the version number in `version.rb`, and then run
378
378
  git commits and tags, and push the `.gem` file to
379
379
  [rubygems.org](https://rubygems.org).
380
380
 
381
+ ## Implementation
382
+
383
+ FIXME
384
+ # ShellOpts is a library for parsing command line options and commands. It
385
+ # consists of the interface module {ShellOpts}, the implementation class
386
+ # {ShellOpts::ShellOpts} and the representation classes
387
+ # {ShellOpts::OptionsHash} and {ShellOpts::OptionsStruct}.
388
+ # {ShellOpts::Messenger} is used for error messages
389
+
390
+
381
391
  ## Contributing
382
392
 
383
393
  Bug reports and pull requests are welcome on GitHub at
data/TODO CHANGED
@@ -1,5 +1,18 @@
1
1
 
2
2
  TODO
3
+ o Add validation block to ShellOpts class methods
4
+ o Get rid of key_name. Define #name on Grammar::Node instead
5
+ o Define #name to the string name of the option/command without prefixed '--'
6
+ for options. This can cause collisions but they can be avoided using aliases
7
+ o Clean-up
8
+ o Grammar::options -> Grammar::option_multihash
9
+ o Clean-up identifiers etc.
10
+ o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
11
+ o subcommand vs. command consistency
12
+ o Implement ObjectStruct#key! and ObjectStruct#value! (?)
13
+ o Allow command_alias == nil to suppress the method
14
+ o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
15
+ o Use hash_tree
3
16
  o Also allow assignment to usage string for ShellOpts::ShellOpts objects
4
17
  o Create a ShellOpts.args method? It would be useful when processing commands:
5
18
  case opt
@@ -9,7 +22,10 @@ TODO
9
22
  ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
10
23
  Another option would be to create an argument-processing method:
11
24
  shellopts.argv(2) -> call error if not exactly two arguments else return elements
12
-
25
+ o Add a ShellOpts.option method:
26
+ file = ShellOpts.option("--file")
27
+ This will only work for options on the outermost level... maybe:
28
+ file = ShellOpts.option("load! --file")
13
29
  o Check on return value from #process block to see if all options was handled:
14
30
  case opt
15
31
  when '-v'; verbose = true # Return value 'true' is ok
@@ -23,7 +23,7 @@ module ShellOpts
23
23
  end
24
24
 
25
25
  # Return either a value (option value), an array of values (command), or
26
- # nil (option without a value). Should be defined in sub-classes
26
+ # nil (option without a value). It must be defined in sub-classes of Ast::Node
27
27
  def values() raise end
28
28
 
29
29
  # :nocov:
@@ -8,12 +8,12 @@ require 'shellopts/grammar/program.rb'
8
8
  module ShellOpts
9
9
  module Grammar
10
10
  # Compiles an option definition string and returns a Grammar::Program
11
- # object. program_name is the name of the program and source is the
11
+ # object. name is the name of the program and source is the
12
12
  # option definition string
13
- def self.compile(program_name, source)
14
- program_name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{program_name.class}"
13
+ def self.compile(name, source)
14
+ name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{name.class}"
15
15
  source.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{source.class}"
16
- Compiler.new(program_name, source).call
16
+ Compiler.new(name, source).call
17
17
  end
18
18
 
19
19
  # Service object for compiling an option definition string. Returns a
@@ -26,8 +26,8 @@ module ShellOpts
26
26
  class Error < RuntimeError; end
27
27
 
28
28
  # Initialize a Compiler object. source is the option definition string
29
- def initialize(program_name, source)
30
- @program_name, @tokens = program_name, source.split(/\s+/).reject(&:empty?)
29
+ def initialize(name, source)
30
+ @name, @tokens = name, source.split(/\s+/).reject(&:empty?)
31
31
 
32
32
  # @commands_by_path is an hash from command-path to Command or Program
33
33
  # object. The top level Program object has nil as its path.
@@ -54,7 +54,7 @@ module ShellOpts
54
54
  end
55
55
 
56
56
  def compile_program
57
- program = @commands_by_path[nil] = Grammar::Program.new(@program_name, compile_options)
57
+ program = @commands_by_path[nil] = Grammar::Program.new(@name, compile_options)
58
58
  while curr_token && curr_token != "--"
59
59
  compile_command
60
60
  end
@@ -0,0 +1,15 @@
1
+
2
+ require 'shellopts/idr.rb'
3
+
4
+ module ShellOpts
5
+ module Idr
6
+ # Generates an Idr::Program from an Ast::Program object
7
+ def self.generate(ast, messenger)
8
+ Idr::Program.new(ast, messenger)
9
+ end
10
+ end
11
+ end
12
+
13
+
14
+
15
+
@@ -11,12 +11,8 @@ module ShellOpts
11
11
  # Name of command (String). Name doesn't include the exclamation point ('!')
12
12
  attr_reader :name
13
13
 
14
- # Hash from option names (both short and long names) to option. This
15
- # means an option can occur more than once as the hash value
16
- attr_reader :options
17
-
18
- # Sub-commands of this command. Is a hash from sub-command name to command object
19
- attr_reader :commands
14
+ # Same as #name. TODO Define in Grammar::Node instead
15
+ alias :key_name :name
20
16
 
21
17
  # List of options in declaration order
22
18
  attr_reader :option_list
@@ -24,6 +20,23 @@ module ShellOpts
24
20
  # List of commands in declaration order
25
21
  attr_reader :command_list
26
22
 
23
+ # Multihash from option key or names (both short and long names) to option. This
24
+ # means an option can occur more than once as the hash value
25
+ def options()
26
+ @option_multihash ||= @option_list.flat_map { |option|
27
+ option.identifiers.map { |ident| [ident, option] }
28
+ }.to_h
29
+ end
30
+
31
+ # Sub-commands of this command. Is a multihash from sub-command key or
32
+ # name to command object. Lazily constructed because subcommands are added
33
+ # after initialization
34
+ def commands()
35
+ @command_multihash ||= @command_list.flat_map { |command|
36
+ command.identifiers.map { |name| [name, command] }
37
+ }.to_h
38
+ end
39
+
27
40
  # Initialize a Command object. parent is the parent Command object or nil
28
41
  # if this is the root object. name is the name of the command (without
29
42
  # the exclamation mark), and option_list a list of Option objects
@@ -32,11 +45,17 @@ module ShellOpts
32
45
  @name = name
33
46
  parent.attach(self) if parent
34
47
  @option_list = option_list
35
- @options = @option_list.flat_map { |opt| opt.names.map { |name| [name, opt] } }.to_h
36
- @commands = {}
37
48
  @command_list = []
38
49
  end
39
50
 
51
+ # Return key for the identifier
52
+ def identifier2key(ident)
53
+ options[ident]&.key || commands[ident]&.key
54
+ end
55
+
56
+ # Return list of identifiers for the command
57
+ def identifiers() [key, name] end
58
+
40
59
  # :nocov:
41
60
  def dump(&block)
42
61
  puts "#{key.inspect}"
@@ -55,7 +74,6 @@ module ShellOpts
55
74
  protected
56
75
  def attach(command)
57
76
  command.instance_variable_set(:@parent, self)
58
- @commands[command.name] = command
59
77
  @command_list << command
60
78
  end
61
79
  end
@@ -10,6 +10,9 @@ module ShellOpts
10
10
  # List of long names (incl. '--')
11
11
  attr_reader :long_names
12
12
 
13
+ # Name of the key attribute (eg. if key is :all then key_name is '--all'
14
+ attr_reader :key_name
15
+
13
16
  # List of flags (Symbol)
14
17
  def flags() @flags.keys end
15
18
 
@@ -23,7 +26,8 @@ module ShellOpts
23
26
  # there's no :string flag, it's status is inferred. label is the optional
24
27
  # informal name of the option argument (eg. 'FILE') or nil if not present
25
28
  def initialize(short_names, long_names, flags, label = nil)
26
- super((long_names.first || short_names.first).sub(/^-+/, "").to_sym)
29
+ @key_name = long_names.first || short_names.first
30
+ super(@key_name.sub(/^-+/, "").to_sym)
27
31
  @short_names, @long_names = short_names, long_names
28
32
  @flags = flags.map { |flag| [flag, true] }.to_h
29
33
  @label = label
@@ -32,6 +36,12 @@ module ShellOpts
32
36
  # Array of option names with short names first and then the long names
33
37
  def names() @short_names + @long_names end
34
38
 
39
+ # Array of names and the key
40
+ def identifiers() names + [key] end
41
+
42
+ # Return true if +ident+ is equal to any name or to key
43
+ def match?(ident) names.include?(ident) || ident == key end
44
+
35
45
  # Flag query methods. Returns true if the flag is present and otherwise nil
36
46
  def repeated?() @flags[:repeated] || false end
37
47
  def argument?() @flags[:argument] || false end
@@ -17,7 +17,7 @@ module ShellOpts
17
17
  def usage
18
18
  (
19
19
  render_options(option_list) +
20
- commands.values.map { |cmd| render_command(cmd) } +
20
+ command_list.map { |cmd| render_command(cmd) } +
21
21
  args
22
22
  ).flatten.join(" ")
23
23
  end
@@ -34,7 +34,7 @@ module ShellOpts
34
34
  private
35
35
  def render_command(command)
36
36
  [command.name] + render_options(command.option_list) +
37
- command.commands.values.map { |cmd| render_command(cmd) }.flatten
37
+ command.command_list.map { |cmd| render_command(cmd) }.flatten
38
38
  end
39
39
 
40
40
  def render_options(options)
@@ -0,0 +1,209 @@
1
+
2
+ module ShellOpts
3
+ # Idr models the Internal Data Representation of a program. It is the native
4
+ # representation of a command
5
+ #
6
+ # The IDR should ideally be completely detached from the compile-time grammar
7
+ # and AST but they are only hidden from view in this implementation. Create
8
+ # a Shellopts object instead to access the compiler data
9
+ #
10
+ module Idr
11
+ # Base class for the Idr class hierarchy. It is constructed from an Ast
12
+ # object by #generate. Node is modelled as an element of a hash with a key
13
+ # and a value. Options have their (optional) argument as value while
14
+ # commands use +self+ as value
15
+ class Node
16
+ # Unique key (within context) for the option or command. nil for the
17
+ # top-level Program object
18
+ #
19
+ # It is usually the first long option if present and else the first short
20
+ # option turned into a Symbol by first removing prefixed dashed, eg.
21
+ # '--all' becomes :all
22
+ attr_reader :key
23
+
24
+ # Name of command and option as used on the command line
25
+ attr_reader :name
26
+
27
+ # Value of node. This can be a simple value (String, Integer, or Float),
28
+ # an Array of values, or a Idr::Command object. Note that the value of a
29
+ # Command object is the object itself
30
+ #
31
+ # Repeated options are implemented as an Array with one element for each
32
+ # use of the option. The element is nil if the option doesn't take
33
+ # arguments or if an optional argument is missing.
34
+ attr_reader :value
35
+
36
+ protected
37
+ # Copy arguments into instance variables
38
+ def initialize(ast, key, name, value)
39
+ @ast, @key, @name, @value = ast, key, name, value
40
+ end
41
+
42
+ # The AST node for this Idr object
43
+ attr_reader :ast
44
+
45
+ # Shorthand to the grammar node for this Idr object
46
+ def grammar() @ast.grammar end
47
+ end
48
+
49
+ # Base class for Options
50
+ class Option < Node
51
+ end
52
+
53
+ class SimpleOption < Option
54
+ protected
55
+ # Initialize with defauls from the Ast. +value+ is set to true if option
56
+ # doesn't take an argument
57
+ def initialize(ast)
58
+ value = ast.grammar.argument? ? ast.value : true
59
+ super(ast, ast.key, ast.name, value)
60
+ end
61
+ end
62
+
63
+ # An OptionGroup models repeated options collapsed into a single key. The
64
+ # name of the group should be set to the name of the key (eg. '--all' if
65
+ # the key is :all)
66
+ class OptionGroup < Option
67
+ # Array of names of the options
68
+ attr_reader :names
69
+
70
+ # Array of values of the options
71
+ alias :values :value
72
+
73
+ # Name is set to the key name and value to an array of option values
74
+ def initialize(key, name, options)
75
+ @names = options.map(&:name)
76
+ super(nil, key, name, options.map(&:value))
77
+ end
78
+ end
79
+
80
+ class Command < Node
81
+ # Hash from key to options with repeated option_list collapsed into a
82
+ # option group. It also include an entry for the subcommand. Options are
83
+ # ordered by first use on the command line. The command entry will always
84
+ # be last
85
+ attr_reader :options
86
+
87
+ # List of command line options in the same order as on the command line
88
+ attr_reader :option_list
89
+
90
+ # Subcommand object. Possibly nil
91
+ attr_reader :subcommand
92
+
93
+ # True if ident is declared
94
+ def declared?(ident) option?(ident) || command?(ident) end
95
+
96
+ # True if ident is declared as an option
97
+ def option?(ident) grammar.options.key?(ident) end
98
+
99
+ # True if ident is declared as a command
100
+ def command?(ident) grammar.commands.key?(ident) end
101
+
102
+ # True if ident is present
103
+ def key?(ident)
104
+ declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
105
+ key = grammar.identifier2key(ident)
106
+ @options.key?(key)
107
+ end
108
+
109
+ # Value of ident. Repeated options are collapsed into an OptionGroup object
110
+ def [](ident)
111
+ declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
112
+ key = grammar.identifier2key(ident)
113
+ if @options.key?(key)
114
+ @options[key].value
115
+ elsif option?(key)
116
+ false
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ # Apply defaults recursively. Values can be lambdas that will be evaluated to
123
+ # get the default value
124
+ def apply(defaults = {}) end
125
+
126
+ # Return options and command as an array
127
+ def to_a() @ast.values end
128
+
129
+ # Return options and command as a hash. The hash also define the
130
+ # singleton method #subcommand that returns the key of the subcommand
131
+ #
132
+ # +key+ controls the type of keys used: +:key+ (the default) use the
133
+ # symbolic key, +:name+ use key_name. Note that using +:name+ can cause name collisions between
134
+ # option and command names and that #to_s raises an exception if it detects a collision
135
+ #
136
+ # +aliases+ maps from key to replacement key (which could be any object).
137
+ # +aliases+ can be used to avoid name collisions between options and
138
+ # commands
139
+ #
140
+ # IDEA: Make subcommand _not_ follow the +key+ setting so that setting key to
141
+ # IDEA: Add a singleton method #subcommand to the hash
142
+ #
143
+ def to_h(use: :key, aliases: {})
144
+ value = {}
145
+ value.define_singleton_method(:subcommand) { nil }
146
+ options.values.each { |opt|
147
+ ident = aliases[opt.key] || (use == :key ? opt.key : opt.ast.grammar.key_name)
148
+ !value.key?(ident) or raise ConversionError, "Duplicate key: #{ident.inspect}"
149
+ case opt
150
+ when Option
151
+ value[ident] = opt.value
152
+ when Command
153
+ value[ident] = opt.value.to_h
154
+ value.define_singleton_method(:subcommand) { ident } # Redefine
155
+ else
156
+ raise InternalError, "Oops"
157
+ end
158
+ }
159
+ value
160
+ end
161
+
162
+ # Return options and command as a struct
163
+ def to_struct(key = :key, aliases = {}) OptionStruct.new(self, key, aliases) end
164
+
165
+ protected
166
+ # Initialize an Idr::Command object and all dependent objects
167
+ def initialize(ast)
168
+ super(ast, ast.key, ast.name, self)
169
+ @option_list = ast.options.map { |node| SimpleOption.new(node) }
170
+ @subcommand = Command.new(ast.command) if ast.command
171
+ @options = @option_list.group_by { |option| option.key }.map { |key, option_list|
172
+ option =
173
+ if ast.grammar.options[key].repeated?
174
+ OptionGroup.new(key, ast.grammar.options[key].key_name, option_list)
175
+ else
176
+ option_list.first
177
+ end
178
+ [key, option]
179
+ }.to_h
180
+ @options[subcommand.key] = @subcommand if @subcommand
181
+ end
182
+ end
183
+
184
+ class Program < Command
185
+ # #key is nil for the top-level Program object
186
+ def key() nil end
187
+
188
+ # Remaining command line arguments
189
+ def args() @ast.arguments end
190
+
191
+ # Messenger object that is used to emit error messages. It should
192
+ # implement #error(*args) and #fail(*args)
193
+ attr_reader :messenger
194
+
195
+ # Initialize the top-level Idr::Program object
196
+ def initialize(ast, messenger)
197
+ @messenger = messenger
198
+ super(ast)
199
+ end
200
+
201
+ # Emit error message and a usage description before exiting with status 1
202
+ def error(*args) messenger.error(*error_messages) end
203
+
204
+ # Emit error message before exiting with status 1
205
+ def fail(*args) messenger.fail(*error_messages) end
206
+ end
207
+ end
208
+ end
209
+
@@ -0,0 +1,71 @@
1
+
2
+ module ShellOpts
3
+ # Service object for output of messages
4
+ #
5
+ # Messages are using the common command line formats
6
+ #
7
+ class Messenger
8
+ # Name of the program. When assigning to +name+ prefixed and suffixed
9
+ # whitespaces are removed
10
+ attr_accessor :name
11
+
12
+ # :nodoc:
13
+ def name=(name) @name = name.strip end
14
+ # :nodoc:
15
+
16
+ # Usage string. If not nil the usage string is printed by #error. When
17
+ # assigning to +usage+ suffixed whitespaces are removed and the format
18
+ # automatically set to +:custom+
19
+ attr_accessor :usage
20
+
21
+ # :nodoc:
22
+ def usage=(usage)
23
+ @format = :custom
24
+ @usage = usage&.rstrip
25
+ end
26
+ # :nodoc:
27
+
28
+ # Format of the usage string: +:default+ prefixes the +usage+ with 'Usage:
29
+ # #{name} ' before printing. +:custom+ prints +usage+ as is
30
+ attr_accessor :format
31
+
32
+ # Initialize a Messenger object. +name+ is the name of the name and +usage+
33
+ # is a short description of the options (eg. '-a -b') or a longer multiline
34
+ # explanation. The +:format+ option selects bewtween the two: +short+ (the
35
+ # default) or :long. Note that
36
+ #
37
+ def initialize(name, usage, format: :default)
38
+ @name = name
39
+ @usage = usage
40
+ @format = format
41
+ end
42
+
43
+ # Print error message and usage string and exit with status 1. Output is
44
+ # using the following format
45
+ #
46
+ # <name name>: <message>
47
+ # Usage: <name name> <options and arguments>
48
+ #
49
+ def error(*msgs)
50
+ $stderr.print "#{name}: #{msgs.join}\n"
51
+ if usage
52
+ $stderr.print "Usage: #{name} " if format == :default
53
+ $stderr.print "#{usage}\n"
54
+ end
55
+ exit 1
56
+ end
57
+
58
+ # Print error message and exit with status 1. It use the current ShellOpts
59
+ # object if defined. This method should not be called in response to
60
+ # user-errors but system errors (like disk full). Output is using the
61
+ # following format:
62
+ #
63
+ # <name name>: <message>
64
+ #
65
+ def fail(*msgs)
66
+ $stderr.puts "#{name}: #{msgs.join}"
67
+ exit 1
68
+ end
69
+ end
70
+ end
71
+