volley 0.1.0.alpha4 → 0.1.0.alpha5

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 (48) hide show
  1. data/.gitignore +3 -1
  2. data/.rspec +1 -0
  3. data/Gemfile +4 -1
  4. data/Rakefile +34 -0
  5. data/bin/volley +88 -40
  6. data/conf/common.volleyfile +12 -29
  7. data/features/publisher.feature +45 -0
  8. data/features/step_definitions/common_steps.rb +9 -0
  9. data/features/step_definitions/publisher_steps.rb +92 -0
  10. data/features/support/env.rb +19 -0
  11. data/init/Volleyfile +1 -104
  12. data/lib/volley/descriptor.rb +28 -0
  13. data/lib/volley/dsl/action.rb +31 -0
  14. data/lib/volley/dsl/argument.rb +96 -0
  15. data/lib/volley/dsl/file.rb +70 -0
  16. data/lib/volley/dsl/plan.rb +110 -235
  17. data/lib/volley/dsl/project.rb +10 -2
  18. data/lib/volley/dsl/pull_action.rb +50 -0
  19. data/lib/volley/dsl/push_action.rb +101 -0
  20. data/lib/volley/dsl/stage.rb +40 -0
  21. data/lib/volley/dsl.rb +7 -0
  22. data/lib/volley/log.rb +22 -8
  23. data/lib/volley/meta.rb +24 -0
  24. data/lib/volley/publisher/amazons3.rb +67 -66
  25. data/lib/volley/publisher/base.rb +81 -42
  26. data/lib/volley/publisher/exceptions.rb +7 -0
  27. data/lib/volley/publisher/local.rb +41 -27
  28. data/lib/volley/scm/base.rb +10 -0
  29. data/lib/volley.rb +38 -12
  30. data/spec/descriptor_spec.rb +39 -0
  31. data/spec/dsl_plan_spec.rb +103 -0
  32. data/spec/dsl_project_spec.rb +36 -0
  33. data/spec/dsl_volleyfile_spec.rb +21 -0
  34. data/spec/meta_spec.rb +26 -0
  35. data/spec/publisher_spec.rb +92 -0
  36. data/test/dsl/amazons3_publisher.volleyfile +6 -0
  37. data/test/dsl/local_publisher.volleyfile +4 -0
  38. data/test/dsl/log_console.volleyfile +2 -0
  39. data/test/dsl/log_file.volleyfile +2 -0
  40. data/test/dsl/simple.volleyfile +17 -0
  41. data/test/meta.yml +3 -0
  42. data/test/project/Rakefile +13 -0
  43. data/test/project/Volleyfile +18 -0
  44. data/test/trunk-1.tgz +0 -0
  45. data/volley.gemspec +2 -1
  46. metadata +67 -5
  47. data/lib/volley/config.rb +0 -8
  48. data/lib/volley/volley_file.rb +0 -45
data/.gitignore CHANGED
@@ -8,9 +8,11 @@ InstalledFiles
8
8
  _yardoc
9
9
  coverage
10
10
  doc/
11
+ log/
11
12
  lib/bundler/man
12
13
  pkg
13
14
  rdoc
14
15
  spec/reports
