JACOP 1.0.1
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 +7 -0
- data/lib/jacop/exception.rb +7 -0
- data/lib/jacop/node.rb +171 -0
- data/lib/jacop/node_builder.rb +88 -0
- data/lib/jacop/option.rb +147 -0
- data/lib/jacop/option_builder.rb +68 -0
- data/lib/jacop/version.rb +7 -0
- data/lib/jacop.rb +49 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cc06d2415d5d58cd68dae44da601be5e14a7bff1afed2fb29dd3720506ba704e
|
|
4
|
+
data.tar.gz: 205de8de785654fe05959899cfc4be4f14ab0c6fedb851db7fc5a6dcd1dac5d8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 15370f5420a1eb3edc5b4ff7e7fadf8b245683088cad3f6cd936f3a394eaa45f5945c2543d3cde2b6f0d4c7f1ebd95666b19da27b0a19f269f2b1b73277ac4b7
|
|
7
|
+
data.tar.gz: 1b5fc247e1be74fe3e3d37854c7d3af836011fe2bfabb87c810be8f3d969fcd9ee82f328c0cf504cedb81188f5254349934cb1e678678d66a99a251c97ab4107
|
data/lib/jacop/node.rb
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require_relative 'option'
|
|
5
|
+
require_relative 'exception'
|
|
6
|
+
|
|
7
|
+
module JACOP
|
|
8
|
+
class Node
|
|
9
|
+
attr_reader :options
|
|
10
|
+
attr_reader :name
|
|
11
|
+
attr_reader :nodes
|
|
12
|
+
attr_reader :hidden
|
|
13
|
+
|
|
14
|
+
def initialize(name:, parent: nil, params:)
|
|
15
|
+
@name = name
|
|
16
|
+
@parent = parent
|
|
17
|
+
@banner = params[:banner]
|
|
18
|
+
@notes = params[:notes]
|
|
19
|
+
@version = params[:version]
|
|
20
|
+
@desc = params[:desc]
|
|
21
|
+
@call = params[:call]
|
|
22
|
+
raise DefinitionError, "#{@call} is not call-able" if @call and !@call.respond_to?(:call)
|
|
23
|
+
|
|
24
|
+
@hidden = params[:hidden]
|
|
25
|
+
@options = []
|
|
26
|
+
@nodes = []
|
|
27
|
+
params[:options]&.each {|h| @options << Option.new(command: self, option: h[:name], params: h) } || []
|
|
28
|
+
@nodes = params[:nodes]&.map { |h| Node.new(name: h[:name] , parent: self, params: h)} || []
|
|
29
|
+
|
|
30
|
+
# Default 'help' and 'version' options get added automatically
|
|
31
|
+
@options << Option.new(option: 'help', command: self, params: {short: 'h', desc: "Show help screen", type: :switch})
|
|
32
|
+
@options << Option.new(option: 'version', command: self, params: {short: 'v', desc: "Show version", type: :switch}) if @version
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse(cmdline)
|
|
36
|
+
case cmdline
|
|
37
|
+
when String then
|
|
38
|
+
cmdline = cmdline.shellsplit
|
|
39
|
+
when NilClass then
|
|
40
|
+
cmdline = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
usage if cmdline.empty? and @call.nil?
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
token = cmdline.shift
|
|
47
|
+
if token && !token.empty? && token !~ /^-{1,2}/ then
|
|
48
|
+
node = @nodes.find {|n| n.name == token}
|
|
49
|
+
raise ParserError, "Unknown command '#{token}'" unless node
|
|
50
|
+
|
|
51
|
+
node.parse(cmdline)
|
|
52
|
+
else
|
|
53
|
+
while token
|
|
54
|
+
is_short = token.match(/^-[[:alnum:]]/) != nil
|
|
55
|
+
t = token.sub(/^-{1,2}/, '')
|
|
56
|
+
opt = @options.find { |o| is_short ? o.short == t : o.option == t }
|
|
57
|
+
raise ParserError, "Unknown option '#{token}'" unless opt
|
|
58
|
+
opt.parse(cmdline, @options.select {|o| !o.value.nil?})
|
|
59
|
+
token = cmdline.shift
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
usage if @options.find {|o| o.option == 'help'}&.value
|
|
63
|
+
version if @options.find {|o| o.option == 'version'}&.value
|
|
64
|
+
@options.select {|o| o.required }.each {|o| o.validate_required }
|
|
65
|
+
|
|
66
|
+
execute if cmdline.empty?
|
|
67
|
+
end
|
|
68
|
+
rescue ParserError => e
|
|
69
|
+
usage(1, e.message)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def request_short(option, short)
|
|
74
|
+
noauto = !short.nil?
|
|
75
|
+
shorts = @options.map {|o| o.short}
|
|
76
|
+
|
|
77
|
+
c = option.downcase.scan(/[a-z]/).each
|
|
78
|
+
short = c.peek unless noauto
|
|
79
|
+
loop do
|
|
80
|
+
if shorts.member?(short) then
|
|
81
|
+
if noauto then
|
|
82
|
+
short = nil
|
|
83
|
+
break
|
|
84
|
+
else
|
|
85
|
+
begin
|
|
86
|
+
short = /[a-z]/.match?(short) ? short.upcase : c.next
|
|
87
|
+
rescue StopIteration
|
|
88
|
+
short = nil
|
|
89
|
+
break
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
break
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return short
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def usage_snippet(width)
|
|
101
|
+
" %-#{width}.#{width}s%s%s" % [@name + (@nodes.select {|n| !n.hidden}.empty? ? '' : '>'), " " * 8, @desc]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_s
|
|
105
|
+
"%s%s%s" % [@parent, @parent ? ' ' : nil, @name]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def banner
|
|
109
|
+
@banner || @parent.banner
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def execute
|
|
115
|
+
return unless @call
|
|
116
|
+
|
|
117
|
+
options = @options.select {|o| !o.value.nil?}.map { |o| {o.option => o.value} }.reduce(&:merge) || {}
|
|
118
|
+
|
|
119
|
+
@call.call(options)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def version
|
|
123
|
+
puts "Version #{@version}"
|
|
124
|
+
exit(0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def usage(exitcode = 0, misusage = nil)
|
|
128
|
+
puts misusage if misusage
|
|
129
|
+
|
|
130
|
+
if banner then
|
|
131
|
+
puts banner
|
|
132
|
+
puts
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts @desc if @desc and !@desc.empty?
|
|
136
|
+
nodes = @nodes.select {|n| !n.hidden }
|
|
137
|
+
|
|
138
|
+
usage_str = +'General usage: '
|
|
139
|
+
usage_str << File.basename($PROGRAM_NAME)
|
|
140
|
+
usage_str << "#{self}"
|
|
141
|
+
usage_str << ' <command>' unless nodes.empty?
|
|
142
|
+
usage_str << ' %s<options>%s' % (@options.any? {|o| o.required } ? [nil, nil] : ['[', ']']) unless @options.empty?
|
|
143
|
+
|
|
144
|
+
puts usage_str
|
|
145
|
+
puts
|
|
146
|
+
|
|
147
|
+
width = (nodes.map {|n| n.name.size + 1 } + @options.map {|o| o.usage_str.size}).max
|
|
148
|
+
|
|
149
|
+
if !nodes.empty?
|
|
150
|
+
puts 'Commands:'
|
|
151
|
+
nodes.each { |m| puts m.usage_snippet(width) }
|
|
152
|
+
puts
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
options = @options.select {|o| !o.hidden }
|
|
156
|
+
if !options.empty?
|
|
157
|
+
puts 'Options:'
|
|
158
|
+
options.each { |o| puts o.usage_snippet(width) }
|
|
159
|
+
puts
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if @notes then
|
|
163
|
+
puts "Notes:"
|
|
164
|
+
@notes.each_line {|line| puts " #{line}"}
|
|
165
|
+
puts
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
exit(exitcode)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'option_builder'
|
|
4
|
+
require_relative 'option'
|
|
5
|
+
require_relative 'exception'
|
|
6
|
+
|
|
7
|
+
module JACOP
|
|
8
|
+
##
|
|
9
|
+
# Internal class, not to be used directly.
|
|
10
|
+
# The methods of this class are the DSL statements for defining a command.
|
|
11
|
+
# They become available in a `configure` or `command` block after including the `JACOP` mixin.
|
|
12
|
+
class NodeBuilder
|
|
13
|
+
attr_reader :params # :nodoc:
|
|
14
|
+
|
|
15
|
+
def initialize(name=nil) # :nodoc:
|
|
16
|
+
@params = {
|
|
17
|
+
name: name,
|
|
18
|
+
nodes: [],
|
|
19
|
+
options: []
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Define a new custom data-type for options.
|
|
25
|
+
# The data-type will be available globally, not just in the blocks where it was defined.
|
|
26
|
+
def add_type(type:, klass:, &block)
|
|
27
|
+
raise DefinitionError, "add_type needs a block" unless block_given?
|
|
28
|
+
|
|
29
|
+
Option.add_type(type: type, klass: klass, &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Set the banner in the help/usage screen
|
|
34
|
+
def banner(banner)
|
|
35
|
+
@params[:banner] = banner
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Set notes shown in the help/usage screen
|
|
40
|
+
def notes(notes)
|
|
41
|
+
@params[:notes] = notes
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Set the version. If set, the `--version` option will be created automatically.
|
|
46
|
+
def version(version)
|
|
47
|
+
@params[:version] = version
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Set the description for the command in the help/usage screen
|
|
52
|
+
def desc(desc)
|
|
53
|
+
@params[:desc] = desc
|
|
54
|
+
end
|
|
55
|
+
alias_method :description, :desc
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Define which method or block should be called when this command is invoked.
|
|
59
|
+
# The given argument will usually be either a `Proc` or `Method` object, or a block.
|
|
60
|
+
# In any case, it must respond to the `#call` method. The `callable` will receive a
|
|
61
|
+
# single argument containing a hash of all supplied command-line arguments.
|
|
62
|
+
def call(callable = nil, &block)
|
|
63
|
+
@params[:call] = block_given? ? block : callable
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Set the command to be hidden. It will not be shown in the help/usage screen.
|
|
68
|
+
def hidden(hidden = true)
|
|
69
|
+
@params[:hidden] = hidden
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Define a new (sub-) command
|
|
74
|
+
def command(name, &block)
|
|
75
|
+
builder = NodeBuilder.new(name)
|
|
76
|
+
builder.instance_eval(&block)
|
|
77
|
+
@params[:nodes] << builder.params
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Define a new option for this command
|
|
82
|
+
def option(name, &block)
|
|
83
|
+
builder = OptionBuilder.new(name)
|
|
84
|
+
builder.instance_eval(&block)
|
|
85
|
+
@params[:options] << builder.params
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/jacop/option.rb
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require_relative 'exception'
|
|
5
|
+
|
|
6
|
+
module JACOP
|
|
7
|
+
class Option
|
|
8
|
+
|
|
9
|
+
@@valid_types = {
|
|
10
|
+
switch: { klass: [NilClass, TrueClass, FalseClass], converter: Proc.new {|val| !!val } },
|
|
11
|
+
string: { klass: String, converter: Proc.new {|val| val.to_s } },
|
|
12
|
+
int: { klass: Integer, converter: Proc.new {|val| val.to_i } },
|
|
13
|
+
percent: { klass: Float, converter: Proc.new {|val| val.to_f / 100 } },
|
|
14
|
+
date: { klass: Date, converter: Proc.new {|val| Date.parse(val) } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def self.valid_types; @@valid_types; end
|
|
18
|
+
|
|
19
|
+
def self.add_type(type:, klass:, &block)
|
|
20
|
+
@@valid_types[type] = { klass: klass, converter: block }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :option
|
|
24
|
+
attr_reader :value
|
|
25
|
+
attr_reader :short
|
|
26
|
+
attr_reader :required
|
|
27
|
+
attr_reader :hidden
|
|
28
|
+
|
|
29
|
+
def initialize(command:, option:, params:)
|
|
30
|
+
@option = option
|
|
31
|
+
@desc = params[:desc]
|
|
32
|
+
@short = command.request_short(@option, params[:short])
|
|
33
|
+
@required = params[:required]
|
|
34
|
+
@conflicts = @required ? [] : params[:conflicts] || []
|
|
35
|
+
@hidden = params[:hidden]
|
|
36
|
+
@type = validate_type(params[:type])
|
|
37
|
+
@multi = params[:multi]
|
|
38
|
+
@default = validate_value(params[:default], multi: @multi, allow_nil: true)
|
|
39
|
+
@value = @default
|
|
40
|
+
|
|
41
|
+
validate
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
@option
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def usage_str
|
|
49
|
+
s = +"--#{@option}"
|
|
50
|
+
s << "|-#{@short}" if !(@short.nil? or short.empty?)
|
|
51
|
+
s << " <%s>%s" % [@type.upcase, @multi ? "[, ...]" : nil] if @type != :switch
|
|
52
|
+
|
|
53
|
+
return s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def usage_snippet(width)
|
|
57
|
+
" %-#{width}.#{width}s%s%s%s" % [usage_str,
|
|
58
|
+
" " * 8,
|
|
59
|
+
@desc,
|
|
60
|
+
@default.nil? ?
|
|
61
|
+
nil :
|
|
62
|
+
" (Default: #{@default})"
|
|
63
|
+
]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def conflicts(other)
|
|
67
|
+
return false if self == other
|
|
68
|
+
|
|
69
|
+
@conflicts.member?(other.option)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse(cmdline, options)
|
|
73
|
+
if options.any? { |o| conflicts(o) || o.conflicts(self)} then
|
|
74
|
+
raise ParserError, "Option '#{option}' conflicts with another option"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
val = []
|
|
78
|
+
while !cmdline.empty? && cmdline[0] !~ /^-{1,2}/ do
|
|
79
|
+
raise ParserError, "Option '#{option}' expects a single argument" unless val.empty? or @multi
|
|
80
|
+
val << (@multi ? cmdline.shift.split(/,/) : cmdline.shift)
|
|
81
|
+
val.flatten!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if @type == :switch then
|
|
85
|
+
@value = true
|
|
86
|
+
elsif @multi then
|
|
87
|
+
@value = val.map { |x| parse_value(x.strip) }
|
|
88
|
+
else
|
|
89
|
+
@value = parse_value(val.first)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_required
|
|
94
|
+
raise ParserError, "Required option '#{@option}' is missing" if @required and !@value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def parse_value(value)
|
|
100
|
+
begin
|
|
101
|
+
value = @@valid_types.dig(@type, :converter).call(value)
|
|
102
|
+
rescue => e
|
|
103
|
+
raise ParserError, "Error while coercing value '#{value}' into type '#{@type}': #{e.message}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_type(type)
|
|
110
|
+
if type then
|
|
111
|
+
t = type.to_sym
|
|
112
|
+
return t if @@valid_types.keys.member?(t)
|
|
113
|
+
|
|
114
|
+
raise DefinitionError, "Unknown type '#{t}'. Valid types are: #{@@valid_types.keys.join(', ')}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
return t
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate
|
|
121
|
+
raise ParserError, "'short' options are expected to be single characters" if @short and @short.size > 1
|
|
122
|
+
raise DefinitionError, "Settings 'required' and 'conflicts' for option '#{@option} are mutually exclusive" if @required and !@conflicts.empty?
|
|
123
|
+
|
|
124
|
+
if @conflicts && !@conflicts.is_a?(Array) then
|
|
125
|
+
@conflicts = [@conflicts]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_value(value, multi: @multi, allow_nil: false)
|
|
130
|
+
return if !value and allow_nil
|
|
131
|
+
|
|
132
|
+
if multi then
|
|
133
|
+
if value.is_a? Array then
|
|
134
|
+
value.each { |x| validate_value(x, multi: false) }
|
|
135
|
+
else
|
|
136
|
+
raise ParserError, "Array expected for option '#{@option}'"
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
type_mismatch = value.class != @@valid_types.dig(@type, :klass)
|
|
140
|
+
raise ParserError, "Value '#{value}' for option '#{@option}' has wrong type, expected '#{@type}'" if type_mismatch
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return value
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JACOP
|
|
4
|
+
##
|
|
5
|
+
# Internal class, not to be used directly.
|
|
6
|
+
# The methods of this class are the DSL statements for defining an option for a command.
|
|
7
|
+
# They become available in an `option` block after including the `JACOP` mixin.
|
|
8
|
+
class OptionBuilder
|
|
9
|
+
attr_reader :params # :nodoc:
|
|
10
|
+
|
|
11
|
+
def initialize(name) # :nodoc:
|
|
12
|
+
@params = {
|
|
13
|
+
name: name
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Set a description for this option in the help/usage screen.
|
|
19
|
+
def desc(desc)
|
|
20
|
+
@params[:desc] = desc
|
|
21
|
+
end
|
|
22
|
+
alias_method :description, :desc
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Set the alternative single-character option for this option. If not given, it will be auto-generated.
|
|
26
|
+
# Auto-generation of the short option can be disabled by setting this to an empty string.
|
|
27
|
+
def short(short)
|
|
28
|
+
@params[:short] = short
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Define this option as being required.
|
|
33
|
+
# `required` and `conflicts` cannot both be used for the same option.
|
|
34
|
+
def required(required = true)
|
|
35
|
+
@params[:required] = required
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Define other options with which this option is mutually exclusive.
|
|
40
|
+
def conflicts(conflicts)
|
|
41
|
+
@params[:conflicts] = conflicts
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Set this option to be hidden. It will not be shown in the help/usage screen.
|
|
46
|
+
def hidden(hidden = true)
|
|
47
|
+
@params[:hidden] = hidden
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Set the data-type of this option's argument. This is a required setting.
|
|
52
|
+
def type(type)
|
|
53
|
+
@params[:type] = type
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Set a default value for this option's argument.
|
|
58
|
+
def default(default)
|
|
59
|
+
@params[:default] = default
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Define that this option can take multiple arguments, separated by commas.
|
|
64
|
+
def multi(multi = true)
|
|
65
|
+
@params[:multi] = multi
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/jacop.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './jacop/node_builder'
|
|
4
|
+
require_relative './jacop/node'
|
|
5
|
+
require_relative './jacop/exception'
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# JACOP is a DSL for building and parsing command-line commands and options.
|
|
9
|
+
# Include this mixin to make use of the DSL.
|
|
10
|
+
module JACOP
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Configure the top-level command level
|
|
14
|
+
def configure(&block)
|
|
15
|
+
builder = NodeBuilder.new
|
|
16
|
+
builder.instance_eval(&block)
|
|
17
|
+
self.root_node = builder.params
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Parse the provided command-line
|
|
22
|
+
def parse(cmdline)
|
|
23
|
+
raise DefinitionError, "#{self.class} has not been configured" if root_node.empty?
|
|
24
|
+
|
|
25
|
+
Node.new(name: root_node[:name], params: root_node).parse(cmdline)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def root_node
|
|
30
|
+
@root_node ||= {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def root_node=(val)
|
|
34
|
+
@root_node = merge_nodes(root_node, val)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def merge_nodes(node1, node2)
|
|
38
|
+
node1.merge(node2) do |key, this_val, other_val|
|
|
39
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
|
40
|
+
merge_nodes(this_val, other_val)
|
|
41
|
+
elsif this_val.is_a?(Array) && other_val.is_a?(Array)
|
|
42
|
+
this_val + other_val
|
|
43
|
+
else
|
|
44
|
+
this_val
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: JACOP
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Oliver Brakmann
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: JACOP is a DSL and command-line parser allowing for regular and Git-style
|
|
13
|
+
command syntax, featuring type-aware option parsing and automatic help screen generation.
|
|
14
|
+
email:
|
|
15
|
+
- oliver.brakmann@posteo.de
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/jacop.rb
|
|
21
|
+
- lib/jacop/exception.rb
|
|
22
|
+
- lib/jacop/node.rb
|
|
23
|
+
- lib/jacop/node_builder.rb
|
|
24
|
+
- lib/jacop/option.rb
|
|
25
|
+
- lib/jacop/option_builder.rb
|
|
26
|
+
- lib/jacop/version.rb
|
|
27
|
+
homepage: https://codeberg.org/obrakmann/jacop
|
|
28
|
+
licenses:
|
|
29
|
+
- EUPL-1.2
|
|
30
|
+
metadata: {}
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2.5.0
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.6.7
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Just Another COmmand-line Parser
|
|
48
|
+
test_files: []
|