hobo-inviqa 0.0.7 → 0.0.8
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 +15 -0
- data/.editorconfig +10 -0
- data/Gemfile.lock +19 -4
- data/Guardfile +2 -2
- data/Hobofile +5 -1
- data/README.md +8 -32
- data/bin/hobo +12 -18
- data/hobo.gemspec +3 -0
- data/lib/hobo.rb +8 -1
- data/lib/hobo/cli.rb +14 -3
- data/lib/hobo/error_handlers/debug.rb +5 -2
- data/lib/hobo/error_handlers/exit_code_map.rb +16 -0
- data/lib/hobo/error_handlers/friendly.rb +8 -8
- data/lib/hobo/errors.rb +11 -1
- data/lib/hobo/helper/http_download.rb +41 -0
- data/lib/hobo/helper/shell.rb +3 -2
- data/lib/hobo/helper/vm_command.rb +235 -14
- data/lib/hobo/lib/host_check.rb +20 -6
- data/lib/hobo/lib/host_check/deps.rb +22 -4
- data/lib/hobo/lib/host_check/git.rb +41 -17
- data/lib/hobo/lib/host_check/ruby.rb +30 -20
- data/lib/hobo/lib/host_check/vagrant.rb +37 -6
- data/lib/hobo/lib/s3sync.rb +22 -44
- data/lib/hobo/lib/seed/project.rb +10 -6
- data/lib/hobo/patches/slop.rb +21 -2
- data/lib/hobo/tasks/assets.rb +12 -15
- data/lib/hobo/tasks/config.rb +15 -0
- data/lib/hobo/tasks/deps.rb +37 -6
- data/lib/hobo/tasks/system.rb +15 -0
- data/lib/hobo/tasks/system/completions.rb +76 -0
- data/lib/hobo/tasks/tools.rb +10 -6
- data/lib/hobo/tasks/vm.rb +64 -11
- data/lib/hobo/ui.rb +27 -10
- data/lib/hobo/util.rb +36 -2
- data/lib/hobo/version.rb +2 -2
- data/spec/hobo/asset_applicator_spec.rb +2 -2
- data/spec/hobo/cli_spec.rb +40 -24
- data/spec/hobo/config/file_spec.rb +1 -3
- data/spec/hobo/error_handlers/debug_spec.rb +39 -5
- data/spec/hobo/error_handlers/friendly_spec.rb +38 -21
- data/spec/hobo/help_formatter_spec.rb +3 -3
- data/spec/hobo/helpers/file_locator_spec.rb +2 -2
- data/spec/hobo/helpers/shell_spec.rb +2 -2
- data/spec/hobo/helpers/vm_command_spec.rb +54 -21
- data/spec/hobo/lib/s3sync_spec.rb +6 -3
- data/spec/hobo/lib/seed/project_spec.rb +2 -3
- data/spec/hobo/lib/seed/replacer_spec.rb +1 -2
- data/spec/hobo/lib/seed/seed_spec.rb +2 -3
- data/spec/hobo/logging_spec.rb +2 -2
- data/spec/hobo/metadata_spec.rb +2 -2
- data/spec/hobo/null_spec.rb +2 -2
- data/spec/hobo/paths_spec.rb +1 -2
- data/spec/hobo/ui_spec.rb +104 -20
- data/spec/hobo/util_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -0
- metadata +55 -46
- data/lib/hobo/tasks/host.rb +0 -19
@@ -0,0 +1,15 @@
|
|
1
|
+
desc "Configure hobo"
|
2
|
+
task :config do
|
3
|
+
config = Hobo.user_config
|
4
|
+
|
5
|
+
# Not required at present
|
6
|
+
# config.full_name = Hobo.ui.ask("Full name", :default => config.full_name).to_s
|
7
|
+
# config.email = Hobo.ui.ask("Email", :default => config.email).to_s
|
8
|
+
|
9
|
+
config[:aws] ||= {}
|
10
|
+
config.aws.access_key_id = Hobo.ui.ask("AWS access key ID", :default => config.aws.access_key_id).to_s
|
11
|
+
config.aws.secret_access_key = Hobo.ui.ask("AWS secret access key", :default => config.aws.secret_access_key).to_s
|
12
|
+
|
13
|
+
Hobo::Config::File.save(Hobo.user_config_file, config)
|
14
|
+
File.chmod(0600, Hobo.user_config_file)
|
15
|
+
end
|
data/lib/hobo/tasks/deps.rb
CHANGED
@@ -21,10 +21,30 @@ namespace :deps do
|
|
21
21
|
Rake::Task["tools:composer"].invoke
|
22
22
|
Hobo.ui.title "Installing composer dependencies"
|
23
23
|
Dir.chdir Hobo.project_path do
|
24
|
-
|
24
|
+
ansi = Hobo.ui.supports_color? ? '--ansi' : ''
|
25
|
+
args = [ "php bin/composer.phar install #{ansi} --prefer-dist", { realtime: true, indent: 2 } ]
|
26
|
+
complete = false
|
27
|
+
|
28
|
+
check = Hobo::Lib::HostCheck.check(:filter => /php_present/)
|
29
|
+
|
30
|
+
if check["Php present"] == :ok
|
31
|
+
begin
|
32
|
+
shell *args
|
33
|
+
complete = true
|
34
|
+
rescue Hobo::ExternalCommandError
|
35
|
+
Hobo.ui.warning "Installing composer dependencies locally failed!"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if !complete
|
40
|
+
vm_shell *args
|
41
|
+
end
|
42
|
+
|
43
|
+
Hobo.ui.success "Composer dependencies installed"
|
25
44
|
end
|
45
|
+
|
46
|
+
Hobo.ui.separator
|
26
47
|
end
|
27
|
-
Hobo.ui.separator
|
28
48
|
end
|
29
49
|
|
30
50
|
desc "Install vagrant plugins"
|
@@ -32,9 +52,11 @@ namespace :deps do
|
|
32
52
|
plugins = shell "vagrant plugin list", :capture => true
|
33
53
|
locate "*Vagrantfile" do
|
34
54
|
File.read("Vagrantfile").split("\n").each do |line|
|
55
|
+
next if line.match /^\s*#/
|
35
56
|
next unless line.match /Vagrant\.require_plugin (.*)/
|
36
57
|
plugin = $1.gsub(/['"]*/, '')
|
37
58
|
next if plugins.include? "#{plugin} "
|
59
|
+
|
38
60
|
Hobo.ui.title "Installing vagrant plugin: #{plugin}"
|
39
61
|
bundle_shell "vagrant", "plugin", "install", plugin, :realtime => true, :indent => 2
|
40
62
|
Hobo.ui.separator
|
@@ -45,13 +67,22 @@ namespace :deps do
|
|
45
67
|
desc "Install chef dependencies"
|
46
68
|
task :chef => [ "deps:gems" ] do
|
47
69
|
locate "*Cheffile" do
|
48
|
-
Hobo.ui.title "Installing chef dependencies"
|
70
|
+
Hobo.ui.title "Installing chef dependencies via librarian"
|
49
71
|
Bundler.with_clean_env do
|
50
|
-
bundle_shell "librarian-chef", "install", "--verbose", realtime
|
51
|
-
line =~ /Installing.*</ ? line : nil
|
72
|
+
bundle_shell "librarian-chef", "install", "--verbose", :realtime => true, :indent => 2 do |line|
|
73
|
+
line =~ /Installing.*</ ? line.strip + "\n" : nil
|
52
74
|
end
|
53
75
|
end
|
54
76
|
Hobo.ui.separator
|
55
77
|
end
|
78
|
+
|
79
|
+
locate "*Berksfile" do
|
80
|
+
Hobo.ui.title "Installing chef dependencies via berkshelf"
|
81
|
+
Bundler.with_clean_env do
|
82
|
+
bundle_shell "berks", "install", :realtime => true, :indent => 2
|
83
|
+
bundle_shell "berks", "install", "--path", "cookbooks"
|
84
|
+
end
|
85
|
+
Hobo.ui.separator
|
86
|
+
end
|
56
87
|
end
|
57
|
-
end
|
88
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
desc "System configuration related commands"
|
2
|
+
namespace :system do
|
3
|
+
|
4
|
+
desc "Check system configuration for potential problems"
|
5
|
+
task :check do
|
6
|
+
Hobo::Lib::HostCheck.check.each do |k,v|
|
7
|
+
if v == :ok
|
8
|
+
Hobo.ui.success "#{k}: OK"
|
9
|
+
else
|
10
|
+
Hobo.ui.error "#{k}: FAILED\n"
|
11
|
+
Hobo.ui.warning v.advice.gsub(/^/, ' ')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
desc "System configuration related commands"
|
2
|
+
namespace :system do
|
3
|
+
|
4
|
+
desc "Shell completion related commands"
|
5
|
+
namespace :completions do
|
6
|
+
|
7
|
+
desc "Install shell completion helpers"
|
8
|
+
option '-f', '--fish', 'Install completions for FISH'
|
9
|
+
option '-b', '--bash', 'Install completions for Bash'
|
10
|
+
option '-z', '--zsh', 'Install completions for ZSH'
|
11
|
+
task "install" do |task|
|
12
|
+
if task.opts[:fish]
|
13
|
+
script = <<-EOF
|
14
|
+
function __hobo_completion -d "Create hobo completions"
|
15
|
+
set -l cache_dir "/tmp/fish_hobo_completion_cache"
|
16
|
+
mkdir -p $cache_dir
|
17
|
+
set -l hashed_pwd (ruby -r 'digest' -e 'puts Digest::MD5.hexdigest(`pwd`)')
|
18
|
+
set -l hobo_cache_file "$cache_dir/$hashed_pwd"
|
19
|
+
|
20
|
+
if not test -f "$hobo_cache_file"
|
21
|
+
hobo system completions fish > "$hobo_cache_file"
|
22
|
+
end
|
23
|
+
|
24
|
+
cat "$hobo_cache_file"
|
25
|
+
end
|
26
|
+
|
27
|
+
function __hobo_scope_test -d "Hobo scoped completion test"
|
28
|
+
set cmd (commandline -opc)
|
29
|
+
if [ "$cmd" = "$argv" ]
|
30
|
+
return 0
|
31
|
+
end
|
32
|
+
return 1
|
33
|
+
end
|
34
|
+
|
35
|
+
eval (__hobo_completion)
|
36
|
+
EOF
|
37
|
+
target = File.join(ENV['HOME'], '.config/fish/completions/hobo.fish')
|
38
|
+
FileUtils.mkdir_p(File.dirname(target))
|
39
|
+
File.write(target, script)
|
40
|
+
end
|
41
|
+
|
42
|
+
if task.opts[:bash]
|
43
|
+
raise "Bash completions not yet implemented"
|
44
|
+
end
|
45
|
+
|
46
|
+
if task.opts[:zsh]
|
47
|
+
raise "ZSH completions not yet implemented"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Display FISH shell completion commands"
|
52
|
+
task :fish do
|
53
|
+
Hobo.cli.slop.options.each do |option|
|
54
|
+
short = option.short.nil? ? '' : "-s #{option.short}"
|
55
|
+
long = option.long.nil? ? '' : "-l #{option.long}"
|
56
|
+
arg = option.config[:argument] ? '' : '-x'
|
57
|
+
puts "complete #{arg} -c hobo #{short} #{long} --description '#{option.description}';"
|
58
|
+
end
|
59
|
+
|
60
|
+
map = Hobo.cli.help_formatter.command_map
|
61
|
+
map.each do |k, v|
|
62
|
+
next if v.description.nil? || v.description.empty?
|
63
|
+
k = k.split(':')
|
64
|
+
k.unshift 'hobo'
|
65
|
+
c = k.pop
|
66
|
+
puts "complete -x -c hobo -n '__hobo_scope_test #{k.join(' ')}' -a #{c} --description '#{v.description}';"
|
67
|
+
v.options.each do |option|
|
68
|
+
short = option.short.nil? ? '' : "-s #{option.short}"
|
69
|
+
long = option.long.nil? ? '' : "-l #{option.long}"
|
70
|
+
arg = option.config[:argument] ? '' : '-x'
|
71
|
+
puts "complete #{arg} -c hobo -n '__hobo_scope_test #{k.concat([c]).join(' ')}' #{short} #{long} --description '#{option.description}';"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/hobo/tasks/tools.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
+
desc "Tasks to retrieve common tools"
|
2
|
+
hidden
|
1
3
|
namespace :tools do
|
4
|
+
|
5
|
+
desc "Fetch composer"
|
2
6
|
task :composer do
|
3
7
|
bin_file = File.join(Hobo.project_bin_path, "composer.phar")
|
4
8
|
unless File.exists?(bin_file)
|
5
|
-
Hobo.ui.success "Getting composer
|
9
|
+
Hobo.ui.success "Getting composer"
|
6
10
|
FileUtils.mkdir_p File.dirname(bin_file)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
Hobo.ui.separator
|
11
|
+
vm_shell "cd bin && php -r \"readfile('https://getcomposer.org/installer');\" | php", :realtime => true, :indent => 2
|
12
|
+
else
|
13
|
+
Hobo.ui.success "Composer already available in bin/composer.phar"
|
11
14
|
end
|
15
|
+
Hobo.ui.separator
|
12
16
|
end
|
13
|
-
end
|
17
|
+
end
|
data/lib/hobo/tasks/vm.rb
CHANGED
@@ -7,14 +7,43 @@ namespace :vm do
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
def vagrant_exec *args
|
11
|
+
opts = { :realtime => true, :indent => 2 }
|
12
|
+
color = Hobo.ui.supports_color? ? '--color' : '--no-color'
|
13
|
+
|
14
|
+
if OS.windows?
|
15
|
+
opts[:env] = { 'VAGRANT_HOME' => windows_short(dir) } if ENV['HOME'].match(/\s+/) && !ENV['VAGRANT_HOME']
|
16
|
+
end
|
17
|
+
|
18
|
+
args.unshift 'vagrant'
|
19
|
+
args.push color
|
20
|
+
args.push opts
|
21
|
+
|
22
|
+
bundle_shell *args
|
23
|
+
end
|
24
|
+
|
25
|
+
def windows_short dir
|
26
|
+
segments = dir.gsub(/\\/, '/').split('/')
|
27
|
+
segments.map do |segment|
|
28
|
+
if segment.match /\s+/
|
29
|
+
# This may fail in some edge cases but better than naught
|
30
|
+
# See the following link for the correct solution
|
31
|
+
# http://stackoverflow.com/questions/10224572/convert-long-filename-to-short-filename-8-3
|
32
|
+
segment.upcase.gsub(/\s+/, '')[0...6] + '~1'
|
33
|
+
else
|
34
|
+
segment
|
35
|
+
end
|
36
|
+
end.join('/')
|
37
|
+
end
|
38
|
+
|
10
39
|
desc "Start & provision VM"
|
11
|
-
task :up => [ 'deps:chef', '
|
40
|
+
task :up => [ 'deps:chef', 'assets:download', 'vm:start', 'vm:provision', 'deps:composer', 'assets:apply' ]
|
12
41
|
|
13
42
|
desc "Stop VM"
|
14
43
|
task :stop => [ "deps:gems" ] do
|
15
44
|
vagrantfile do
|
16
45
|
Hobo.ui.title "Stopping VM"
|
17
|
-
|
46
|
+
vagrant_exec 'suspend'
|
18
47
|
Hobo.ui.separator
|
19
48
|
end
|
20
49
|
end
|
@@ -26,7 +55,7 @@ namespace :vm do
|
|
26
55
|
task :destroy => [ "deps:gems" ] do
|
27
56
|
vagrantfile do
|
28
57
|
Hobo.ui.title "Destroying VM"
|
29
|
-
|
58
|
+
vagrant_exec 'destroy', '--force'
|
30
59
|
Hobo.ui.separator
|
31
60
|
end
|
32
61
|
end
|
@@ -35,7 +64,7 @@ namespace :vm do
|
|
35
64
|
task :start => [ "deps:gems", "deps:vagrant_plugins" ] do
|
36
65
|
vagrantfile do
|
37
66
|
Hobo.ui.title "Starting vagrant VM"
|
38
|
-
|
67
|
+
vagrant_exec 'up', '--no-provision'
|
39
68
|
Hobo.ui.separator
|
40
69
|
end
|
41
70
|
end
|
@@ -44,23 +73,47 @@ namespace :vm do
|
|
44
73
|
task :provision => [ "deps:gems" ] do
|
45
74
|
vagrantfile do
|
46
75
|
Hobo.ui.title "Provisioning VM"
|
47
|
-
|
76
|
+
vagrant_exec 'provision'
|
48
77
|
Hobo.ui.separator
|
49
78
|
end
|
50
79
|
end
|
51
80
|
|
52
81
|
desc "Open an SSH connection"
|
53
|
-
task :ssh do
|
54
|
-
|
82
|
+
task :ssh do |task|
|
83
|
+
execute = task.opts[:_unparsed]
|
84
|
+
opts = { :psuedo_tty => STDIN.tty? }
|
85
|
+
|
86
|
+
Hobo.ui.success "Determining VM connection details..." if STDOUT.tty?
|
87
|
+
command = execute.empty? ? vm_command(nil, opts) : vm_command(execute, opts)
|
88
|
+
Hobo.logger.debug "vm:ssh: #{command}"
|
89
|
+
|
90
|
+
Hobo.ui.success "Connecting..." if STDOUT.tty?
|
91
|
+
exec command
|
55
92
|
end
|
56
93
|
|
57
94
|
desc "Open a MySQL cli connection"
|
58
|
-
|
59
|
-
|
95
|
+
option '-D=', '--db=', 'Database'
|
96
|
+
task :mysql do |task|
|
97
|
+
opts = { :psuedo_tty => STDIN.tty? }
|
98
|
+
opts[:db] = task.opts[:db] if task.opts[:db]
|
99
|
+
|
100
|
+
Hobo.ui.success "Determining VM connection details..." if STDOUT.tty?
|
101
|
+
command = vm_mysql(opts)
|
102
|
+
Hobo.logger.debug "vm:mysql: #{command}"
|
103
|
+
|
104
|
+
Hobo.ui.success "Connecting..." if STDOUT.tty?
|
105
|
+
exec command
|
60
106
|
end
|
61
107
|
|
62
108
|
desc "Open a Redis cli connection"
|
63
109
|
task :redis do
|
64
|
-
|
110
|
+
opts = { :psuedo_tty => STDIN.tty? }
|
111
|
+
|
112
|
+
Hobo.ui.success "Determining VM connection details..." if STDOUT.tty?
|
113
|
+
command = vm_command("redis-cli", opts)
|
114
|
+
Hobo.logger.debug "vm:redis: #{command}"
|
115
|
+
|
116
|
+
Hobo.ui.success "Connecting..." if STDOUT.tty?
|
117
|
+
exec command
|
65
118
|
end
|
66
|
-
end
|
119
|
+
end
|
data/lib/hobo/ui.rb
CHANGED
@@ -6,6 +6,8 @@ module Hobo
|
|
6
6
|
end
|
7
7
|
|
8
8
|
class Ui
|
9
|
+
include Hobo::Logging
|
10
|
+
|
9
11
|
attr_accessor :interactive
|
10
12
|
|
11
13
|
COLORS = {
|
@@ -22,10 +24,12 @@ module Hobo
|
|
22
24
|
:description => [:bold]
|
23
25
|
}
|
24
26
|
|
25
|
-
def initialize
|
27
|
+
def initialize input = $stdin, output = $stdout, error = $stderr
|
26
28
|
HighLine.color_scheme = HighLine::ColorScheme.new COLORS
|
27
|
-
@
|
28
|
-
@
|
29
|
+
@output_io = output
|
30
|
+
@out = ::HighLine.new input, output
|
31
|
+
@error = ::HighLine.new input, error
|
32
|
+
use_color supports_color?
|
29
33
|
end
|
30
34
|
|
31
35
|
def color_scheme scheme = nil
|
@@ -33,15 +37,25 @@ module Hobo
|
|
33
37
|
HighLine.color_scheme
|
34
38
|
end
|
35
39
|
|
40
|
+
def supports_color?
|
41
|
+
return @output_io.tty? unless OS.windows?
|
42
|
+
return (ENV['ANSICON'] || ENV['TERM'] == 'xterm') && @output_io.tty? # ANSICON or MinTTY && output is TTY
|
43
|
+
end
|
44
|
+
|
36
45
|
def use_color opt = nil
|
37
46
|
HighLine.use_color = opt unless opt.nil?
|
38
47
|
HighLine.use_color?
|
39
48
|
end
|
40
49
|
|
41
50
|
def ask question, opts = {}
|
42
|
-
|
43
|
-
|
44
|
-
|
51
|
+
opts = {
|
52
|
+
:validate => nil,
|
53
|
+
:default => nil
|
54
|
+
}.merge(opts)
|
55
|
+
|
56
|
+
unless @interactive
|
57
|
+
raise Hobo::NonInteractiveError.new(question) if opts[:default].nil?
|
58
|
+
return opts[:default].to_s
|
45
59
|
end
|
46
60
|
|
47
61
|
question = "#{question} [#{opts[:default]}]" if opts[:default]
|
@@ -51,7 +65,8 @@ module Hobo
|
|
51
65
|
q.validate = opts[:validate] if opts[:validate]
|
52
66
|
q.readline
|
53
67
|
end
|
54
|
-
answer
|
68
|
+
answer = answer.to_s
|
69
|
+
answer.strip.empty? ? opts[:default].to_s : answer.strip
|
55
70
|
rescue EOFError
|
56
71
|
Hobo.ui.info ""
|
57
72
|
""
|
@@ -65,7 +80,7 @@ module Hobo
|
|
65
80
|
end
|
66
81
|
|
67
82
|
def separator
|
68
|
-
info ""
|
83
|
+
info(supports_color? ? "" : "\n")
|
69
84
|
end
|
70
85
|
|
71
86
|
def color *args
|
@@ -100,7 +115,9 @@ module Hobo
|
|
100
115
|
|
101
116
|
def say channel, message, color
|
102
117
|
return if message.nil?
|
103
|
-
|
118
|
+
message = color ? channel.color(message, color) : message
|
119
|
+
channel.say(message) unless logger.level <= Logger::DEBUG
|
120
|
+
logger.debug(message)
|
104
121
|
end
|
105
122
|
end
|
106
|
-
end
|
123
|
+
end
|
data/lib/hobo/util.rb
CHANGED
@@ -1,7 +1,41 @@
|
|
1
|
+
require 'ruby-progressbar'
|
2
|
+
|
1
3
|
module Hobo
|
2
4
|
class << self
|
5
|
+
|
6
|
+
attr_accessor :project_bar_cache
|
7
|
+
|
3
8
|
def in_project?
|
4
|
-
|
9
|
+
!!Hobo.project_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def progress file, increment, total, type, opts = {}
|
13
|
+
opts = {
|
14
|
+
:title => File.basename(file),
|
15
|
+
:total => total,
|
16
|
+
:format => "%t [%B] %p%% %e"
|
17
|
+
}.merge(opts)
|
18
|
+
|
19
|
+
# Hack to stop newline spam on windows
|
20
|
+
opts[:length] = 79 if Gem::win_platform?
|
21
|
+
|
22
|
+
@progress_bar_cache ||= {}
|
23
|
+
|
24
|
+
if type == :reset
|
25
|
+
type = :update
|
26
|
+
@progress_bar_cache.delete file
|
27
|
+
end
|
28
|
+
|
29
|
+
@progress_bar_cache[file] ||= ProgressBar.create(opts)
|
30
|
+
|
31
|
+
case type
|
32
|
+
when :update
|
33
|
+
@progress_bar_cache[file].progress += increment
|
34
|
+
when :finished
|
35
|
+
@progress_bar_cache[file].finish
|
36
|
+
end
|
37
|
+
|
38
|
+
return @progress_bar_cache[file]
|
5
39
|
end
|
6
40
|
end
|
7
|
-
end
|
41
|
+
end
|