dk 0.0.1 → 0.1.0

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.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +643 -1
  3. data/bin/dk +7 -0
  4. data/dk.gemspec +7 -3
  5. data/lib/dk/ansi.rb +98 -0
  6. data/lib/dk/cli.rb +173 -0
  7. data/lib/dk/config.rb +217 -0
  8. data/lib/dk/config_runner.rb +24 -0
  9. data/lib/dk/dk_runner.rb +13 -0
  10. data/lib/dk/dry_runner.rb +43 -0
  11. data/lib/dk/has_set_param.rb +42 -0
  12. data/lib/dk/has_ssh_opts.rb +36 -0
  13. data/lib/dk/has_the_runs.rb +23 -0
  14. data/lib/dk/has_the_stubs.rb +116 -0
  15. data/lib/dk/local.rb +84 -0
  16. data/lib/dk/null_logger.rb +13 -0
  17. data/lib/dk/remote.rb +132 -0
  18. data/lib/dk/runner.rb +202 -0
  19. data/lib/dk/task.rb +266 -0
  20. data/lib/dk/task_run.rb +17 -0
  21. data/lib/dk/test_runner.rb +54 -0
  22. data/lib/dk/tree_runner.rb +64 -0
  23. data/lib/dk/version.rb +1 -1
  24. data/lib/dk.rb +23 -1
  25. data/test/helper.rb +6 -1
  26. data/test/support/config/dk.rb +7 -0
  27. data/test/support/config/task_defs.rb +10 -0
  28. data/test/support/factory.rb +38 -0
  29. data/test/support/log/.gitkeep +0 -0
  30. data/test/system/has_the_stubs_tests.rb +355 -0
  31. data/test/system/runner_tests.rb +222 -0
  32. data/test/unit/ansi_tests.rb +40 -0
  33. data/test/unit/cli_tests.rb +317 -0
  34. data/test/unit/config_runner_tests.rb +60 -0
  35. data/test/unit/config_tests.rb +427 -0
  36. data/test/unit/dk_runner_tests.rb +34 -0
  37. data/test/unit/dk_tests.rb +49 -0
  38. data/test/unit/dry_runner_tests.rb +71 -0
  39. data/test/unit/has_set_param_tests.rb +46 -0
  40. data/test/unit/has_ssh_opts_tests.rb +81 -0
  41. data/test/unit/has_the_runs_tests.rb +37 -0
  42. data/test/unit/has_the_stubs_tests.rb +279 -0
  43. data/test/unit/local_tests.rb +174 -0
  44. data/test/unit/null_logger_tests.rb +17 -0
  45. data/test/unit/remote_tests.rb +330 -0
  46. data/test/unit/runner_tests.rb +398 -0
  47. data/test/unit/task_run_tests.rb +40 -0
  48. data/test/unit/task_tests.rb +943 -0
  49. data/test/unit/test_runner_tests.rb +189 -0
  50. data/test/unit/tree_runner_tests.rb +152 -0
  51. metadata +106 -9
