shellopts 1.0.0 → 2.0.0.pre.7
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 +4 -4
- data/.ruby-version +1 -1
- data/README.md +10 -0
- data/TODO +23 -1
- data/lib/shellopts.rb +209 -199
- data/lib/shellopts/args.rb +48 -0
- data/lib/shellopts/ast/command.rb +6 -6
- data/lib/shellopts/ast/node.rb +1 -1
- data/lib/shellopts/compiler.rb +25 -27
- data/lib/shellopts/generator.rb +15 -0
- data/lib/shellopts/grammar/command.rb +35 -18
- data/lib/shellopts/grammar/node.rb +11 -3
- data/lib/shellopts/grammar/option.rb +12 -1
- data/lib/shellopts/grammar/program.rb +4 -4
- data/lib/shellopts/idr.rb +236 -0
- data/lib/shellopts/option_struct.rb +148 -0
- data/lib/shellopts/parser.rb +11 -11
- data/lib/shellopts/shellopts.rb +116 -0
- data/lib/shellopts/version.rb +1 -1
- metadata +9 -5
- data/lib/shellopts/utils.rb +0 -16
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
module ShellOpts
|
3
|
+
# Specialization of Array for arguments lists. Args extends Array with a
|
4
|
+
# #extract and an #expect method to extract elements from the array. The
|
5
|
+
# methods call #error() in response to errors
|
6
|
+
class Args < Array
|
7
|
+
def initialize(shellopts, *args)
|
8
|
+
@shellopts = shellopts
|
9
|
+
super(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Remove and return elements from beginning of the array. If
|
13
|
+
# +count_or_range+ is a number, that number of elements will be returned.
|
14
|
+
# If the count is one, a simple value is returned instead of an array. If
|
15
|
+
# the count is negative, the elements will be removed from the end of the
|
16
|
+
# array. If +count_or_range+ is a range, the number of elements returned
|
17
|
+
# will be in that range. The range can't contain negative numbers #expect
|
18
|
+
# calls #error() if there's is not enough elements in the array to satisfy
|
19
|
+
# the request
|
20
|
+
def extract(count_or_range, message = nil)
|
21
|
+
if count_or_range.is_a?(Range)
|
22
|
+
range = count_or_range
|
23
|
+
range.min <= self.size or inoa(message)
|
24
|
+
n_extract = [self.size, range.max].min
|
25
|
+
n_extend = range.max > self.size ? range.max - self.size : 0
|
26
|
+
r = self.shift(n_extract) + Array.new(n_extend)
|
27
|
+
else
|
28
|
+
count = count_or_range
|
29
|
+
self.size >= count.abs or inoa(message)
|
30
|
+
start = count >= 0 ? 0 : size + count
|
31
|
+
r = slice!(start, count.abs)
|
32
|
+
r.size == 0 ? nil : (r.size == 1 ? r.first : r)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# As extract except it doesn't allow negative counts and that the array is
|
37
|
+
# expect to be emptied by the operation
|
38
|
+
def expect(count_or_range, message = nil)
|
39
|
+
count_or_range === self.size or inoa(message)
|
40
|
+
extract(count_or_range) # Can't fail
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def inoa(message = nil)
|
45
|
+
raise ShellOpts::UserError, message || "Illegal number of arguments"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -7,17 +7,17 @@ module ShellOpts
|
|
7
7
|
|
8
8
|
# Optional sub-command (Ast::Command). Initially nil but assigned by the
|
9
9
|
# parser
|
10
|
-
attr_accessor :
|
10
|
+
attr_accessor :subcommand
|
11
11
|
|
12
12
|
def initialize(grammar, name)
|
13
13
|
super(grammar, name)
|
14
14
|
@options = []
|
15
|
-
@
|
15
|
+
@subcommand = nil
|
16
16
|
end
|
17
17
|
|
18
18
|
# Array of option or command tuples
|
19
19
|
def values
|
20
|
-
(options + (Array(
|
20
|
+
(options + (Array(subcommand || []))).map { |node| node.to_tuple }
|
21
21
|
end
|
22
22
|
|
23
23
|
# :nocov:
|
@@ -26,10 +26,10 @@ module ShellOpts
|
|
26
26
|
yield if block_given?
|
27
27
|
puts "options:"
|
28
28
|
indent { options.each { |opt| opt.dump } }
|
29
|
-
print "
|
30
|
-
if
|
29
|
+
print "subcommand:"
|
30
|
+
if subcommand
|
31
31
|
puts
|
32
|
-
indent {
|
32
|
+
indent { subcommand.dump }
|
33
33
|
else
|
34
34
|
puts "nil"
|
35
35
|
end
|
data/lib/shellopts/ast/node.rb
CHANGED
@@ -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).
|
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:
|
data/lib/shellopts/compiler.rb
CHANGED
@@ -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.
|
11
|
+
# object. name is the name of the program and source is the
|
12
12
|
# option definition string
|
13
|
-
def self.compile(
|
14
|
-
|
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(
|
16
|
+
Compiler.new(name, source).call
|
17
17
|
end
|
18
18
|
|
19
19
|
# Service object for compiling an option definition string. Returns a
|
@@ -26,14 +26,14 @@ 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(
|
30
|
-
@
|
29
|
+
def initialize(name, source)
|
30
|
+
@name, @tokens = name, source.split(/\s+/).reject(&:empty?)
|
31
31
|
|
32
|
-
# @
|
32
|
+
# @subcommands_by_path is an hash from subcommand-path to Command or Program
|
33
33
|
# object. The top level Program object has nil as its path.
|
34
|
-
# @
|
35
|
-
# link sub-
|
36
|
-
@
|
34
|
+
# @subcommands_by_path is used to check for uniqueness of subcommands and to
|
35
|
+
# link sub-subcommands to their parents
|
36
|
+
@subcommands_by_path = {}
|
37
37
|
end
|
38
38
|
|
39
39
|
def call
|
@@ -49,30 +49,26 @@ module ShellOpts
|
|
49
49
|
# Returns the current token and advance to the next token
|
50
50
|
def next_token() @tokens.shift end
|
51
51
|
|
52
|
-
def error(msg) # Just a shorthand. Unrelated to ShellOpts.error
|
53
|
-
raise Compiler::Error.new(msg)
|
54
|
-
end
|
55
|
-
|
56
52
|
def compile_program
|
57
|
-
program = @
|
53
|
+
program = @subcommands_by_path[nil] = Grammar::Program.new(@name, compile_options)
|
58
54
|
while curr_token && curr_token != "--"
|
59
|
-
|
55
|
+
compile_subcommand
|
60
56
|
end
|
61
57
|
program.args.concat(@tokens[1..-1]) if curr_token
|
62
58
|
program
|
63
59
|
end
|
64
60
|
|
65
|
-
def
|
61
|
+
def compile_subcommand
|
66
62
|
path = curr_token[0..-2]
|
67
63
|
ident_list = compile_ident_list(path, ".")
|
68
64
|
parent_path = ident_list.size > 1 ? ident_list[0..-2].join(".") : nil
|
69
65
|
name = ident_list[-1]
|
70
66
|
|
71
|
-
parent = @
|
72
|
-
|
73
|
-
!@
|
67
|
+
parent = @subcommands_by_path[parent_path] or
|
68
|
+
raise Compiler::Error, "No such subcommand: #{parent_path.inspect}"
|
69
|
+
!@subcommands_by_path.key?(path) or raise Compiler::Error, "Duplicate subcommand: #{path.inspect}"
|
74
70
|
next_token
|
75
|
-
@
|
71
|
+
@subcommands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
|
76
72
|
end
|
77
73
|
|
78
74
|
def compile_options
|
@@ -81,7 +77,7 @@ module ShellOpts
|
|
81
77
|
option_list << compile_option
|
82
78
|
end
|
83
79
|
dup = option_list.map(&:names).flatten.find_dup and
|
84
|
-
|
80
|
+
raise Compiler::Error, "Duplicate option name: #{dup.inspect}"
|
85
81
|
option_list
|
86
82
|
end
|
87
83
|
|
@@ -102,7 +98,7 @@ module ShellOpts
|
|
102
98
|
long_names = []
|
103
99
|
ident_list = compile_ident_list(names, ",")
|
104
100
|
(dup = ident_list.find_dup).nil? or
|
105
|
-
|
101
|
+
raise Compiler::Error, "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
|
106
102
|
ident_list.each { |ident|
|
107
103
|
if ident.size == 1
|
108
104
|
short_names << "-#{ident}"
|
@@ -115,13 +111,15 @@ module ShellOpts
|
|
115
111
|
Grammar::Option.new(short_names, long_names, flags, label)
|
116
112
|
end
|
117
113
|
|
118
|
-
# Compile list of option names or a
|
114
|
+
# Compile list of option names or a subcommand path
|
119
115
|
def compile_ident_list(ident_list_str, sep)
|
120
116
|
ident_list_str.split(sep, -1).map { |str|
|
121
|
-
!str.empty? or
|
122
|
-
|
117
|
+
!str.empty? or
|
118
|
+
raise Compiler::Error, "Empty identifier in #{curr_token.inspect}"
|
119
|
+
!str.start_with?("-") or
|
120
|
+
raise Compiler::Error, "Identifier can't start with '-' in #{curr_token.inspect}"
|
123
121
|
str !~ /([^\w\d#{sep}-])/ or
|
124
|
-
|
122
|
+
raise Compiler::Error, "Illegal character #{$1.inspect} in #{curr_token.inspect}"
|
125
123
|
str
|
126
124
|
}
|
127
125
|
end
|
@@ -11,32 +11,50 @@ module ShellOpts
|
|
11
11
|
# Name of command (String). Name doesn't include the exclamation point ('!')
|
12
12
|
attr_reader :name
|
13
13
|
|
14
|
-
#
|
15
|
-
|
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
|
23
19
|
|
24
20
|
# List of commands in declaration order
|
25
|
-
attr_reader :
|
21
|
+
attr_reader :subcommand_list
|
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 subcommands()
|
35
|
+
@subcommand_multihash ||= @subcommand_list.flat_map { |subcommand|
|
36
|
+
subcommand.identifiers.map { |name| [name, subcommand] }
|
37
|
+
}.to_h
|
38
|
+
end
|
26
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
|
30
43
|
def initialize(parent, name, option_list)
|
31
|
-
super("#{name}!".to_sym)
|
32
|
-
@name = name
|
44
|
+
super("#{name}!".to_sym, name)
|
33
45
|
parent.attach(self) if parent
|
34
46
|
@option_list = option_list
|
35
|
-
@
|
36
|
-
@commands = {}
|
37
|
-
@command_list = []
|
47
|
+
@subcommand_list = []
|
38
48
|
end
|
39
49
|
|
50
|
+
# Return key for the identifier
|
51
|
+
def identifier2key(ident)
|
52
|
+
options[ident]&.key || subcommands[ident]&.key
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return list of identifiers for the command
|
56
|
+
def identifiers() [key, name] end
|
57
|
+
|
40
58
|
# :nocov:
|
41
59
|
def dump(&block)
|
42
60
|
puts "#{key.inspect}"
|
@@ -46,17 +64,16 @@ module ShellOpts
|
|
46
64
|
yield if block_given?
|
47
65
|
puts "options:"
|
48
66
|
indent { option_list.each { |opt| opt.dump } }
|
49
|
-
puts "
|
50
|
-
indent {
|
67
|
+
puts "subcommands: "
|
68
|
+
indent { subcommand_list.each { |cmd| cmd.dump } }
|
51
69
|
}
|
52
70
|
end
|
53
71
|
# :nocov:
|
54
72
|
|
55
73
|
protected
|
56
|
-
def attach(
|
57
|
-
|
58
|
-
@
|
59
|
-
@command_list << command
|
74
|
+
def attach(subcommand)
|
75
|
+
subcommand.instance_variable_set(:@parent, self)
|
76
|
+
@subcommand_list << subcommand
|
60
77
|
end
|
61
78
|
end
|
62
79
|
end
|
@@ -10,14 +10,22 @@ module ShellOpts
|
|
10
10
|
# Key (Symbol) of node. Unique within the enclosing command
|
11
11
|
attr_reader :key
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
# Name of node. The name of an option is without the prefixed '-' or
|
14
|
+
# '--', the name of a command is without the suffixed '!'. Note that name
|
15
|
+
# collisions can happen between options and commands names
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
def initialize(key, name)
|
19
|
+
@key, @name = key, name
|
15
20
|
end
|
16
21
|
|
17
22
|
# :nocov:
|
18
23
|
def dump(&block)
|
19
24
|
puts key.inspect
|
20
|
-
indent {
|
25
|
+
indent {
|
26
|
+
puts "name: #{name.inspect}"
|
27
|
+
yield if block_given?
|
28
|
+
}
|
21
29
|
end
|
22
30
|
# :nocov:
|
23
31
|
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,9 @@ 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
|
-
|
29
|
+
@key_name = long_names.first || short_names.first
|
30
|
+
name = @key_name.sub(/^-+/, "")
|
31
|
+
super(name.to_sym, name)
|
27
32
|
@short_names, @long_names = short_names, long_names
|
28
33
|
@flags = flags.map { |flag| [flag, true] }.to_h
|
29
34
|
@label = label
|
@@ -32,6 +37,12 @@ module ShellOpts
|
|
32
37
|
# Array of option names with short names first and then the long names
|
33
38
|
def names() @short_names + @long_names end
|
34
39
|
|
40
|
+
# Array of names and the key
|
41
|
+
def identifiers() names + [key] end
|
42
|
+
|
43
|
+
# Return true if +ident+ is equal to any name or to key
|
44
|
+
def match?(ident) names.include?(ident) || ident == key end
|
45
|
+
|
35
46
|
# Flag query methods. Returns true if the flag is present and otherwise nil
|
36
47
|
def repeated?() @flags[:repeated] || false end
|
37
48
|
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
|
-
|
20
|
+
subcommand_list.map { |cmd| render_subcommand(cmd) } +
|
21
21
|
args
|
22
22
|
).flatten.join(" ")
|
23
23
|
end
|
@@ -32,9 +32,9 @@ module ShellOpts
|
|
32
32
|
# :nocov:
|
33
33
|
|
34
34
|
private
|
35
|
-
def
|
36
|
-
[
|
37
|
-
|
35
|
+
def render_subcommand(subcommand)
|
36
|
+
[subcommand.name] + render_options(subcommand.option_list) +
|
37
|
+
subcommand.subcommand_list.map { |cmd| render_subcommand(cmd) }.flatten
|
38
38
|
end
|
39
39
|
|
40
40
|
def render_options(options)
|
@@ -0,0 +1,236 @@
|
|
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
|
+
# Parent node. nil for the top-level Program object
|
17
|
+
attr_reader :parent
|
18
|
+
|
19
|
+
# Unique key (within context) for the option or command. nil for the
|
20
|
+
# top-level Program object
|
21
|
+
attr_reader :key
|
22
|
+
|
23
|
+
# Name of command or option as used on the command line
|
24
|
+
attr_reader :name
|
25
|
+
|
26
|
+
# Value of node. This can be a simple value (String, Integer, or Float),
|
27
|
+
# an Array of values, or a Idr::Command object. Note that the value of a
|
28
|
+
# Command object is the object itself
|
29
|
+
#
|
30
|
+
# Repeated options are implemented as an Array with one element for each
|
31
|
+
# use of the option. The element is nil if the option doesn't take
|
32
|
+
# arguments or if an optional argument is missing.
|
33
|
+
attr_reader :value
|
34
|
+
|
35
|
+
# The top-level Program object
|
36
|
+
def program() @program ||= (parent&.program || self) end
|
37
|
+
|
38
|
+
protected
|
39
|
+
# Copy arguments into instance variables
|
40
|
+
def initialize(parent, ast, key, name, value)
|
41
|
+
@parent, @ast, @key, @name, @value = parent, ast, key, name, value
|
42
|
+
end
|
43
|
+
|
44
|
+
# The AST node for this Idr object
|
45
|
+
attr_reader :ast
|
46
|
+
|
47
|
+
# Shorthand to the grammar node for this Idr object
|
48
|
+
def grammar() @ast.grammar end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Base class for Options
|
52
|
+
class Option < Node
|
53
|
+
end
|
54
|
+
|
55
|
+
class SimpleOption < Option
|
56
|
+
protected
|
57
|
+
# Initialize with defauls from the Ast. +value+ is set to true if option
|
58
|
+
# doesn't take an argument
|
59
|
+
def initialize(parent, ast)
|
60
|
+
value = ast.grammar.argument? ? ast.value : true
|
61
|
+
super(parent, ast, ast.key, ast.name, value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# An OptionGroup models repeated options collapsed into a single key. The
|
66
|
+
# name of the group should be set to the name of the key (eg. '--all' if
|
67
|
+
# the key is :all)
|
68
|
+
class OptionGroup < Option
|
69
|
+
# Array of names of the options
|
70
|
+
attr_reader :names
|
71
|
+
|
72
|
+
# Array of values of the options
|
73
|
+
alias :values :value
|
74
|
+
|
75
|
+
# Name is set to the key name and value to an array of option values
|
76
|
+
def initialize(parent, key, name, options)
|
77
|
+
@names = options.map(&:name)
|
78
|
+
super(parent, nil, key, name, options.map(&:value))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Command < Node
|
83
|
+
# Hash from key to options with repeated option_list collapsed into a
|
84
|
+
# option group. It also include an entry for the subcommand. Options are
|
85
|
+
# ordered by first use on the command line. The command entry will always
|
86
|
+
# be last
|
87
|
+
attr_reader :options
|
88
|
+
|
89
|
+
# List of command line options in the same order as on the command line
|
90
|
+
attr_reader :option_list
|
91
|
+
|
92
|
+
# Subcommand object. Possibly nil
|
93
|
+
attr_reader :subcommand
|
94
|
+
|
95
|
+
# True if ident is declared
|
96
|
+
def declared?(ident) option?(ident) || subcommand?(ident) end
|
97
|
+
|
98
|
+
# True if ident is declared as an option
|
99
|
+
def option?(ident) grammar.options.key?(ident) end
|
100
|
+
|
101
|
+
# True if ident is declared as a command
|
102
|
+
def subcommand?(ident) grammar.subcommands.key?(ident) end
|
103
|
+
|
104
|
+
# True if ident is present
|
105
|
+
def key?(ident)
|
106
|
+
declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
|
107
|
+
key = grammar.identifier2key(ident)
|
108
|
+
@options.key?(key)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Value of ident. Repeated options are collapsed into an OptionGroup object
|
112
|
+
def [](ident)
|
113
|
+
declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
|
114
|
+
key = grammar.identifier2key(ident)
|
115
|
+
if @options.key?(key)
|
116
|
+
@options[key].value
|
117
|
+
elsif option?(key)
|
118
|
+
false
|
119
|
+
else
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Apply defaults recursively. Values can be lambdas that will be evaluated to
|
125
|
+
# get the default value. TODO
|
126
|
+
def apply(defaults = {}) raise InternalError, "Not implemented" end
|
127
|
+
|
128
|
+
# Return options and command as an array
|
129
|
+
def to_a() @ast.values end
|
130
|
+
|
131
|
+
# Return options and command as a hash. The hash also define the
|
132
|
+
# singleton method #subcommand that returns the key of the subcommand
|
133
|
+
#
|
134
|
+
# +key_type+ controls the type of keys used: +:key+ (the default) use the
|
135
|
+
# symbolic key, +:name+ use #name. Note that using +:name+ can cause
|
136
|
+
# name collisions between option and command names
|
137
|
+
#
|
138
|
+
# +aliases+ maps from key to replacement key (which could be any object).
|
139
|
+
# +aliases+ can be used to avoid name collisions between options and
|
140
|
+
# commands when using key_format: :name
|
141
|
+
#
|
142
|
+
# IDEA: Add a singleton methods to the hash with #name, #usage, etc.
|
143
|
+
#
|
144
|
+
def to_h(key_type: ::ShellOpts.default_key_type, aliases: {})
|
145
|
+
keys = map_keys(key_type, aliases)
|
146
|
+
value = {}
|
147
|
+
value.define_singleton_method(:subcommand) { nil }
|
148
|
+
options.values.each { |opt| # includes subcommand
|
149
|
+
key = keys[opt.key]
|
150
|
+
case opt
|
151
|
+
when Option
|
152
|
+
value[key] = opt.value
|
153
|
+
when Command
|
154
|
+
value[key] = opt.value.to_h(key_type: key_type, aliases: aliases[opt.key] || {})
|
155
|
+
value.define_singleton_method(:subcommand) { key } # Redefine
|
156
|
+
else
|
157
|
+
# :nocov:
|
158
|
+
raise InternalError, "Oops"
|
159
|
+
# :nocov:
|
160
|
+
end
|
161
|
+
}
|
162
|
+
value
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return options and command as a struct
|
166
|
+
def to_struct(key_type: ::ShellOpts.default_key_type, aliases: {})
|
167
|
+
OptionStruct.new(self, key_type, aliases)
|
168
|
+
end
|
169
|
+
|
170
|
+
protected
|
171
|
+
# Initialize an Idr::Command object and all dependent objects
|
172
|
+
def initialize(parent, ast)
|
173
|
+
super(parent, ast, ast.key, ast.name, self)
|
174
|
+
@option_list = ast.options.map { |node| SimpleOption.new(self, node) }
|
175
|
+
@subcommand = Command.new(self, ast.subcommand) if ast.subcommand
|
176
|
+
@options = @option_list.group_by { |option| option.key }.map { |key, option_list|
|
177
|
+
option =
|
178
|
+
if ast.grammar.options[key].repeated?
|
179
|
+
OptionGroup.new(self, key, ast.grammar.options[key].key_name, option_list)
|
180
|
+
else
|
181
|
+
option_list.first
|
182
|
+
end
|
183
|
+
[key, option]
|
184
|
+
}.to_h
|
185
|
+
@options[subcommand.key] = @subcommand if @subcommand
|
186
|
+
end
|
187
|
+
|
188
|
+
# Internal-key to used-key map. Checks for reserved words and
|
189
|
+
# name-collisions
|
190
|
+
def map_keys(key_type, aliases, reserved_words = [])
|
191
|
+
keys = {}
|
192
|
+
used_keys = {}
|
193
|
+
(grammar.option_list + grammar.subcommand_list).each { |node|
|
194
|
+
internal_key = node.key
|
195
|
+
key = aliases[internal_key] || (key_type == :name ? node.name.to_sym : internal_key)
|
196
|
+
!reserved_words.include?(key) or
|
197
|
+
raise ::ShellOpts::ConversionError, "'#{key}' is a reserved word"
|
198
|
+
!used_keys.key?(key) or
|
199
|
+
raise ::ShellOpts::ConversionError, "Name collision between '--#{key}' and '#{key}!'"
|
200
|
+
keys[internal_key] = key
|
201
|
+
used_keys[key] = true
|
202
|
+
}
|
203
|
+
keys
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class Program < Command
|
208
|
+
# Name of program
|
209
|
+
def name() @shellopts.name end
|
210
|
+
def name=(name) @shellopts.name = name end
|
211
|
+
|
212
|
+
# Usage string
|
213
|
+
def usage() @shellopts.usage end
|
214
|
+
def usage=(usage) @shellopts.usage = usage end
|
215
|
+
|
216
|
+
# #key is nil for the top-level Program object
|
217
|
+
def key() nil end
|
218
|
+
|
219
|
+
# Remaining command line arguments
|
220
|
+
def args() @shellopts.args end
|
221
|
+
|
222
|
+
# Initialize the top-level Idr::Program object
|
223
|
+
def initialize(shellopts)
|
224
|
+
@shellopts = shellopts
|
225
|
+
super(nil, shellopts.ast)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Emit error message and a usage description before exiting with status 1
|
229
|
+
def error(*args) @shellopts.error(*error_messages) end
|
230
|
+
|
231
|
+
# Emit error message before exiting with status 1
|
232
|
+
def fail(*args) @shellopts.fail(*error_messages) end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|