jls-clamp 0.3.1
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.
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/Gemfile +9 -0
- data/README.markdown +274 -0
- data/Rakefile +12 -0
- data/clamp.gemspec +24 -0
- data/examples/flipflop +31 -0
- data/examples/fubar +23 -0
- data/examples/gitdown +61 -0
- data/examples/speak +33 -0
- data/lib/clamp/attribute.rb +40 -0
- data/lib/clamp/attribute_declaration.rb +40 -0
- data/lib/clamp/command.rb +142 -0
- data/lib/clamp/errors.rb +26 -0
- data/lib/clamp/help.rb +100 -0
- data/lib/clamp/option/declaration.rb +57 -0
- data/lib/clamp/option/parsing.rb +59 -0
- data/lib/clamp/option.rb +80 -0
- data/lib/clamp/parameter/declaration.rb +28 -0
- data/lib/clamp/parameter/parsing.rb +24 -0
- data/lib/clamp/parameter.rb +80 -0
- data/lib/clamp/subcommand/declaration.rb +44 -0
- data/lib/clamp/subcommand/parsing.rb +41 -0
- data/lib/clamp/subcommand.rb +23 -0
- data/lib/clamp/version.rb +3 -0
- data/lib/clamp.rb +3 -0
- data/spec/clamp/command_group_spec.rb +267 -0
- data/spec/clamp/command_spec.rb +766 -0
- data/spec/clamp/option_module_spec.rb +37 -0
- data/spec/clamp/option_spec.rb +149 -0
- data/spec/clamp/parameter_spec.rb +201 -0
- data/spec/spec_helper.rb +45 -0
- metadata +84 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
Clamp
|
2
|
+
=====
|
3
|
+
|
4
|
+
"Clamp" is a minimal framework for command-line utilities.
|
5
|
+
|
6
|
+
It handles boring stuff like parsing the command-line, and generating help, so you can get on with making your command actually do stuff.
|
7
|
+
|
8
|
+
Not another one!
|
9
|
+
----------------
|
10
|
+
|
11
|
+
Yeah, sorry. There are a bunch of existing command-line parsing libraries out there, and Clamp draws inspiration from a variety of sources, including [Thor], [optparse], and [Clip]. In the end, though, I wanted a slightly rounder wheel.
|
12
|
+
|
13
|
+
[optparse]: http://ruby-doc.org/stdlib/libdoc/optparse/rdoc/index.html
|
14
|
+
[Thor]: http://github.com/wycats/thor
|
15
|
+
[Clip]: http://clip.rubyforge.org/
|
16
|
+
|
17
|
+
Quick Start
|
18
|
+
-----------
|
19
|
+
|
20
|
+
Clamp models a command as a Ruby class; a subclass of `Clamp::Command`. They look something like this:
|
21
|
+
|
22
|
+
class SpeakCommand < Clamp::Command
|
23
|
+
|
24
|
+
option "--loud", :flag, "say it loud"
|
25
|
+
option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
|
26
|
+
Integer(s)
|
27
|
+
end
|
28
|
+
|
29
|
+
parameter "WORDS ...", "the thing to say", :attribute_name => :words
|
30
|
+
|
31
|
+
def execute
|
32
|
+
the_truth = words.join(" ")
|
33
|
+
the_truth.upcase! if loud?
|
34
|
+
iterations.times do
|
35
|
+
puts the_truth
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
Calling `run` on a command class creates an instance of it, then invokes it using command-line arguments (from ARGV, by default).
|
42
|
+
|
43
|
+
SpeakCommand.run
|
44
|
+
|
45
|
+
Class-level methods like `option` and `parameter` declare attributes (in a similar way to `attr_accessor`), and arrange for them to be populated automatically based on command-line arguments. They are also used to generate `help` documentation.
|
46
|
+
|
47
|
+
Declaring options
|
48
|
+
-----------------
|
49
|
+
|
50
|
+
Options are declared using the `option` method. The three required arguments are:
|
51
|
+
|
52
|
+
1. the option switch (or switches),
|
53
|
+
2. an option argument name
|
54
|
+
3. a short description
|
55
|
+
|
56
|
+
For example:
|
57
|
+
|
58
|
+
option "--flavour", "FLAVOUR", "ice-cream flavour"
|
59
|
+
|
60
|
+
It works a little like `attr_accessor`, defining reader and writer methods on the command class. The attribute name is derived from the switch (in this case, "`flavour`"). When you pass options to your command, Clamp will populate the attributes, which are then available for use in your `#execute` method.
|
61
|
+
|
62
|
+
def execute
|
63
|
+
puts "You chose #{flavour}. Excellent choice!"
|
64
|
+
end
|
65
|
+
|
66
|
+
If you don't like the inferred attribute name, you can override it:
|
67
|
+
|
68
|
+
option "--type", "TYPE", "type of widget", :attribute_name => :widget_type
|
69
|
+
# to avoid clobbering Object#type
|
70
|
+
|
71
|
+
### Short/long option switches
|
72
|
+
|
73
|
+
The first argument to `option` can be an array, rather than a single string, in which case all the switches are treated as aliases:
|
74
|
+
|
75
|
+
option ["-s", "--subject"], "SUBJECT", "email subject line"
|
76
|
+
|
77
|
+
### Flag options
|
78
|
+
|
79
|
+
Some options are just boolean flags. Pass "`:flag`" as the second parameter to tell Clamp not to expect an option argument:
|
80
|
+
|
81
|
+
option "--verbose", :flag, "be chatty"
|
82
|
+
|
83
|
+
For flag options, Clamp appends "`?`" to the generated reader method; ie. you get a method called "`#verbose?`", rather than just "`#verbose`".
|
84
|
+
|
85
|
+
Negatable flags are easy to generate, too:
|
86
|
+
|
87
|
+
option "--[no-]force", :flag, "be forceful (or not)"
|
88
|
+
|
89
|
+
Clamp will handle both "`--force`" and "`--no-force`" options, setting the value of "`#force?`" appropriately.
|
90
|
+
|
91
|
+
Declaring parameters
|
92
|
+
--------------------
|
93
|
+
|
94
|
+
Positional parameters can be declared using `parameter`, specifying
|
95
|
+
|
96
|
+
1. the parameter name, and
|
97
|
+
2. a short description
|
98
|
+
|
99
|
+
For example:
|
100
|
+
|
101
|
+
parameter "SRC", "source file"
|
102
|
+
|
103
|
+
Like options, parameters are implemented as attributes of the command, with the default attribute name derived from the parameter name (in this case, "`src`"). By convention, parameter names are specified in uppercase, to make them obvious in usage help.
|
104
|
+
|
105
|
+
### Optional parameters
|
106
|
+
|
107
|
+
Wrapping a parameter name in square brackets indicates that it's optional, e.g.
|
108
|
+
|
109
|
+
parameter "[TARGET_DIR]", "target directory"
|
110
|
+
|
111
|
+
### Greedy parameters
|
112
|
+
|
113
|
+
Three dots at the end of a parameter name makes it "greedy" - it will consume all remaining command-line arguments. For example:
|
114
|
+
|
115
|
+
parameter "FILE ...", "input files"
|
116
|
+
|
117
|
+
The suffix "`_list`" is appended to the default attribute name for greedy parameters; in this case, an attribute called "`file_list`" would be generated.
|
118
|
+
|
119
|
+
Parsing and validation of options and parameters
|
120
|
+
------------------------------------------------
|
121
|
+
|
122
|
+
When you `#run` a command, it will first attempt to `#parse` command-line arguments, and map them onto the declared options and parameters, before invoking your `#execute` method.
|
123
|
+
|
124
|
+
Clamp will verify that all required (ie. non-optional) parameters are present, and signal a error if they aren't.
|
125
|
+
|
126
|
+
### Validation block
|
127
|
+
|
128
|
+
Both `option` and `parameter` accept an optional block. If present, the block will be
|
129
|
+
called with the raw string option argument, and is expected to coerce it to
|
130
|
+
the correct type, e.g.
|
131
|
+
|
132
|
+
option "--port", "PORT", "port to listen on" do |s|
|
133
|
+
Integer(s)
|
134
|
+
end
|
135
|
+
|
136
|
+
If the block raises an ArgumentError, Clamp will catch it, and report that the value was bad:
|
137
|
+
|
138
|
+
!!!plain
|
139
|
+
ERROR: option '--port': invalid value for Integer: "blah"
|
140
|
+
|
141
|
+
### Advanced option/parameter handling
|
142
|
+
|
143
|
+
While Clamp provides an attribute-writer method for each declared option or parameter, you always have the option of overriding it to provide custom argument-handling logic, e.g.
|
144
|
+
|
145
|
+
parameter "SERVER", "location of server"
|
146
|
+
|
147
|
+
def server=(server)
|
148
|
+
@server_address, @server_port = server.split(":")
|
149
|
+
end
|
150
|
+
|
151
|
+
### Default values
|
152
|
+
|
153
|
+
Default values can be specified for options:
|
154
|
+
|
155
|
+
option "--flavour", "FLAVOUR", "ice-cream flavour", :default => "chocolate"
|
156
|
+
|
157
|
+
and also for optional parameters
|
158
|
+
|
159
|
+
parameter "[HOST]", "server host", :default => "localhost"
|
160
|
+
|
161
|
+
For more advanced cases, you can also specify default values by defining a method called "`default_#{attribute_name}`":
|
162
|
+
|
163
|
+
option "--http-port", "PORT", "web-server port", :default => 9000
|
164
|
+
|
165
|
+
option "--admin-port", "PORT", "admin port"
|
166
|
+
|
167
|
+
def default_admin_port
|
168
|
+
http_port + 1
|
169
|
+
end
|
170
|
+
|
171
|
+
Declaring Subcommands
|
172
|
+
---------------------
|
173
|
+
|
174
|
+
Subcommand support helps you wrap a number of related commands into a single script (ala tools like "`git`"). Clamp will inspect the first command-line argument (after options are parsed), and delegate to the named subcommand.
|
175
|
+
|
176
|
+
Unsuprisingly, subcommands are declared using the `subcommand` method. e.g.
|
177
|
+
|
178
|
+
class MainCommand < Clamp::Command
|
179
|
+
|
180
|
+
subcommand "init", "Initialize the repository" do
|
181
|
+
|
182
|
+
def execute
|
183
|
+
# ...
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
Clamp generates an anonymous subclass of the current class, to represent the subcommand. Alternatively, you can provide an explicit subcommand class:
|
191
|
+
|
192
|
+
class MainCommand < Clamp::Command
|
193
|
+
|
194
|
+
subcommand "init", "Initialize the repository", InitCommand
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
class InitCommand < Clamp::Command
|
199
|
+
|
200
|
+
def execute
|
201
|
+
# ...
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
### Default subcommand
|
207
|
+
|
208
|
+
You can set a default subcommand, at the class level, as follows:
|
209
|
+
|
210
|
+
class MainCommand < Clamp::Command
|
211
|
+
|
212
|
+
self.default_subcommand = "status"
|
213
|
+
|
214
|
+
subcommand "status", "Display current status" do
|
215
|
+
|
216
|
+
def execute
|
217
|
+
# ...
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
Then, if when no SUBCOMMAND argument is provided, the default will be selected.
|
225
|
+
|
226
|
+
### Subcommand options and parameters
|
227
|
+
|
228
|
+
Options are inheritable, so any options declared for a command are supported for it's sub-classes (e.g. those created using `subcommand`). Parameters, on the other hand, are not inherited - each subcommand must declare it's own parameter list.
|
229
|
+
|
230
|
+
Note that, if a subcommand accepts options, they must be specified on the command-line _after_ the subcommand name.
|
231
|
+
|
232
|
+
Getting help
|
233
|
+
------------
|
234
|
+
|
235
|
+
All Clamp commands support a "`--help`" option, which outputs brief usage documentation, based on those seemingly useless extra parameters that you had to pass to `option` and `parameter`.
|
236
|
+
|
237
|
+
$ speak --help
|
238
|
+
Usage:
|
239
|
+
speak [OPTIONS] WORDS ...
|
240
|
+
|
241
|
+
Arguments:
|
242
|
+
WORDS ... the thing to say
|
243
|
+
|
244
|
+
Options:
|
245
|
+
--loud say it loud
|
246
|
+
-n, --iterations N say it N times (default: 1)
|
247
|
+
-h, --help print help
|
248
|
+
|
249
|
+
License
|
250
|
+
-------
|
251
|
+
|
252
|
+
Copyright (C) 2011 [Mike Williams](mailto:mdub@dogbiscuit.org)
|
253
|
+
|
254
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
255
|
+
of this software and associated documentation files (the "Software"), to
|
256
|
+
deal in the Software without restriction, including without limitation the
|
257
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
258
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
259
|
+
furnished to do so, subject to the following conditions:
|
260
|
+
|
261
|
+
The above copyright notice and this permission notice shall be included in
|
262
|
+
all copies or substantial portions of the Software.
|
263
|
+
|
264
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
265
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
266
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
267
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
268
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
269
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
270
|
+
|
271
|
+
Contributing to Clamp
|
272
|
+
---------------------
|
273
|
+
|
274
|
+
Source-code for Clamp is [on Github](https://github.com/mdub/clamp).
|
data/Rakefile
ADDED
data/clamp.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "clamp/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
|
7
|
+
s.name = "jls-clamp"
|
8
|
+
s.version = Clamp::VERSION.dup
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Mike Williams"]
|
11
|
+
s.email = "mdub@dogbiscuit.org"
|
12
|
+
s.homepage = "http://github.com/mdub/clamp"
|
13
|
+
|
14
|
+
s.summary = %q{a minimal framework for command-line utilities}
|
15
|
+
s.description = <<EOF
|
16
|
+
Clamp provides an object-model for command-line utilities.
|
17
|
+
It handles parsing of command-line options, and generation of usage help.
|
18
|
+
EOF
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
|
24
|
+
end
|
data/examples/flipflop
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
# An example of subcommands
|
4
|
+
|
5
|
+
require "clamp"
|
6
|
+
require "clamp/version"
|
7
|
+
|
8
|
+
class FlipFlop < Clamp::Command
|
9
|
+
|
10
|
+
option ["--version", "-v"], :flag, "Show version" do
|
11
|
+
puts "Powered by Clamp-#{Clamp::VERSION}"
|
12
|
+
exit(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
self.default_subcommand = "flip"
|
16
|
+
|
17
|
+
subcommand "flip", "flip it" do
|
18
|
+
def execute
|
19
|
+
puts "FLIPPED"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
subcommand "flop", "flop it" do
|
24
|
+
def execute
|
25
|
+
puts "FLOPPED"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
FlipFlop.run
|
data/examples/fubar
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
# An example of subcommands
|
4
|
+
|
5
|
+
require "clamp"
|
6
|
+
|
7
|
+
class Fubar < Clamp::Command
|
8
|
+
|
9
|
+
subcommand "foo", "Foo!" do
|
10
|
+
|
11
|
+
subcommand "bar", "Baaaa!" do
|
12
|
+
|
13
|
+
def execute
|
14
|
+
puts "FUBAR"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
Fubar.run
|
data/examples/gitdown
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
# Demonstrate how subcommands can be declared as classes
|
4
|
+
|
5
|
+
require "clamp"
|
6
|
+
|
7
|
+
module GitDown
|
8
|
+
|
9
|
+
class AbstractCommand < Clamp::Command
|
10
|
+
|
11
|
+
option ["-v", "--verbose"], :flag, "be verbose"
|
12
|
+
|
13
|
+
option "--version", :flag, "show version" do
|
14
|
+
puts "GitDown-0.0.0a"
|
15
|
+
exit(0)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class CloneCommand < AbstractCommand
|
21
|
+
|
22
|
+
parameter "REPOSITORY", "repository to clone"
|
23
|
+
parameter "[DIR]", "working directory", :default => "."
|
24
|
+
|
25
|
+
def execute
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
class PullCommand < AbstractCommand
|
32
|
+
|
33
|
+
option "--[no-]commit", :flag, "Perform the merge and commit the result."
|
34
|
+
|
35
|
+
def execute
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class StatusCommand < AbstractCommand
|
42
|
+
|
43
|
+
option ["-s", "--short"], :flag, "Give the output in the short-format."
|
44
|
+
|
45
|
+
def execute
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class MainCommand < AbstractCommand
|
52
|
+
|
53
|
+
subcommand "clone", "Clone a remote repository.", CloneCommand
|
54
|
+
subcommand "pull", "Fetch and merge updates.", PullCommand
|
55
|
+
subcommand "status", "Display status of local repository.", StatusCommand
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
GitDown::MainCommand.run
|
data/examples/speak
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
# A simple Clamp command, with options and parameters
|
4
|
+
|
5
|
+
require "clamp"
|
6
|
+
|
7
|
+
class SpeakCommand < Clamp::Command
|
8
|
+
|
9
|
+
self.description = %{
|
10
|
+
Say something.
|
11
|
+
}
|
12
|
+
|
13
|
+
option "--loud", :flag, "say it loud"
|
14
|
+
option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
|
15
|
+
Integer(s)
|
16
|
+
end
|
17
|
+
|
18
|
+
parameter "WORDS ...", "the thing to say", :attribute_name => :words
|
19
|
+
|
20
|
+
def execute
|
21
|
+
|
22
|
+
the_truth = words.join(" ")
|
23
|
+
the_truth.upcase! if loud?
|
24
|
+
|
25
|
+
iterations.times do
|
26
|
+
puts the_truth
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
SpeakCommand.run
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Attribute
|
4
|
+
|
5
|
+
attr_reader :description, :attribute_name, :default_value, :env_var
|
6
|
+
|
7
|
+
def help_rhs
|
8
|
+
rhs = description
|
9
|
+
if defined?(@default_value)
|
10
|
+
rhs += " (default: #{@default_value.inspect})"
|
11
|
+
end
|
12
|
+
if defined?(@env_var)
|
13
|
+
rhs += " (env: #{@env_var.inspect})"
|
14
|
+
end
|
15
|
+
rhs
|
16
|
+
end
|
17
|
+
|
18
|
+
def help
|
19
|
+
[help_lhs, help_rhs]
|
20
|
+
end
|
21
|
+
|
22
|
+
def ivar_name
|
23
|
+
"@#{attribute_name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_method
|
27
|
+
attribute_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_method
|
31
|
+
"default_#{read_method}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_method
|
35
|
+
"#{attribute_name}="
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
module AttributeDeclaration
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def define_accessors_for(attribute, &block)
|
8
|
+
define_reader_for(attribute)
|
9
|
+
define_default_for(attribute)
|
10
|
+
define_writer_for(attribute, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def define_reader_for(attribute)
|
14
|
+
define_method(attribute.read_method) do
|
15
|
+
if instance_variable_defined?(attribute.ivar_name)
|
16
|
+
instance_variable_get(attribute.ivar_name)
|
17
|
+
else
|
18
|
+
send(attribute.default_method)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def define_default_for(attribute)
|
24
|
+
define_method(attribute.default_method) do
|
25
|
+
attribute.default_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def define_writer_for(attribute, &block)
|
30
|
+
define_method(attribute.write_method) do |value|
|
31
|
+
if block
|
32
|
+
value = instance_exec(value, &block)
|
33
|
+
end
|
34
|
+
instance_variable_set(attribute.ivar_name, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|