@@ -0,0 +1,116 @@
1
+ require 'much-plugin'
2
+ require 'dk/local'
3
+ require 'dk/remote'
4
+
5
+ module Dk
6
+
7
+ module HasTheStubs
8
+ include MuchPlugin
9
+
10
+ plugin_included do
11
+ include InstanceMethods
12
+
13
+ end
14
+
15
+ module InstanceMethods
16
+
17
+ # cmd stub api
18
+
19
+ def local_cmd_stubs
20
+ @local_cmd_stubs ||= []
21
+ end
22
+
23
+ def stub_cmd(cmd_str, args = nil, &block)
24
+ args ||= {}
25
+
26
+ cmd_str_proc = get_cmd_ssh_proc(cmd_str)
27
+ input_proc = get_cmd_ssh_proc(args[:input])
28
+ given_opts_proc = get_cmd_ssh_proc(args[:opts])
29
+
30
+ local_cmd_stubs.unshift(
31
+ Stub.new(cmd_str_proc, input_proc, given_opts_proc, block)
32
+ )
33
+ end
34
+
35
+ def unstub_all_cmds
36
+ local_cmd_stubs.clear
37
+ end
38
+
39
+ # ssh stub API
40
+
41
+ def remote_cmd_stubs
42
+ @remote_cmd_stubs ||= []
43
+ end
44
+
45
+ def stub_ssh(cmd_str, args = nil, &block)
46
+ args ||= {}
47
+
48
+ cmd_str_proc = get_cmd_ssh_proc(cmd_str)
49
+ input_proc = get_cmd_ssh_proc(args[:input])
50
+ given_opts_proc = get_cmd_ssh_proc(args[:opts])
51
+
52
+ remote_cmd_stubs.unshift(
53
+ Stub.new(cmd_str_proc, input_proc, given_opts_proc, block)
54
+ )
55
+ end
56
+
57
+ def unstub_all_ssh
58
+ remote_cmd_stubs.clear
59
+ end
60
+
61
+ private
62
+
63
+ def get_cmd_ssh_proc(obj)
64
+ obj.kind_of?(::Proc) ? obj : proc{ obj }
65
+ end
66
+
67
+ def find_cmd_ssh_stub_block(stubs, task, cmd_str, input, given_opts)
68
+ stub = stubs.find do |stub|
69
+ task.instance_eval(&stub.cmd_str_proc) == cmd_str &&
70
+ task.instance_eval(&stub.input_proc) == input &&
71
+ task.instance_eval(&stub.given_opts_proc) == given_opts
72
+ end
73
+ stub ? stub.block : nil
74
+ end
75
+
76
+ # if the cmd is stubbed, build a spy and apply the stub (or return the
77
+ # cached spy), otherwise let the runner decide how to handle the local
78
+ # cmd
79
+ def build_local_cmd(task, cmd_str, input, given_opts)
80
+ b = find_cmd_ssh_stub_block(local_cmd_stubs, task, cmd_str, input, given_opts)
81
+ if b
82
+ Local::CmdSpy.new(cmd_str, given_opts).tap(&b)
83
+ else
84
+ has_the_stubs_build_local_cmd(cmd_str, given_opts)
85
+ end
86
+ end
87
+
88
+ def has_the_stubs_build_local_cmd(cmd_str, given_opts)
89
+ raise NotImplementedError
90
+ end
91
+
92
+ # if the cmd is stubbed, build a spy and apply the stub (or return the
93
+ # cached spy), otherwise let the runner decide how to handle the remote
94
+ # cmd; when building the spy use the ssh opts, this allows stubbing and
95
+ # calling ssh cmds with the same opts but also allows building a valid
96
+ # remote cmd that has an ssh host
97
+ def build_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
98
+ b = find_cmd_ssh_stub_block(remote_cmd_stubs, task, cmd_str, input, given_opts)
99
+ if b
100
+ Remote::CmdSpy.new(cmd_str, ssh_opts).tap(&b)
101
+ else
102
+ has_the_stubs_build_remote_cmd(cmd_str, ssh_opts)
103
+ end
104
+ end
105
+
106
+ def has_the_stubs_build_remote_cmd(cmd_str, ssh_opts)
107
+ raise NotImplementedError
108
+ end
109
+
110
+ end
111
+
112
+ Stub = Struct.new(:cmd_str_proc, :input_proc, :given_opts_proc, :block)
113
+
114
+ end
115
+
116
+ end
data/lib/dk/local.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'scmd'
2
+
3
+ module Dk; end
4
+ module Dk::Local
5
+
6
+ class BaseCmd
7
+
8
+ attr_reader :scmd, :cmd_str
9
+
10
+ def initialize(scmd_or_spy_klass, cmd_str, opts)
11
+ opts ||= {}
12
+
13
+ @cmd_str = cmd_str
14
+ @scmd = scmd_or_spy_klass.new(@cmd_str, :env => opts[:env])
15
+ end
16
+
17
+ def to_s; self.cmd_str; end
18
+
19
+ def run(input = nil)
20
+ @scmd.run(input)
21
+ self
22
+ end
23
+
24
+ def stdout; @scmd.stdout; end
25
+ def stderr; @scmd.stderr; end
26
+ def success?; @scmd.success?; end
27
+
28
+ def output_lines
29
+ build_stdout_lines(self.stdout) + build_stderr_lines(self.stderr)
30
+ end
31
+
32
+ private
33
+
34
+ def build_stdout_lines(stdout)
35
+ build_output_lines('stdout', stdout)
36
+ end
37
+
38
+ def build_stderr_lines(stderr)
39
+ build_output_lines('stderr', stderr)
40
+ end
41
+
42
+ def build_output_lines(name, output)
43
+ output.to_s.strip.split("\n").map{ |line| OutputLine.new(name, line) }
44
+ end
45
+
46
+ OutputLine = Struct.new(:name, :line)
47
+
48
+ end
49
+
50
+ class Cmd < BaseCmd
51
+
52
+ def initialize(cmd_str, opts = nil)
53
+ super(Scmd, cmd_str, opts)
54
+ end
55
+
56
+ end
57
+
58
+ class CmdSpy < BaseCmd
59
+
60
+ attr_reader :cmd_opts
61
+
62
+ def initialize(cmd_str, opts = nil)
63
+ require 'scmd/command_spy'
64
+ super(Scmd::CommandSpy, cmd_str, opts)
65
+ @cmd_opts = opts
66
+ end
67
+
68
+ def run_input
69
+ return nil unless self.run_called?
70
+ self.run_calls.first.input
71
+ end
72
+
73
+ def stdout=(value); @scmd.stdout = value; end
74
+ def stderr=(value); @scmd.stderr = value; end
75
+ def exitstatus=(value); @scmd.exitstatus = value; end
76
+
77
+ def run_calls; @scmd.run_calls; end
78
+ def run_called?; @scmd.run_called?; end
79
+
80
+ def ssh?; false; end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,13 @@
1
+ require 'logger'
2
+
3
+ module Dk
4
+
5
+ class NullLogger
6
+
7
+ ::Logger::Severity.constants.each do |name|
8
+ define_method(name.downcase){ |*args| } # no-op
9
+ end
10
+
11
+ end
12
+
13
+ end
data/lib/dk/remote.rb ADDED
@@ -0,0 +1,132 @@
1
+ require 'dk/config'
2
+ require 'dk/local'
3
+
4
+ module Dk; end
5
+ module Dk::Remote
6
+
7
+ def self.ssh_cmd_str(cmd_str, host, args, host_args)
8
+ host_args = host_args[host.to_s] if !host.nil?
9
+ val = "\"#{cmd_str.gsub(/\s+/, ' ')}\"".gsub("\\", "\\\\\\").gsub('"', '\"')
10
+ "ssh #{args} #{host_args} #{host} -- \"sh -c #{val}\""
11
+ end
12
+
13
+ class BaseCmd
14
+
15
+ attr_reader :hosts, :ssh_args, :host_ssh_args, :cmd_str, :local_cmds
16
+
17
+ def initialize(local_cmd_or_spy_klass, cmd_str, opts)
18
+ opts ||= {}
19
+ if nil_or_empty_or_missing_hosts(opts[:hosts])
20
+ raise NoHostsError, "no hosts to run cmd on (#{opts[:hosts].inspect})"
21
+ end
22
+
23
+ @hosts = opts[:hosts].sort
24
+ @ssh_args = opts[:ssh_args] || Dk::Config::DEFAULT_SSH_ARGS.dup
25
+ @host_ssh_args = opts[:host_ssh_args] || Dk::Config::DEFAULT_HOST_SSH_ARGS.dup
26
+ @cmd_str = cmd_str
27
+
28
+ @local_cmds = @hosts.inject({}) do |cmds, host|
29
+ cmds[host] = local_cmd_or_spy_klass.new(self.ssh_cmd_str(host), {
30
+ :env => opts[:env],
31
+ :dry_tree_run => opts[:dry_tree_run]
32
+ })
33
+ cmds
34
+ end
35
+ end
36
+
37
+ def to_s; self.cmd_str; end
38
+
39
+ def ssh_cmd_str(host)
40
+ build_ssh_cmd_str(@cmd_str, host, @ssh_args, @host_ssh_args)
41
+ end
42
+
43
+ def run(input = nil)
44
+ self.hosts.each{ |host| @local_cmds[host].scmd.start(input) }
45
+ self.hosts.each{ |host| @local_cmds[host].scmd.wait }
46
+ self
47
+ end
48
+
49
+ def stdout
50
+ self.hosts.inject('') do |out, host|
51
+ out.empty? ? @local_cmds[host].stdout.to_s : out
52
+ end
53
+ end
54
+
55
+ def stderr
56
+ self.hosts.inject('') do |err, host|
57
+ err.empty? ? @local_cmds[host].stderr.to_s : err
58
+ end
59
+ end
60
+
61
+ def success?
62
+ self.hosts.inject(true) do |success, host|
63
+ success && @local_cmds[host].success?
64
+ end
65
+ end
66
+
67
+ def output_lines
68
+ self.hosts.inject([]) do |lines, host|
69
+ lines + build_output_lines(host, @local_cmds[host].output_lines)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def nil_or_empty_or_missing_hosts(h)
76
+ h.nil? ||
77
+ !h.respond_to?(:empty?) || h.empty? ||
78
+ !h.respond_to?(:select) || h.select(&:nil?).size > 0
79
+ end
80
+
81
+ # escape everything properly; run in sh to ensure full profile is loaded
82
+ def build_ssh_cmd_str(cmd_str, host, args, host_args)
83
+ Dk::Remote.ssh_cmd_str(cmd_str, host, args, host_args)
84
+ end
85
+
86
+ def build_output_lines(host, local_cmd_output_lines)
87
+ local_cmd_output_lines.map{ |ol| OutputLine.new(host, ol.name, ol.line) }
88
+ end
89
+
90
+ OutputLine = Struct.new(:host, :name, :line)
91
+
92
+ end
93
+
94
+ NoHostsError = Class.new(ArgumentError)
95
+
96
+ class Cmd < BaseCmd
97
+
98
+ def initialize(cmd_str, opts = nil)
99
+ super(Dk::Local::Cmd, cmd_str, opts)
100
+ end
101
+
102
+ end
103
+
104
+ class CmdSpy < BaseCmd
105
+
106
+ attr_reader :cmd_opts
107
+
108
+ def initialize(cmd_str, opts = nil)
109
+ super(Dk::Local::CmdSpy, cmd_str, opts)
110
+ @cmd_opts = opts
111
+ @first_local_cmd_spy = @local_cmds[@hosts.first]
112
+ end
113
+
114
+ def run_input
115
+ return nil unless self.run_called?
116
+ self.run_calls.first.input
117
+ end
118
+
119
+ # just set the first local cmd, this will have an overall effect
120
+ def stdout=(value); @first_local_cmd_spy.stdout = value; end
121
+ def stderr=(value); @first_local_cmd_spy.stderr = value; end
122
+ def exitstatus=(value); @first_local_cmd_spy.exitstatus = value; end
123
+
124
+ # just query the firs tlocal cmd - if run for one it was run for all
125
+ def run_calls; @first_local_cmd_spy.scmd.start_calls; end
126
+ def run_called?; @first_local_cmd_spy.scmd.start_called?; end
127
+
128
+ def ssh?; true; end
129
+
130
+ end
131
+
132
+ end
data/lib/dk/runner.rb ADDED
@@ -0,0 +1,202 @@
1
+ require 'benchmark'
2
+ require 'set'
3
+ require 'dk'
4
+ require 'dk/ansi'
5
+ require 'dk/config'
6
+ require 'dk/has_set_param'
7
+ require 'dk/has_ssh_opts'
8
+ require 'dk/local'
9
+ require 'dk/null_logger'
10
+ require 'dk/remote'
11
+
12
+ module Dk
13
+
14
+ class Runner
15
+ include Dk::HasSetParam
16
+ include Dk::HasSSHOpts
17
+
18
+ TASK_START_LOG_PREFIX = ' >>> '.freeze
19
+ TASK_END_LOG_PREFIX = ' <<< '.freeze
20
+ INDENT_LOG_PREFIX = ' '.freeze
21
+ CMD_LOG_PREFIX = '[CMD] '.freeze
22
+ SSH_LOG_PREFIX = '[SSH] '.freeze
23
+ CMD_SSH_OUT_LOG_PREFIX = "> ".freeze
24
+
25
+ attr_reader :params, :logger
26
+
27
+ def initialize(opts = nil)
28
+ opts ||= {}
29
+ @params = Hash.new{ |h, k| raise Dk::NoParamError, "no param named `#{k}`" }
30
+ @params.merge!(dk_normalize_params(opts[:params]))
31
+
32
+ d = Config::DEFAULT_CALLBACKS
33
+ @task_callbacks = {
34
+ 'before' => opts[:before_callbacks] || d.dup,
35
+ 'prepend_before' => opts[:prepend_before_callbacks] || d.dup,
36
+ 'after' => opts[:after_callbacks] || d.dup,
37
+ 'prepend_after' => opts[:prepend_after_callbacks] || d.dup
38
+ }
39
+
40
+ @ssh_hosts = opts[:ssh_hosts] || Config::DEFAULT_SSH_HOSTS.dup
41
+ @ssh_args = opts[:ssh_args] || Config::DEFAULT_SSH_ARGS.dup
42
+ @host_ssh_args = opts[:host_ssh_args] || Config::DEFAULT_HOST_SSH_ARGS.dup
43
+
44
+ @logger = opts[:logger] || NullLogger.new
45
+
46
+ @has_run_task_classes = Set.new
47
+ end
48
+
49
+ def task_callbacks(named, task_class)
50
+ @task_callbacks[named][task_class] || []
51
+ end
52
+
53
+ def task_callback_task_classes(named, task_class)
54
+ task_callbacks(named, task_class).map(&:task_class)
55
+ end
56
+
57
+ def add_task_callback(named, subject_task_class, callback_task_class, params)
58
+ @task_callbacks[named][subject_task_class] << Task::Callback.new(
59
+ callback_task_class,
60
+ params
61
+ )
62
+ end
63
+
64
+ # called by CLI on top-level tasks
65
+ def run(task_class, params = nil)
66
+ check_run_once_and_build_and_run_task(task_class, params)
67
+ end
68
+
69
+ # called by other tasks on sub-tasks
70
+ def run_task(task_class, params = nil)
71
+ check_run_once_and_build_and_run_task(task_class, params)
72
+ end
73
+
74
+ def log_info(msg, *ansi_styles)
75
+ self.logger.info("#{INDENT_LOG_PREFIX}#{Ansi.styled_msg(msg, *ansi_styles)}")
76
+ end
77
+
78
+ def log_debug(msg, *ansi_styles)
79
+ self.logger.debug("#{INDENT_LOG_PREFIX}#{Ansi.styled_msg(msg, *ansi_styles)}")
80
+ end
81
+
82
+ def log_error(msg, *ansi_styles)
83
+ self.logger.error("#{INDENT_LOG_PREFIX}#{Ansi.styled_msg(msg, *ansi_styles)}")
84
+ end
85
+
86
+ def log_task_run(task_class, &run_block)
87
+ self.logger.debug "#{TASK_START_LOG_PREFIX}#{task_class}"
88
+ time = Benchmark.realtime(&run_block)
89
+ self.logger.debug "#{TASK_END_LOG_PREFIX}#{task_class} (#{self.pretty_run_time(time)})"
90
+ end
91
+
92
+ def log_cli_task_run(task_name, &run_block)
93
+ self.logger.info "Starting `#{task_name}`."
94
+ time = Benchmark.realtime(&run_block)
95
+ self.logger.info "`#{task_name}` finished in #{self.pretty_run_time(time)}."
96
+ self.logger.info ""
97
+ self.logger.info ""
98
+ end
99
+
100
+ def log_cli_run(cli_argv, &run_block)
101
+ 15.times{ self.logger.debug "" }
102
+ self.logger.debug "===================================="
103
+ self.logger.debug ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> `#{cli_argv}`"
104
+ self.logger.debug "===================================="
105
+ time = Benchmark.realtime(&run_block)
106
+ self.logger.info "(#{self.pretty_run_time(time)})"
107
+ self.logger.debug "===================================="
108
+ self.logger.debug "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< `#{cli_argv}`"
109
+ self.logger.debug "===================================="
110
+ end
111
+
112
+ def cmd(task, cmd_str, input, given_opts)
113
+ build_and_run_local_cmd(task, cmd_str, input, given_opts)
114
+ end
115
+
116
+ def ssh(task, cmd_str, input, given_opts, ssh_opts)
117
+ build_and_run_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
118
+ end
119
+
120
+ def has_run_task?(task_class)
121
+ @has_run_task_classes.include?(task_class)
122
+ end
123
+
124
+ def pretty_run_time(raw_run_time)
125
+ if raw_run_time > 1.5 # seconds
126
+ "#{raw_run_time.to_i / 60}:#{(raw_run_time.round % 60).to_i.to_s.rjust(2, '0')}s"
127
+ else
128
+ "#{(raw_run_time * 1000 * 10.0).round / 10.0}ms"
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def check_run_once_and_build_and_run_task(task_class, params = nil)
135
+ if task_class.run_only_once && self.has_run_task?(task_class)
136
+ build_task(task_class, params)
137
+ else
138
+ build_and_run_task(task_class, params)
139
+ end
140
+ end
141
+
142
+ def build_and_run_task(task_class, params = nil)
143
+ build_task(task_class, params).tap do |task|
144
+ task.dk_run
145
+ @has_run_task_classes << task_class
146
+ end
147
+ end
148
+
149
+ def build_task(task_class, params = nil)
150
+ task_class.new(self, params)
151
+ end
152
+
153
+ def build_and_run_local_cmd(task, cmd_str, input, given_opts)
154
+ local_cmd = build_local_cmd(task, cmd_str, input, given_opts)
155
+ log_local_cmd(local_cmd){ |cmd| cmd.run(input) }
156
+ end
157
+
158
+ # input is needed for the `TestRunner` so it can use it with stubbing
159
+ # otherwise it is ignored when building a local cmd
160
+ def build_local_cmd(task, cmd_str, input, given_opts)
161
+ Local::Cmd.new(cmd_str, given_opts)
162
+ end
163
+
164
+ def log_local_cmd(cmd, &block)
165
+ self.logger.info("#{CMD_LOG_PREFIX}#{cmd.cmd_str}")
166
+ time = Benchmark.realtime{ block.call(cmd) }
167
+ self.logger.info("#{INDENT_LOG_PREFIX}(#{self.pretty_run_time(time)})")
168
+ cmd.output_lines.each do |output_line|
169
+ self.logger.debug("#{INDENT_LOG_PREFIX}#{CMD_SSH_OUT_LOG_PREFIX}#{output_line.line}")
170
+ end
171
+ cmd
172
+ end
173
+
174
+ def build_and_run_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
175
+ remote_cmd = build_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
176
+ log_remote_cmd(remote_cmd){ |cmd| cmd.run(input) }
177
+ end
178
+
179
+ # input and given opts are needed for the `TestRunner` so it can use it with
180
+ # stubbing otherwise they are ignored when building a remote cmd
181
+ def build_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
182
+ Remote::Cmd.new(cmd_str, ssh_opts)
183
+ end
184
+
185
+ def log_remote_cmd(cmd, &block)
186
+ self.logger.info("#{SSH_LOG_PREFIX}#{cmd.cmd_str}")
187
+ self.logger.debug("#{INDENT_LOG_PREFIX}#{cmd.ssh_cmd_str('<host>')}")
188
+ cmd.hosts.each do |host|
189
+ self.logger.info("#{INDENT_LOG_PREFIX}[#{host}]")
190
+ end
191
+ time = Benchmark.realtime{ block.call(cmd) }
192
+ self.logger.info("#{INDENT_LOG_PREFIX}(#{self.pretty_run_time(time)})")
193
+ cmd.output_lines.each do |ol|
194
+ self.logger.debug "#{INDENT_LOG_PREFIX}[#{ol.host}] " \
195
+ "#{CMD_SSH_OUT_LOG_PREFIX}#{ol.line}"
196
+ end
197
+ cmd
198
+ end
199
+
200
+ end
201
+
202
+ end