15
- test/*
16
+ test/publisher
17
+ test/project/file
16
18
  tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format s -c
data/Gemfile CHANGED
@@ -4,4 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem 'rake'
7
- gem 'templar'
7
+ gem 'cucumber'
8
+ gem 'rspec'
9
+
10
+ gem 'awesome_print'
data/Rakefile CHANGED
@@ -1,2 +1,36 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+ require 'cucumber/rake/task'
5
+
6
+ $:.unshift File.expand_path("lib/")
7
+ require 'volley'
8
+
9
+ desc 'Default: run specs.'
10
+ task :default => :test
11
+
12
+ desc "Run specs"
13
+ RSpec::Core::RakeTask.new(:spec) do |t|
14
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
15
+ # Put spec opts in a file named .rspec in root
16
+ end
17
+
18
+ desc "Run Cucumber"
19
+ Cucumber::Rake::Task.new do |t|
20
+ t.cucumber_opts = %w{--format pretty --no-snippets}
21
+ end
22
+
23
+ desc "Run RSpec and Cucumber tests"
24
+ task :test do
25
+ begin
26
+ Rake::Task["spec"].invoke
27
+ rescue => e
28
+ puts "#{e.message} at #{e.backtrace.first}"
29
+ end
30
+
31
+ begin
32
+ Rake::Task["cucumber"].invoke
33
+ rescue => e
34
+ puts "#{e.message} at #{e.backtrace.first}"
35
+ end
36
+ end
data/bin/volley CHANGED
@@ -1,55 +1,103 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
+ require 'docopt'
4
5
  require 'volley'
5
- require 'clamp'
6
+ require 'awesome_print'
6
7
 
7
- Volley::VolleyFile.init
8
- STDOUT.sync = true
8
+ DOC = <<-DOC
9
+ Usage: volley [options] <plan> <descriptor> [argument]...
10
+
11
+ <plan>
12
+ The plan to run.
13
+ reserved plan names: [versions, latest, list]
14
+
15
+ <descriptor>
16
+ A descriptor should conform to the following format:
17
+ <project>[@<branch>[:<version>]]
18
+
19
+ project: the project name.
20
+ branch: the branch name.
21
+ Defaults to the branch currently in use,
22
+ must be specified for remote mode.
23
+ version: the version (revision)
24
+ Defaults to the current revision
25
+ In remote mode, defaults to "latest"
26
+
27
+ <argument>
28
+ A list of key=value pairs
29
+
30
+ Options:
31
+ -h --help show this help message and exit
32
+ --version show version and exit
33
+ -d --debug show debug output, change log level to debug
34
+ -c --config FILE load additional Volleyfile [default: ~/.Volleyfile]
35
+ -p --primary FILE load primary Volleyfile [default: ./Volleyfile]
36
+
37
+ -f --fork fork process into background and exit
38
+ -l --log LOG log file [default: /opt/volley/volley.log]
39
+ -L --level LEVEL log level [default: debug]
40
+ DOC
9
41
 
10
42
  module Volley
11
- class Command < Clamp::Command
12
- option %w{-c --config}, "CONFIG", "configuration file", :default => "~/.Volleyfile"
13
- option %w{-p --primary}, "PRIMARY", "primary configuration file", :default => "./Volleyfile"
14
- option %w{-d --debug}, :flag, "set debug flag"
15
- option %w{-f --fork}, :flag, "fork process into background"
16
-
17
- parameter "DESCRIPTOR", "volley descriptor, follows the format: project:plan[@branch[:version]]"
18
- parameter "[ARG] ...", "additional arguments passed to plan of the form: key:value"
19
-
20
- def execute
21
- Volley::VolleyFile.load(config, :optional => true)
22
- Volley::VolleyFile.load(primary, :primary => true) if File.file?(primary)
23
- Volley.config.debug = debug?
24
-
25
- (project, plan, branch, version) = descriptor.split(/[:\@]/)
26
- project = "volley" if project.nil? || project.blank?
27
-
28
- args = arg_list
29
- args << "branch:#{branch}" if branch
30
- args << "version:#{version}" if version
31
-
32
- if debug?
33
- puts "project: #{project}"
34
- puts "plan: #{plan}"
35
- puts "branch: #{branch}"
36
- puts "version: #{version}"
37
- puts "args_list: #{args.join(",")}"
43
+ class Command
44
+ def initialize
45
+ end
46
+
47
+ def run(argv)
48
+ STDOUT.sync = true
49
+ options = Docopt(DOC, Volley::Version::STRING)
50
+ debug = options[:debug]
51
+ config = options[:config]
52
+ primary = options[:primary]
53
+ fork = options[:fork]
54
+ log = options[:log]
55
+ level = options[:level]
56
+
57
+ Volley::Dsl::VolleyFile.init
58
+ Volley::Dsl::VolleyFile.load(config, :optional => true)
59
+ Volley::Dsl::VolleyFile.load(primary, :primary => true) if File.file?(primary)
60
+ Volley::Log.add(level.to_sym, log)
61
+ Volley::Log.console_debug if debug
62
+ Volley.config.debug = debug
63
+
64
+ kvs = argv.select { |e| e.match(/(\w+)\=(\w+)/) }
65
+ pos = argv.reject { |e| e.match(/(\w+)\=(\w+)/) }
66
+
67
+ plan = pos.shift
68
+ desc = pos.shift
69
+
70
+ raise "must specify plan" unless plan
71
+
72
+ if Volley::Dsl.project(:volley).plan?(plan)
73
+ # the plan is reserved
74
+ plan = "volley:#{plan}"
75
+ else
76
+ # the plan isn't reserved
77
+ raise "must specify descriptor" unless desc
78
+ (project, branch, version) = Volley::Descriptor.new(desc).get
79
+ plan = "#{project}:#{plan}"
38
80
  end
39
81
 
40
- #raise "you must specify project (#{project}): [#{Volley::Dsl.projects.keys.join(',')}]" unless project && Volley::Dsl.project(project)
41
- #raise "you must specify plan: #{project} [#{Volley::Dsl::Project.project(project).plans.keys.join(", ")}]" unless plan && Volley::Dsl::Project.project(project).plan(plan)
82
+ if debug
83
+ Volley::Log.debug "## OPTIONS ##"
84
+ Volley::Log.debug "plan: #{plan}"
85
+ Volley::Log.debug "descriptor: #{desc}"
86
+ Volley::Log.debug "positional: #{pos.join(",")}"
87
+ Volley::Log.debug "key:value: #{kvs.join(",")}"
88
+ end
42
89
 
43
- Volley.process(:project => project, :plan => plan, :branch => branch, :version => version, :args => args)
44
- rescue Interrupt => e
45
- Volley::Log.warn "Cancelled..."
90
+ Volley::Log.info "processing '#{plan}' plan for '#{desc}'"
91
+ Volley.process(:plan => plan, :descriptor => desc, :args => kvs)
46
92
  rescue => e
47
- Volley::Log.error "error: #{e.message}"
48
- Volley::Log.error e if debug?
49
- Volley::Log.debug e
50
- raise Clamp::HelpWanted, self
93
+ Volley::Log.error "exception: #{e.message} at #{e.backtrace.first}"
94
+ if debug
95
+ Volley::Log.error e
96
+ else
97
+ Volley::Log.debug e
98
+ end
51
99
  end
52
100
  end
53
101
  end
54
102
 
55
- Volley::Command.run
103
+ Volley::Command.new.run(ARGV)
@@ -1,13 +1,14 @@
1
1
  project :volley do
2
- plan :init do
2
+ scm :base
3
+ plan :init, :remote => false do
3
4
  default do
4
- file = File.expand_path("../init/Volleyfile", __FILE__)
5
+ file = File.expand_path("../../init/Volleyfile", __FILE__)
5
6
  dest = "#{Dir.pwd}/Volleyfile"
6
7
  FileUtils.copy(file, dest)
7
8
  puts "created: #{dest}"
8
9
  end
9
10
  end
10
- plan :list do
11
+ plan :list, :remote => false do
11
12
  default do
12
13
  Volley::Dsl::Project.projects.each do |p, project|
13
14
  Volley::Log.info "project: #{p}"
@@ -18,40 +19,24 @@ project :volley do
18
19
  end
19
20
  end
20
21
  plan :latest do
21
- argument :project #, :required => true
22
- argument :branch #, :required => true
23
22
  default do
24
- project = args.project
25
- branch = args.branch
26
-
27
- if project.nil? && rawargs
28
- first = rawargs.first
29
- (p, b) = first.split(/\//) if first
30
- if p
31
- project = p
32
- branch = b
33
- end
34
- end
23
+ ap args
24
+ (project,branch,version) = args.descriptor.get
35
25
  raise "project and branch must be specified" unless project && branch
36
26
 
37
27
  pub = Volley::Dsl.publisher
38
28
  puts pub.latest(project, branch)
39
29
  end
40
30
  end
41
- plan :versions do
42
- argument :project
43
- argument :branch
44
- argument :version
31
+ plan :versions, :remote => false do
45
32
  argument :all, :convert => :boolean, :default => false
46
33
  argument :output, :default => "list", :convert => :to_sym, :choices => ["json", "xml", "list"]
47
34
 
48
35
  default do
49
- project = args.project
50
- branch = args.branch
51
- version = args.version
36
+ (project, branch, version) = args.descriptor.get
52
37
 
53
- if project.nil? && rawargs
54
- first = rawargs.first
38
+ if project.nil? && argv.count
39
+ first = argv.first
55
40
  (p, b, v) = first.split(/\//) if first
56
41
  if p
57
42
  project = p
@@ -68,7 +53,7 @@ project :volley do
68
53
  else
69
54
  if project
70
55
  if branch
71
- if version
56
+ if version != "latest"
72
57
  data = pub.contents(project, branch, version)
73
58
  else
74
59
  data = pub.versions(project, branch)
@@ -92,11 +77,9 @@ project :volley do
92
77
  end
93
78
  end
94
79
  plan :remote do
95
- argument :version, :required => true
96
-
97
80
  default do
98
81
  pub = Volley::Dsl.publisher
99
- (pr, br, vr) = args.version.split(/[\:\/\.]/)
82
+ (pr, br, vr) = args.descriptor.get
100
83
  vr ||= 'latest'
101
84
  vf = pub.volleyfile(:project => pr, :branch => br, :version => vr)
102
85
  load vf
@@ -0,0 +1,45 @@
1
+
2
+ # Built these tests as a starting point
3
+ # duplicates some of the tests in spec/publisher_spec
4
+ Feature: Publisher
5
+ In order to store artifacts
6
+ I want to be able to manage files with a publisher
7
+
8
+ Background:
9
+ Given I have a publisher with an empty repository
10
+
11
+ Scenario: Empty publisher
12
+ When I request a list of projects from the publisher
13
+ Then I should see an empty list
14
+
15
+ Scenario: Publish multiple artifacts
16
+ When I publish the artifact test/trunk/1
17
+ Then there should be 1 projects in the publisher
18
+ And the test project should have 1 branches
19
+ When I publish the artifact test/staging/2
20
+ Then there should be 1 projects in the publisher
21
+ And the test project should have 2 branches
22
+
23
+ Scenario: Publish and retrieve artifact
24
+ When I publish the artifact test/trunk/1
25
+ Then there should be 1 projects in the publisher
26
+ And the test project should have 1 branches
27
+ When I deploy the artifact test/trunk/1
28
+ Then I should not receive an exception
29
+
30
+ Scenario: Latest artifacts
31
+ Given I have a populated repository
32
+ Then the test project should have 3 branches
33
+ And the latest of test/trunk should be 7
34
+ And the latest of test/staging should be 5
35
+
36
+ Scenario: Duplicate Artifact
37
+ Given I have a populated repository
38
+ When I publish a duplicate artifact test/staging/12
39
+ Then I should receive an exception
40
+
41
+ # TODO: make this work
42
+ # Scenario: Duplicate Artifact force
43
+ # Given I have a populated repository
44
+ # When I force publish a duplicate artifact test/staging/12
45
+ # Then I should not receive an exception
@@ -0,0 +1,9 @@
1
+
2
+
3
+ Then /^I should receive an exception$/ do
4
+ fail if @exception.nil?
5
+ end
6
+
7
+ Then /^I should not receive an exception$/ do
8
+ fail unless @exception.nil?
9
+ end
@@ -0,0 +1,92 @@
1
+ def publish(desc)
2
+ pwd = Dir.pwd
3
+ Dir.chdir("test/project")
4
+ Volley::Dsl::VolleyFile.load("Volleyfile")
5
+ Volley.process(:plan => "publish", :descriptor => desc)
6
+ Dir.chdir(pwd)
7
+ end
8
+
9
+ def deploy(desc)
10
+ pwd = Dir.pwd
11
+ Volley.process(:plan => "deploy", :descriptor => desc)
12
+ Dir.chdir(pwd)
13
+ end
14
+
15
+ Given /^I have a publisher with an empty repository$/ do
16
+ @root ||= Volley.config.project_root
17
+ %w{local remote}.each { |d| FileUtils.rm_rf("#@root/test/publisher/#{d}") }
18
+ %w{local remote}.each { |d| FileUtils.mkdir_p("#@root/test/publisher/#{d}") }
19
+ Volley::Dsl::VolleyFile.load("#@root/test/dsl/local_publisher.volleyfile")
20
+ @pub = Volley::Dsl.publisher
21
+ end
22
+
23
+ Given /^I have a populated repository$/ do
24
+ steps %Q{
25
+ When I publish the artifact test/trunk/1
26
+ And I publish the artifact test/staging/2
27
+ And I publish the artifact test/trunk/3
28
+ And I publish the artifact test/master/4
29
+ And I publish the artifact test/staging/5
30
+ And I publish the artifact test/trunk/7
31
+ }
32
+ end
33
+
34
+ When /^I request a list of projects from the publisher$/ do
35
+ @emptylist = @pub.projects
36
+ end
37
+
38
+ Then /^I should see an empty list$/ do
39
+ fail unless @emptylist.count == 0
40
+ end
41
+
42
+ When /^I publish the artifact (.*)$/ do |desc|
43
+ publish(desc)
44
+ end
45
+
46
+ When /^I deploy the artifact (.*)$/ do |desc|
47
+ begin
48
+ deploy(desc)
49
+ rescue => e
50
+ @exception = e
51
+ end
52
+ end
53
+
54
+ When /^I publish a duplicate artifact (.*)$/ do |desc|
55
+ steps %Q{
56
+ When I publish the artifact #{desc}
57
+ }
58
+ begin
59
+ publish(desc)
60
+ rescue => e
61
+ @exception = e
62
+ end
63
+ end
64
+
65
+ # TODO: make this work
66
+ When /^I force publish a duplicate artifact (.*)$/ do |desc|
67
+ steps %Q{
68
+ When I publish the artifact #{desc}
69
+ }
70
+ begin
71
+ publish(desc)
72
+ rescue => e
73
+ puts "EXCEPTION"
74
+ @exception = e
75
+ end
76
+ end
77
+
78
+ Then /^there should be (.*) projects in the publisher$/ do |count|
79
+ list = @pub.projects
80
+ #puts "LIST:#{list.inspect}"
81
+ fail unless list.count == count.to_i
82
+ end
83
+
84
+ Then /^the (.*) project should have (.*) branches$/ do |project, count|
85
+ list = @pub.branches(project)
86
+ fail unless list.count == count.to_i
87
+ end
88
+
89
+ Then /^the latest of (.*)\/(.*) should be (.*)$/ do |project, branch, version|
90
+ latest = @pub.latest(project, branch)
91
+ fail unless latest == "#{project}/#{branch}/#{version}"
92
+ end
@@ -0,0 +1,19 @@
1
+ $:.unshift File.expand_path("lib/")
2
+
3
+ require 'fileutils'
4
+ require 'volley'
5
+
6
+ root = Dir.pwd
7
+
8
+ Volley::Log.add(:debug, "#{Dir.pwd}/log/volley.log")
9
+ Volley::Log.console_disable
10
+ Volley.config.project_root = root
11
+
12
+ World do
13
+ @root = root
14
+ end
15
+
16
+ at_exit do
17
+ puts "cleaning up... #{root}"
18
+ %w{local remote}.each { |d| FileUtils.rm_rf("#{root}/test/publisher/#{d}") }
19
+ end
data/init/Volleyfile CHANGED
@@ -1,104 +1 @@
1
- # TODO: implement source control framework
2
- # Source Control - access to source control information from within actions
3
- # scm :svn
4
- # or
5
- # scm :git
6
- #
7
- # Configuring one of these, enables you to use source control
8
- # information inside actions:
9
- # action :name do
10
- # args.branch = source.branch
11
- # args.version = source.revision
12
- # end
13
-
14
- project :myproject do
15
- plan :push do
16
- # Encryption allows you to share artifact files on public data stores.
17
- #
18
- # encryption is turned off by default
19
- # encrypt false
20
- # to enable encryption you must supply a keyfile
21
- # encrypt true, :key => "/path/to/keyfile"
22
- # or supply a key directly
23
- # encrypt true, :key => "this is the key"
24
-
25
- # Command output is disabled by default
26
- # output false
27
- # setting this to true, will display output
28
- # from all commands that are run as part
29
- # of the plan
30
- # output true
31
-
32
- # Arguments are captured from the command line
33
- # in key:value pairs.
34
- #
35
- # By specifying the following:
36
- # argument :branch
37
- # You will then have access to args.branch within your plan's actions
38
- #
39
- # Argument options
40
- # :default set the default value for the argument
41
- # :convert convert the value received from the command line (see below)
42
- # :required raise an error if this argument is not specified
43
- #
44
- # Convert Option:
45
- # :convert => :boolean
46
- # :convert => :to_s
47
- # :convert => :to_i
48
- # Boolean is a custom conversion, all others are delegated to the string class
49
- #
50
-
51
- # Actions
52
- # Actions are executed in order that they are defined in.
53
- #
54
- # Generally you will use action wrappers (like: build and push)
55
- #
56
- # The build action is a specialized action that *requires*
57
- # the return value of the block to be the list of files to add
58
- # to the artifact.
59
- #
60
- # build do |attr|
61
- # # run build commands
62
- # ["target/something.war"]
63
- # end
64
- #
65
- # By default, the build action will also configure the 'pack' action
66
- # (which handles creating the final combined artifact) and the 'encrypt'
67
- # action, if encryption is enabled.
68
- #
69
- # The 'push' action is a shortcut to Publish the artifact to a data store.
70
- # push :publisher
71
- # Volley includes the Amazon S3 publisher.
72
- # push :amazons3
73
- #
74
- # You also have the ability to add actions directly:
75
- #
76
- # action :name do
77
- # # do something
78
- # end
79
-
80
- build do |attr|
81
- clean = args.clean ? "clean" : ""
82
-
83
- # to run commands inside of actions, use the shellout method
84
- # this is a wrapper around Mixlib::Shellout
85
- shellout("rake #{clean} build")
86
-
87
- # the 'branch' argument is generally set to the branch name.
88
- # by using the ||= operator here, we can also set the 'branch' argument on the command line.
89
- args.branch ||= source.branch
90
- # the 'version' argument is generally set to the source control revision number.
91
- # by using the ||= operator here, we can also set the 'branch' argument on the command line.
92
- args.version ||= source.revision
93
-
94
- final = "target/awesome.war"
95
- [final]
96
- end
97
-
98
- push :amazons3
99
- end
100
-
101
- plan :deploy do
102
- pull :amazons3
103
- end
104
- end
1
+ # TODO: write this
@@ -0,0 +1,28 @@
1
+
2
+ module Volley
3
+ class Descriptor
4
+ attr_reader :project, :branch, :version
5
+
6
+ def initialize(desc="", options={})
7
+ @options = {
8
+ :partial => false,
9
+ }.merge(options)
10
+
11
+ if desc
12
+ list = desc.split(/[\@\:\.\/\\\-]/)
13
+ raise "error parsing descriptor: #{desc}" if (list.count < 2 || list.count > 3) && !@options[:partial]
14
+ (@project, @branch, @version) = list
15
+ @version ||= "latest"
16
+ raise "error parsing descriptor: #{desc}" unless (@project && @branch && @version) || @options[:partial]
17
+ end
18
+ end
19
+
20
+ def get
21
+ [@project, @branch, @version]
22
+ end
23
+
24
+ def ==(other)
25
+ @project == other.project && @branch == other.branch && @version == other.version
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module Volley
3
+ module Dsl
4
+ class Action
5
+ attr_reader :plan
6
+
7
+ def initialize(name, options={}, &block)
8
+ @name = name.to_sym
9
+ @stage = options.delete(:stage)
10
+ @plan = options.delete(:plan)
11
+ @block = block
12
+ @options = {
13
+ }.merge(options)
14
+ raise "stage instance must be set" unless @stage
15
+ raise "plan instance must be set" unless @plan
16
+ end
17
+
18
+ def call
19
+ Volley::Log.debug ".. .. #@name"
20
+ self.instance_eval &@block if @block
21
+ end
22
+
23
+ delegate :project, :args, :files, :file, :attributes, :log, :arguments, :argv, :branch, :version, :action,
24
+ :to => :plan
25
+
26
+ def command(cmd)
27
+ @plan.shellout(cmd)
28
+ end
29
+ end
30
+ end
31
+ end