lino 3.2.0.pre.4 → 3.2.0.pre.6
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/Gemfile.lock +95 -60
- data/Rakefile +1 -3
- data/lib/lino/builders/command_line.rb +59 -0
- data/lib/lino/builders/mixins/appliables.rb +27 -0
- data/lib/lino/builders/mixins/arguments.rb +38 -0
- data/lib/lino/builders/mixins/defaulting.rb +19 -0
- data/lib/lino/builders/mixins/environment_variables.rb +46 -0
- data/lib/lino/builders/mixins/executor.rb +24 -0
- data/lib/lino/builders/mixins/option_config.rb +50 -0
- data/lib/lino/builders/mixins/options.rb +118 -0
- data/lib/lino/builders/mixins/state_boundary.rb +22 -0
- data/lib/lino/builders/mixins/subcommands.rb +52 -0
- data/lib/lino/builders/mixins/validation.rb +21 -0
- data/lib/lino/builders/mixins/working_directory.rb +24 -0
- data/lib/lino/builders/subcommand.rb +47 -0
- data/lib/lino/builders.rb +9 -0
- data/lib/lino/errors/execution_error.rb +18 -0
- data/lib/lino/errors.rb +8 -0
- data/lib/lino/executors/childprocess.rb +63 -0
- data/lib/lino/executors/open4.rb +42 -0
- data/lib/lino/executors.rb +9 -0
- data/lib/lino/model/argument.rb +41 -0
- data/lib/lino/model/command_line.rb +117 -0
- data/lib/lino/model/environment_variable.rb +63 -0
- data/lib/lino/model/flag.rb +53 -0
- data/lib/lino/model/option.rb +73 -0
- data/lib/lino/model/subcommand.rb +52 -0
- data/lib/lino/model.rb +13 -0
- data/lib/lino/version.rb +1 -1
- data/lib/lino.rb +11 -2
- metadata +72 -10
- data/lib/lino/appliables.rb +0 -23
- data/lib/lino/command_line.rb +0 -28
- data/lib/lino/command_line_builder.rb +0 -225
- data/lib/lino/options.rb +0 -72
- data/lib/lino/subcommand_builder.rb +0 -49
- data/lib/lino/utilities.rb +0 -59
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'validation'
|
4
|
+
require_relative '../../model'
|
5
|
+
|
6
|
+
module Lino
|
7
|
+
module Builders
|
8
|
+
module Mixins
|
9
|
+
module Subcommands
|
10
|
+
include Validation
|
11
|
+
|
12
|
+
def initialize(state)
|
13
|
+
@subcommands = Hamster::Vector.new(state[:subcommands] || [])
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def with_subcommand(subcommand, &block)
|
18
|
+
return self if nil_or_empty?(subcommand)
|
19
|
+
|
20
|
+
with(
|
21
|
+
subcommands: @subcommands.add(
|
22
|
+
(block || ->(sub) { sub }).call(
|
23
|
+
Builders::Subcommand.for_subcommand(subcommand)
|
24
|
+
)
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_subcommands(subcommands, &)
|
30
|
+
return self if nil_or_empty?(subcommands)
|
31
|
+
|
32
|
+
without_block = subcommands[0...-1]
|
33
|
+
with_block = subcommands.last
|
34
|
+
|
35
|
+
without_block
|
36
|
+
.inject(self) { |s, sc| s.with_subcommand(sc) }
|
37
|
+
.with_subcommand(with_block, &)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def state
|
43
|
+
super.merge(subcommands: @subcommands)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_subcommands
|
47
|
+
@subcommands.map(&:build)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Builders
|
5
|
+
module Mixins
|
6
|
+
module Validation
|
7
|
+
def nil?(value)
|
8
|
+
value.nil?
|
9
|
+
end
|
10
|
+
|
11
|
+
def empty?(value)
|
12
|
+
value.respond_to?(:empty?) && value.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def nil_or_empty?(value)
|
16
|
+
nil?(value) || empty?(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Builders
|
5
|
+
module Mixins
|
6
|
+
module WorkingDirectory
|
7
|
+
def initialize(state)
|
8
|
+
@working_directory = state[:working_directory]
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_working_directory(working_directory)
|
13
|
+
with(working_directory:)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def state
|
19
|
+
super.merge(working_directory: @working_directory)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hamster'
|
4
|
+
|
5
|
+
require_relative '../model'
|
6
|
+
require_relative 'mixins/options'
|
7
|
+
require_relative 'mixins/appliables'
|
8
|
+
|
9
|
+
module Lino
|
10
|
+
module Builders
|
11
|
+
class Subcommand
|
12
|
+
include Mixins::Options
|
13
|
+
include Mixins::Appliables
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def for_subcommand(subcommand)
|
17
|
+
Builders::Subcommand.new(subcommand:)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(state)
|
22
|
+
@subcommand = state[:subcommand]
|
23
|
+
@options = Hamster::Vector.new(state[:options] || [])
|
24
|
+
end
|
25
|
+
|
26
|
+
def build
|
27
|
+
Model::Subcommand.new(
|
28
|
+
@subcommand,
|
29
|
+
options: build_options
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def with(replacements)
|
36
|
+
Builders::Subcommand.new(state.merge(replacements))
|
37
|
+
end
|
38
|
+
|
39
|
+
def state
|
40
|
+
{
|
41
|
+
subcommand: @subcommand,
|
42
|
+
options: @options
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Errors
|
5
|
+
class ExecutionError < StandardError
|
6
|
+
def initialize(
|
7
|
+
command_line = nil,
|
8
|
+
exit_code = nil,
|
9
|
+
cause = nil
|
10
|
+
)
|
11
|
+
@command_line = command_line
|
12
|
+
@exit_code = exit_code
|
13
|
+
@cause = cause
|
14
|
+
super('Failed while executing command line.')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/lino/errors.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'childprocess'
|
4
|
+
|
5
|
+
require_relative '../errors'
|
6
|
+
|
7
|
+
module Lino
|
8
|
+
module Executors
|
9
|
+
class Childprocess
|
10
|
+
def execute(command_line, opts = {})
|
11
|
+
process = ::ChildProcess.build(*command_line.array)
|
12
|
+
|
13
|
+
set_output_streams(process, opts)
|
14
|
+
set_working_directory(process, command_line.working_directory)
|
15
|
+
set_environment(process, command_line.environment_variables)
|
16
|
+
start_process(process, opts)
|
17
|
+
|
18
|
+
exit_code = process.wait
|
19
|
+
|
20
|
+
return unless exit_code != 0
|
21
|
+
|
22
|
+
raise Lino::Errors::ExecutionError.new(
|
23
|
+
command_line.string, exit_code
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
self.class == other.class
|
29
|
+
end
|
30
|
+
|
31
|
+
alias eql? ==
|
32
|
+
|
33
|
+
def hash
|
34
|
+
self.class.hash
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def start_process(process, opts)
|
40
|
+
process.duplex = true if opts[:stdin]
|
41
|
+
process.start
|
42
|
+
process.io.stdin.write(opts[:stdin]) if opts[:stdin]
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_output_streams(process, opts)
|
46
|
+
process.io.inherit!
|
47
|
+
process.io.stdout = opts[:stdout] if opts[:stdout]
|
48
|
+
process.io.stderr = opts[:stderr] if opts[:stderr]
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_working_directory(process, working_directory)
|
52
|
+
process.cwd = working_directory
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_environment(process, environment_variables)
|
56
|
+
environment_variables.each do |environment_variable|
|
57
|
+
process.environment[environment_variable.name] =
|
58
|
+
environment_variable.value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open4'
|
4
|
+
|
5
|
+
module Lino
|
6
|
+
module Executors
|
7
|
+
class Open4
|
8
|
+
def execute(command_line, opts = {})
|
9
|
+
opts = with_defaults(opts)
|
10
|
+
|
11
|
+
::Open4.spawn(
|
12
|
+
command_line.env,
|
13
|
+
*command_line.array,
|
14
|
+
stdin: opts[:stdin],
|
15
|
+
stdout: opts[:stdout],
|
16
|
+
stderr: opts[:stderr],
|
17
|
+
cwd: command_line.working_directory
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
self.class == other.class
|
23
|
+
end
|
24
|
+
|
25
|
+
alias eql? ==
|
26
|
+
|
27
|
+
def hash
|
28
|
+
self.class.hash
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def with_defaults(opts)
|
34
|
+
{
|
35
|
+
stdin: opts[:stdin] || '',
|
36
|
+
stdout: opts[:stdout] || $stdout,
|
37
|
+
stderr: opts[:stderr] || $stderr
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Model
|
5
|
+
class Argument
|
6
|
+
attr_reader :argument
|
7
|
+
|
8
|
+
def initialize(argument)
|
9
|
+
@argument = argument
|
10
|
+
end
|
11
|
+
|
12
|
+
def string
|
13
|
+
argument
|
14
|
+
end
|
15
|
+
alias to_s string
|
16
|
+
|
17
|
+
def array
|
18
|
+
[argument]
|
19
|
+
end
|
20
|
+
alias to_a string
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
self.class == other.class &&
|
24
|
+
state == other.state
|
25
|
+
end
|
26
|
+
alias eql? ==
|
27
|
+
|
28
|
+
def hash
|
29
|
+
[self.class, state].hash
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def state
|
35
|
+
[
|
36
|
+
@argument
|
37
|
+
]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Model
|
5
|
+
class CommandLine
|
6
|
+
COMPONENTS = [
|
7
|
+
%i[environment_variables],
|
8
|
+
%i[command],
|
9
|
+
%i[options after_command],
|
10
|
+
%i[subcommands],
|
11
|
+
%i[options after_subcommands],
|
12
|
+
%i[arguments],
|
13
|
+
%i[options after_arguments]
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
attr_reader :command,
|
17
|
+
:subcommands,
|
18
|
+
:options,
|
19
|
+
:arguments,
|
20
|
+
:environment_variables,
|
21
|
+
:executor,
|
22
|
+
:working_directory
|
23
|
+
|
24
|
+
def initialize(command, opts = {})
|
25
|
+
opts = with_defaults(opts)
|
26
|
+
@command = command
|
27
|
+
@subcommands = Hamster::Vector.new(opts[:subcommands])
|
28
|
+
@options = Hamster::Vector.new(opts[:options])
|
29
|
+
@arguments = Hamster::Vector.new(opts[:arguments])
|
30
|
+
@environment_variables =
|
31
|
+
Hamster::Vector.new(opts[:environment_variables])
|
32
|
+
@executor = opts[:executor]
|
33
|
+
@working_directory = opts[:working_directory]
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute(opts = {})
|
37
|
+
@executor.execute(self, opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
def env
|
41
|
+
@environment_variables.to_h(&:array)
|
42
|
+
end
|
43
|
+
|
44
|
+
def array
|
45
|
+
format_components(:array, COMPONENTS.drop(1)).flatten
|
46
|
+
end
|
47
|
+
|
48
|
+
alias to_a array
|
49
|
+
|
50
|
+
def string
|
51
|
+
format_components(:string, COMPONENTS).join(' ')
|
52
|
+
end
|
53
|
+
|
54
|
+
alias to_s string
|
55
|
+
|
56
|
+
def ==(other)
|
57
|
+
self.class == other.class && state == other.state
|
58
|
+
end
|
59
|
+
|
60
|
+
alias eql? ==
|
61
|
+
|
62
|
+
def hash
|
63
|
+
[self.class, state].hash
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def state
|
69
|
+
[
|
70
|
+
@command,
|
71
|
+
@subcommands,
|
72
|
+
@options,
|
73
|
+
@arguments,
|
74
|
+
@environment_variables,
|
75
|
+
@executor,
|
76
|
+
@working_directory
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def with_defaults(opts)
|
83
|
+
{
|
84
|
+
subcommands: opts.fetch(:subcommands, []),
|
85
|
+
options: opts.fetch(:options, []),
|
86
|
+
arguments: opts.fetch(:arguments, []),
|
87
|
+
environment_variables: opts.fetch(:environment_variables, []),
|
88
|
+
executor: opts.fetch(:executor, Executors::Childprocess.new),
|
89
|
+
working_directory: opts.fetch(:working_directory, nil)
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def format_components(format, paths)
|
94
|
+
paths
|
95
|
+
.collect { |p| components_at_path(formatted_components(format), p) }
|
96
|
+
.compact
|
97
|
+
.reject(&:empty?)
|
98
|
+
end
|
99
|
+
|
100
|
+
def formatted_components(format)
|
101
|
+
{
|
102
|
+
environment_variables: @environment_variables.map(&format),
|
103
|
+
command: @command,
|
104
|
+
options: @options
|
105
|
+
.group_by(&:placement)
|
106
|
+
.map { |p, o| [p, o.map(&format)] },
|
107
|
+
subcommands: @subcommands.map(&format),
|
108
|
+
arguments: @arguments.map(&format)
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def components_at_path(components, path)
|
113
|
+
path.inject(components) { |c, p| c && c[p] }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
module Lino
|
6
|
+
module Model
|
7
|
+
class EnvironmentVariable
|
8
|
+
attr_reader :name,
|
9
|
+
:value,
|
10
|
+
:quoting
|
11
|
+
|
12
|
+
def initialize(name, value, opts = {})
|
13
|
+
opts = with_defaults(opts)
|
14
|
+
@name = name
|
15
|
+
@value = value
|
16
|
+
@quoting = opts[:quoting]
|
17
|
+
end
|
18
|
+
|
19
|
+
def quoted_value
|
20
|
+
"#{quoting}#{value.to_s.gsub(quoting.to_s, "\\#{quoting}")}#{quoting}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def string
|
24
|
+
"#{name}=#{quoted_value}"
|
25
|
+
end
|
26
|
+
alias to_s string
|
27
|
+
|
28
|
+
def array
|
29
|
+
[name, value]
|
30
|
+
end
|
31
|
+
alias to_a array
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
self.class == other.class &&
|
35
|
+
state == other.state
|
36
|
+
end
|
37
|
+
|
38
|
+
alias eql? ==
|
39
|
+
|
40
|
+
def hash
|
41
|
+
[self.class, state].hash
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def state
|
47
|
+
[
|
48
|
+
@name,
|
49
|
+
@value,
|
50
|
+
@quoting
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def with_defaults(opts)
|
57
|
+
{
|
58
|
+
quoting: opts[:quoting] || '"'
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Model
|
5
|
+
class Flag
|
6
|
+
attr_reader :flag,
|
7
|
+
:placement
|
8
|
+
|
9
|
+
def initialize(flag, opts = {})
|
10
|
+
opts = with_defaults(opts)
|
11
|
+
@flag = flag
|
12
|
+
@placement = opts[:placement]
|
13
|
+
end
|
14
|
+
|
15
|
+
def string
|
16
|
+
flag
|
17
|
+
end
|
18
|
+
alias to_s string
|
19
|
+
|
20
|
+
def array
|
21
|
+
[flag]
|
22
|
+
end
|
23
|
+
alias to_a string
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
self.class == other.class &&
|
27
|
+
state == other.state
|
28
|
+
end
|
29
|
+
alias eql? ==
|
30
|
+
|
31
|
+
def hash
|
32
|
+
[self.class, state].hash
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def state
|
38
|
+
[
|
39
|
+
@flag,
|
40
|
+
@placement
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def with_defaults(opts)
|
47
|
+
{
|
48
|
+
placement: opts[:placement] || :after_command
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Model
|
5
|
+
class Option
|
6
|
+
attr_reader :option,
|
7
|
+
:value,
|
8
|
+
:separator,
|
9
|
+
:quoting,
|
10
|
+
:placement
|
11
|
+
|
12
|
+
def initialize(option, value, opts = {})
|
13
|
+
opts = with_defaults(opts)
|
14
|
+
@option = option
|
15
|
+
@value = value
|
16
|
+
@separator = opts[:separator]
|
17
|
+
@quoting = opts[:quoting]
|
18
|
+
@placement = opts[:placement]
|
19
|
+
end
|
20
|
+
|
21
|
+
def quoted_value
|
22
|
+
"#{quoting}#{value}#{quoting}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def string
|
26
|
+
"#{option}#{separator}#{quoted_value}"
|
27
|
+
end
|
28
|
+
alias to_s string
|
29
|
+
|
30
|
+
def array
|
31
|
+
if separator == ' '
|
32
|
+
[option, value]
|
33
|
+
else
|
34
|
+
["#{option}#{separator}#{value}"]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias to_a array
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
self.class == other.class &&
|
41
|
+
state == other.state
|
42
|
+
end
|
43
|
+
|
44
|
+
alias eql? ==
|
45
|
+
|
46
|
+
def hash
|
47
|
+
[self.class, state].hash
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def state
|
53
|
+
[
|
54
|
+
@option,
|
55
|
+
@value,
|
56
|
+
@separator,
|
57
|
+
@quoting,
|
58
|
+
@placement
|
59
|
+
]
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def with_defaults(opts)
|
65
|
+
{
|
66
|
+
separator: opts[:separator] || ' ',
|
67
|
+
quoting: opts[:quoting],
|
68
|
+
placement: opts[:placement] || :after_command
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lino
|
4
|
+
module Model
|
5
|
+
class Subcommand
|
6
|
+
attr_reader :subcommand, :options
|
7
|
+
|
8
|
+
def initialize(subcommand, opts = {})
|
9
|
+
opts = with_defaults(opts)
|
10
|
+
@subcommand = subcommand
|
11
|
+
@options = Hamster::Vector.new(opts[:options])
|
12
|
+
end
|
13
|
+
|
14
|
+
def string
|
15
|
+
[@subcommand, @options.map(&:string)].reject(&:empty?).join(' ')
|
16
|
+
end
|
17
|
+
alias to_s string
|
18
|
+
|
19
|
+
def array
|
20
|
+
[@subcommand, @options.map(&:array)].flatten
|
21
|
+
end
|
22
|
+
alias to_a array
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
self.class == other.class &&
|
26
|
+
state == other.state
|
27
|
+
end
|
28
|
+
alias eql? ==
|
29
|
+
|
30
|
+
def hash
|
31
|
+
[self.class, state].hash
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def state
|
37
|
+
[
|
38
|
+
@subcommand,
|
39
|
+
@options
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def with_defaults(opts)
|
46
|
+
{
|
47
|
+
options: opts.fetch(:options, [])
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|