cli-template 3.1.0 → 3.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +44 -5
- data/lib/cli-template/helpers.rb +3 -0
- data/lib/cli-template/sequence.rb +5 -4
- data/lib/cli-template/version.rb +1 -1
- data/lib/templates/colon_namespaces/%project_name%.gemspec.tt +30 -0
- data/lib/templates/colon_namespaces/.gitignore +16 -0
- data/lib/templates/colon_namespaces/.rspec +2 -0
- data/lib/templates/colon_namespaces/CHANGELOG.md +7 -0
- data/lib/templates/colon_namespaces/Gemfile.tt +6 -0
- data/lib/templates/colon_namespaces/Guardfile +19 -0
- data/lib/templates/colon_namespaces/LICENSE.txt.tt +22 -0
- data/lib/templates/colon_namespaces/README.md.tt +47 -0
- data/lib/templates/colon_namespaces/Rakefile +6 -0
- data/lib/templates/colon_namespaces/exe/%project_name%.tt +14 -0
- data/lib/templates/colon_namespaces/lib/%project_name%.rb.tt +12 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/cli.rb.tt +166 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/command.rb.tt +186 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/completer.rb.tt +145 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/completer/script.rb.tt +6 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/completer/script.sh.tt +16 -0
- data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/completions.rb.tt +0 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/help.rb.tt +9 -0
- data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/help/completions.md.tt +0 -0
- data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/help/completions/script.md.tt +0 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/help/hello.md.tt +5 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/help/main.md.tt +5 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/help/sub/goodbye.md.tt +5 -0
- data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/main.rb.tt +0 -0
- data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/rake_command.rb.tt +0 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/sub.rb.tt +12 -0
- data/lib/templates/colon_namespaces/lib/%underscored_name%/version.rb.tt +3 -0
- data/lib/templates/colon_namespaces/spec/lib/cli_spec.rb.tt +37 -0
- data/lib/templates/colon_namespaces/spec/spec_helper.rb.tt +29 -0
- data/lib/templates/default/.gitignore +1 -1
- data/lib/templates/default/Gemfile.lock.tt +64 -0
- data/lib/templates/default/LICENSE.txt +1 -1
- data/lib/templates/default/lib/%project_name%.rb.tt +1 -2
- data/lib/templates/default/lib/%underscored_name%/cli.rb.tt +24 -165
- data/lib/templates/default/lib/%underscored_name%/command.rb.tt +21 -151
- data/lib/templates/default/lib/%underscored_name%/completer.rb.tt +58 -57
- data/lib/templates/default/lib/%underscored_name%/completer/script.sh.tt +4 -10
- data/lib/templates/default/lib/%underscored_name%/completion.rb.tt +15 -0
- data/lib/templates/default/lib/%underscored_name%/help/completion.md.tt +22 -0
- data/lib/templates/default/lib/%underscored_name%/help/completion_script.md.tt +3 -0
- data/lib/templates/default/spec/lib/cli_spec.rb.tt +18 -17
- data/lib/templates/default/spec/spec_helper.rb.tt +2 -2
- data/spec/lib/cli_spec.rb +49 -38
- metadata +35 -7
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "active_support" # for autoload
|
|
3
|
+
require "active_support/core_ext"
|
|
4
|
+
|
|
5
|
+
# Override thor's long_desc identation behavior
|
|
6
|
+
# https://github.com/erikhuda/thor/issues/398
|
|
7
|
+
class Thor
|
|
8
|
+
module Shell
|
|
9
|
+
class Basic
|
|
10
|
+
def print_wrapped(message, options = {})
|
|
11
|
+
message = "\n#{message}" unless message[0] == "\n"
|
|
12
|
+
stdout.puts message
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module <%= project_class_name %>
|
|
19
|
+
class Command < Thor
|
|
20
|
+
class << self
|
|
21
|
+
# thor_args is an array of commands. Examples:
|
|
22
|
+
# ["help"]
|
|
23
|
+
# ["dynamodb:migrate"]
|
|
24
|
+
#
|
|
25
|
+
# Same signature as RakeCommand.perform. Signature is a little weird
|
|
26
|
+
# with some repetition. Examples:
|
|
27
|
+
#
|
|
28
|
+
# <%= project_class_name %>::Main.perform("hello", ["hello"])
|
|
29
|
+
# <%= project_class_name %>::Main.perform("dynamodb:migrate", ["migrate"])
|
|
30
|
+
#
|
|
31
|
+
def perform(full_command, thor_args)
|
|
32
|
+
config = {} # doesnt seem like config is used
|
|
33
|
+
dispatch(nil, thor_args, nil, config)
|
|
34
|
+
rescue Thor::InvocationError => e
|
|
35
|
+
puts e.message
|
|
36
|
+
puts " ufo #{full_command} -h # for more help"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Track all command subclasses.
|
|
41
|
+
def subclasses
|
|
42
|
+
@subclasses ||= []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inherited(base)
|
|
46
|
+
super
|
|
47
|
+
|
|
48
|
+
if base.name
|
|
49
|
+
self.subclasses << base
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Useful for help menu when we need to have all the definitions loaded.
|
|
54
|
+
# Using constantize instead of require so we dont care about
|
|
55
|
+
# order. The eager load actually uses autoloading.
|
|
56
|
+
@@eager_loaded = false
|
|
57
|
+
def eager_load!
|
|
58
|
+
return if @@eager_loaded
|
|
59
|
+
|
|
60
|
+
path = File.expand_path("../../", __FILE__)
|
|
61
|
+
Dir.glob("#{path}/<%= underscored_name %>/**/*.rb").select do |path|
|
|
62
|
+
next if !File.file?(path)
|
|
63
|
+
|
|
64
|
+
class_name = path
|
|
65
|
+
.sub(/\.rb$/,'')
|
|
66
|
+
.sub(%r{.*/lib/},'')
|
|
67
|
+
.classify
|
|
68
|
+
|
|
69
|
+
if class_name.include?('-')
|
|
70
|
+
puts "WARN: Unable to autoload a class with a dash in the name" if debug?
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class_name = class_map[class_name] || class_name
|
|
75
|
+
|
|
76
|
+
puts "eager_load! loading path: #{path} class_name: #{class_name}" if debug?
|
|
77
|
+
class_name.constantize # dont have to worry about order.
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@@eager_loaded = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Special class mapping cases. This is because ActiveSupport's autoloading
|
|
84
|
+
# forces a specific naming convention.
|
|
85
|
+
def class_map
|
|
86
|
+
map = {
|
|
87
|
+
"<%= project_class_name %>::Cli" => "<%= project_class_name %>::CLI",
|
|
88
|
+
"<%= project_class_name %>::Version" => "<%= project_class_name %>::VERSION",
|
|
89
|
+
"<%= project_class_name %>::Completion" => "<%= project_class_name %>::Completions",
|
|
90
|
+
}
|
|
91
|
+
map.merge(additional_class_map)
|
|
92
|
+
map
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Override this if you need add addtional class mappings.
|
|
96
|
+
def additional_class_map
|
|
97
|
+
{}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Fully qualifed task names. Examples:
|
|
101
|
+
# hello
|
|
102
|
+
# sub:goodbye
|
|
103
|
+
def namespaced_commands
|
|
104
|
+
eager_load!
|
|
105
|
+
subclasses.map do |klass|
|
|
106
|
+
klass.all_tasks.keys.map do |task_name|
|
|
107
|
+
klass = klass.to_s.sub('<%= project_class_name %>::','')
|
|
108
|
+
namespace = klass =~ /^Main/ ? nil : klass.underscore.gsub('/',':')
|
|
109
|
+
[namespace, task_name].compact.join(':')
|
|
110
|
+
end
|
|
111
|
+
end.flatten.sort
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Use <%= project_class_name %> banner instead of Thor to account for namespaces in commands.
|
|
115
|
+
def banner(command, namespace = nil, subcommand = false)
|
|
116
|
+
namespace = namespace_from_class(self)
|
|
117
|
+
command_name = command.usage # set with desc when defining tht Thor class
|
|
118
|
+
namespaced_command = [namespace, command_name].compact.join(':')
|
|
119
|
+
|
|
120
|
+
"<%= project_name %> #{namespaced_command}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def namespace_from_class(klass)
|
|
124
|
+
namespace = klass.to_s.sub('<%= project_class_name %>::', '').underscore.gsub('/',':')
|
|
125
|
+
namespace unless namespace == "main"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def help_list(all=false)
|
|
129
|
+
# hack to show hidden comands when requested
|
|
130
|
+
Thor::HiddenCommand.class_eval do
|
|
131
|
+
def hidden?; false; end
|
|
132
|
+
end if all
|
|
133
|
+
|
|
134
|
+
list = []
|
|
135
|
+
eager_load!
|
|
136
|
+
subclasses.each do |klass|
|
|
137
|
+
commands = klass.printable_commands(true, false)
|
|
138
|
+
commands.reject! { |array| array[0].include?(':help') }
|
|
139
|
+
list += commands
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
list.sort_by! { |array| array[0] }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Example:
|
|
146
|
+
# klass_from_namespace(nil) => Main
|
|
147
|
+
# klass_from_namespace("sub") => Sub
|
|
148
|
+
def klass_from_namespace(namespace)
|
|
149
|
+
if namespace.nil?
|
|
150
|
+
<%= project_class_name %>::Main
|
|
151
|
+
else
|
|
152
|
+
class_name = namespace.gsub(':','/')
|
|
153
|
+
class_name = "<%= project_class_name %>::#{class_name.classify}"
|
|
154
|
+
class_name = class_map[class_name] || class_name
|
|
155
|
+
class_name.constantize
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# If this fails to match then it'l just return the original full command
|
|
160
|
+
def autocomplete(full_command)
|
|
161
|
+
return nil if full_command.nil? # <%= project_name %> help
|
|
162
|
+
|
|
163
|
+
eager_load!
|
|
164
|
+
|
|
165
|
+
words = full_command.split(':')
|
|
166
|
+
namespace = words[0..-2].join(':') if words.size > 1
|
|
167
|
+
command = words.last
|
|
168
|
+
|
|
169
|
+
# Thor's normalize_command_name autocompletes the command but then we need to add the namespace back
|
|
170
|
+
begin
|
|
171
|
+
thor_subclass = klass_from_namespace(namespace) # could NameError
|
|
172
|
+
command = thor_subclass.normalize_command_name(command) # could Thor::AmbiguousCommandError
|
|
173
|
+
[namespace, command].compact.join(':')
|
|
174
|
+
rescue NameError
|
|
175
|
+
full_command # return original full_command
|
|
176
|
+
rescue Thor::AmbiguousCommandError => e
|
|
177
|
+
full_command # return original full_command
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def debug?
|
|
182
|
+
ENV['DEBUG'] && !ENV['TEST']
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Code Explanation. This is mainly focused on the run method.
|
|
2
|
+
#
|
|
3
|
+
# There are 3 main branches of logic for completions:
|
|
4
|
+
#
|
|
5
|
+
# 1. top-level commands - when there are zero completed words
|
|
6
|
+
# 2. params completions - when a command has some required params
|
|
7
|
+
# 3. options completions - when we have finished auto-completing the top-level command and required params, the rest of the completion words will be options
|
|
8
|
+
#
|
|
9
|
+
# Terms:
|
|
10
|
+
#
|
|
11
|
+
# params - these are params in the command itself. Example: for the method `scale(service, count)` the params would be `service, count`.
|
|
12
|
+
# options - these are cli options flags. Examples: --noop, --verbose
|
|
13
|
+
#
|
|
14
|
+
# When we are done processing method params, the completions will be only options. When the detected params size is greater than the arity we are have finished auto-completing the parameters in the method declaration. For example, say you had a method for a CLI command with the following form:
|
|
15
|
+
#
|
|
16
|
+
# scale(service, count) = arity of 2
|
|
17
|
+
#
|
|
18
|
+
# <%= project_name %> scale service count [TAB] # there are 3 params including the "scale" command
|
|
19
|
+
#
|
|
20
|
+
# So the completions will be something like:
|
|
21
|
+
#
|
|
22
|
+
# --noop --verbose etc
|
|
23
|
+
#
|
|
24
|
+
# A note about artity values:
|
|
25
|
+
#
|
|
26
|
+
# We are using the arity of the command method to determine if we have finish auto-completing the params completions. When the ruby method has a splat param, it's arity will be negative. Here are some example methods and their arities.
|
|
27
|
+
#
|
|
28
|
+
# ship(service) = 1
|
|
29
|
+
# scale(service, count) = 2
|
|
30
|
+
# ships(*services) = -1
|
|
31
|
+
# foo(example, *rest) = -2
|
|
32
|
+
#
|
|
33
|
+
# Fortunately, negative and positive arity values are processed the same way. So we take simply take the abs of the arity.
|
|
34
|
+
#
|
|
35
|
+
# To test:
|
|
36
|
+
#
|
|
37
|
+
# <%= project_name %> completions
|
|
38
|
+
# <%= project_name %> completions hello
|
|
39
|
+
# <%= project_name %> completions hello name
|
|
40
|
+
# <%= project_name %> completions hello name --
|
|
41
|
+
# <%= project_name %> completions hello name --noop
|
|
42
|
+
#
|
|
43
|
+
# <%= project_name %> completions
|
|
44
|
+
# <%= project_name %> completions sub:goodbye
|
|
45
|
+
# <%= project_name %> completions sub:goodbye name
|
|
46
|
+
#
|
|
47
|
+
# Note when testing, the first top-level word must be an exact match
|
|
48
|
+
#
|
|
49
|
+
# <%= project_name %> completions hello # works fine
|
|
50
|
+
# <%= project_name %> completions he # incomplete, this will just break
|
|
51
|
+
#
|
|
52
|
+
# The completions assumes that the top-level word that is being passed in
|
|
53
|
+
# from completor/scripts.sh will always match exactly. This must be the
|
|
54
|
+
# case. For parameters, the word does not have to match exactly.
|
|
55
|
+
#
|
|
56
|
+
module <%= project_class_name %>
|
|
57
|
+
class Completer
|
|
58
|
+
autoload :Script, '<%= underscored_name %>/completer/script'
|
|
59
|
+
|
|
60
|
+
def initialize(*params)
|
|
61
|
+
@params = params
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run
|
|
65
|
+
if @params.size == 0
|
|
66
|
+
puts all_commands
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# will only get to here if the top-level command has been fully auto-completed.
|
|
71
|
+
arity = command_class.instance_method(trailing_command).arity.abs
|
|
72
|
+
if @params.size <= arity
|
|
73
|
+
puts params_completions(current_command)
|
|
74
|
+
else
|
|
75
|
+
puts options_completions(current_command)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# all top-level commands
|
|
80
|
+
def all_commands
|
|
81
|
+
# Interesing, extra :help commands show up here but no whne using
|
|
82
|
+
# <%= project_class_name %>::Command.help_list in main_help -> thor_list
|
|
83
|
+
# We'll filter out :help for auto-completion.
|
|
84
|
+
commands = <%= project_class_name %>::Command.namespaced_commands
|
|
85
|
+
commands.reject { |c| c =~ /:help$/ }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def params_completions(current_command)
|
|
89
|
+
method_params = command_class.instance_method(trailing_command).parameters
|
|
90
|
+
# Example:
|
|
91
|
+
# >> Sub.instance_method(:goodbye).parameters
|
|
92
|
+
# => [[:req, :name]]
|
|
93
|
+
# >>
|
|
94
|
+
method_params.map!(&:last)
|
|
95
|
+
|
|
96
|
+
offset = @params.size - 1
|
|
97
|
+
offset_params = method_params[offset..-1]
|
|
98
|
+
method_params[offset..-1].first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def options_completions(current_command)
|
|
102
|
+
used = ARGV.select { |a| a.include?('--') } # so we can remove used options
|
|
103
|
+
|
|
104
|
+
method_options = command_class.all_commands[trailing_command].options.keys
|
|
105
|
+
class_options = command_class.class_options.keys
|
|
106
|
+
|
|
107
|
+
all_options = method_options + class_options + ['help']
|
|
108
|
+
|
|
109
|
+
all_options.map! { |o| "--#{o.to_s.dasherize}" }
|
|
110
|
+
filtered_options = all_options - used
|
|
111
|
+
filtered_options.uniq
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def current_command
|
|
115
|
+
@params[0]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Example: sub:goodbye => "sub"
|
|
119
|
+
def namespace
|
|
120
|
+
return nil unless current_command
|
|
121
|
+
|
|
122
|
+
if current_command.include?(':')
|
|
123
|
+
words = current_command.split(':')
|
|
124
|
+
words.pop
|
|
125
|
+
words.join(':')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Example: sub:goodbye => "goodbye"
|
|
130
|
+
def trailing_command
|
|
131
|
+
current_command.split(':').last
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def command_class
|
|
135
|
+
@command_class ||= <%= project_class_name %>::Command.klass_from_namespace(namespace)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Useful for debugging. Using puts messes up completion.
|
|
139
|
+
def log(msg)
|
|
140
|
+
File.open("/tmp/complete.log", "a") do |file|
|
|
141
|
+
file.puts(msg)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
_<%= project_name %>() {
|
|
2
|
+
COMPREPLY=()
|
|
3
|
+
local word="${COMP_WORDS[COMP_CWORD]}"
|
|
4
|
+
|
|
5
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
6
|
+
local completions=$(<%= project_name %> completions)
|
|
7
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
|
|
8
|
+
else
|
|
9
|
+
local words=("${COMP_WORDS[@]}")
|
|
10
|
+
unset words[0]
|
|
11
|
+
local completions=$(<%= project_name %> completions ${words[@]})
|
|
12
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$word") )
|
|
13
|
+
fi
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
complete -F _<%= project_name %> <%= project_name %>
|
|
File without changes
|
data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/help/completions.md.tt
RENAMED
|
File without changes
|
data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/help/completions/script.md.tt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module <%= project_class_name %>
|
|
2
|
+
class Sub < Command
|
|
3
|
+
|
|
4
|
+
desc "goodbye NAME", "say goodbye to NAME"
|
|
5
|
+
long_desc Help.text("sub:goodbye")
|
|
6
|
+
option :from, desc: "from person"
|
|
7
|
+
def goodbye(name="you")
|
|
8
|
+
puts "from: #{options[:from]}" if options[:from]
|
|
9
|
+
puts "Goodbye #{name}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe <%= project_class_name %>::CLI do
|
|
4
|
+
before(:all) do
|
|
5
|
+
@args = "--from Tung"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe "<%= project_name %>" do
|
|
9
|
+
it "hello" do
|
|
10
|
+
out = execute("exe/<%= project_name %> hello world #{@args}")
|
|
11
|
+
expect(out).to include("from: Tung\nHello world")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "goodbye" do
|
|
15
|
+
out = execute("exe/<%= project_name %> sub:goodbye world #{@args}")
|
|
16
|
+
expect(out).to include("from: Tung\nGoodbye world")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "completions" do
|
|
20
|
+
out = execute("exe/<%= project_name %> completions")
|
|
21
|
+
expect(out).to include("hello")
|
|
22
|
+
expect(out).to include("sub:goodbye")
|
|
23
|
+
|
|
24
|
+
out = execute("exe/<%= project_name %> completions hello")
|
|
25
|
+
expect(out).to include("name")
|
|
26
|
+
|
|
27
|
+
out = execute("exe/<%= project_name %> completions hello name")
|
|
28
|
+
expect(out).to include("--from")
|
|
29
|
+
|
|
30
|
+
out = execute("exe/<%= project_name %> completions sub:goodbye")
|
|
31
|
+
expect(out).to include("name")
|
|
32
|
+
|
|
33
|
+
out = execute("exe/<%= project_name %> completions sub:goodbye name")
|
|
34
|
+
expect(out).to include("--from")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|