couch-shell 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/couch-shell-plugin/core.rb +241 -0
- data/lib/couch-shell-plugin/core_designs.rb +24 -0
- data/lib/couch-shell-plugin/core_edit.rb +38 -0
- data/lib/couch-shell-plugin/core_lucene.rb +54 -0
- data/lib/couch-shell-plugin/core_views.rb +165 -0
- data/lib/couch-shell.rb +5 -0
- data/lib/couch-shell/eval_context.rb +26 -3
- data/lib/couch-shell/exceptions.rb +65 -0
- data/lib/couch-shell/json_value.rb +91 -19
- data/lib/couch-shell/plugin.rb +251 -0
- data/lib/couch-shell/plugin_utils.rb +88 -0
- data/lib/couch-shell/response.rb +19 -14
- data/lib/couch-shell/shell.rb +204 -415
- data/lib/couch-shell/version.rb +1 -1
- metadata +11 -3
@@ -0,0 +1,241 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "couch-shell/plugin"
|
4
|
+
|
5
|
+
module CouchShell
|
6
|
+
|
7
|
+
class CorePlugin < Plugin
|
8
|
+
|
9
|
+
var "A fresh uuid from the CouchDB server."
|
10
|
+
def lookup_uuid
|
11
|
+
shell.execute "uuids"
|
12
|
+
if shell.responses.current(&:ok?)
|
13
|
+
json = shell.responses.current.json_value
|
14
|
+
if json && (uuids = json["uuids"]) && uuids.array? && uuids.length > 0
|
15
|
+
uuids[0]
|
16
|
+
else
|
17
|
+
raise ShellError, "unkown json structure"
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise ShellError, "uuids request failed"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
var "Value of the id or _id member of the last response."
|
25
|
+
def lookup_id
|
26
|
+
shell.responses.current { |r| r.attr "id", "_id" } or raise VarNotSet
|
27
|
+
end
|
28
|
+
|
29
|
+
var "Value of the rev or _rev member of the last response."
|
30
|
+
def lookup_rev
|
31
|
+
@responses.current { |r| r.attr "rev", "_rev" } or raise VarNotSet
|
32
|
+
end
|
33
|
+
|
34
|
+
var "Shortcut for $(id)?rev=$(rev)."
|
35
|
+
def lookup_idr
|
36
|
+
shell.interpolate "$(id)?rev=$(rev)"
|
37
|
+
end
|
38
|
+
|
39
|
+
var "Content-Type of the last response."
|
40
|
+
def lookup_content_type
|
41
|
+
shell.responses.current(&:content_type)
|
42
|
+
end
|
43
|
+
|
44
|
+
var "Current server url."
|
45
|
+
def lookup_server
|
46
|
+
raise VarNotSet unless shell.server_url
|
47
|
+
u = shell.server_url
|
48
|
+
"#{u.scheme}://#{u.host}:#{u.port}#{u.path}"
|
49
|
+
end
|
50
|
+
|
51
|
+
var "Get response with index X."
|
52
|
+
def lookup_prefix_r(name)
|
53
|
+
i = name.to_i
|
54
|
+
raise VarNotSet unless shell.responses.readable_index?(i)
|
55
|
+
shell.responses[i]
|
56
|
+
end
|
57
|
+
|
58
|
+
var "Get json of response with index X."
|
59
|
+
def lookup_prefix_j(name)
|
60
|
+
i = name.to_i
|
61
|
+
if shell.responses.readable_index?(i)
|
62
|
+
if shell.responses[i].json_value
|
63
|
+
shell.responses[i].json_value
|
64
|
+
else
|
65
|
+
raise ShellError, "no json in response #{i}"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
raise ShellError, "no response index #{i}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def request_command_with_body(method, argstr)
|
73
|
+
if argstr =~ CouchShell::JSON_DOC_START_RX
|
74
|
+
url, bodyarg = nil, argstr
|
75
|
+
else
|
76
|
+
url, bodyarg= argstr.split(/\s+/, 2)
|
77
|
+
end
|
78
|
+
if bodyarg && bodyarg.start_with?("@")
|
79
|
+
filename, content_type = bodyarg[1..-1].split(/\s+/, 2)
|
80
|
+
body = CouchShell::FileToUpload.new(filename, content_type)
|
81
|
+
else
|
82
|
+
body = bodyarg
|
83
|
+
end
|
84
|
+
real_url = shell.interpolate(url)
|
85
|
+
shell.request method, real_url, body
|
86
|
+
real_url
|
87
|
+
end
|
88
|
+
|
89
|
+
cmd "Perform a GET http request.", synopsis: "[URL]"
|
90
|
+
def execute_get(argstr)
|
91
|
+
shell.request "GET", shell.interpolate(argstr)
|
92
|
+
end
|
93
|
+
|
94
|
+
cmd "Perform a PUT http request.",
|
95
|
+
synopsis: "[URL] [JSON|@FILENAME]"
|
96
|
+
def execute_put(argstr)
|
97
|
+
request_command_with_body("PUT", argstr)
|
98
|
+
end
|
99
|
+
|
100
|
+
cmd "put, followed by cd if put was successful"
|
101
|
+
def execute_cput(argstr)
|
102
|
+
url = request_command_with_body("PUT", argstr)
|
103
|
+
cd url if shell.responses.current(&:ok?)
|
104
|
+
end
|
105
|
+
|
106
|
+
cmd "Perform a POST http request.",
|
107
|
+
synopsis: "[URL] [JSON|@FILENAME]"
|
108
|
+
def execute_post(argstr)
|
109
|
+
request_command_with_body("POST", argstr)
|
110
|
+
end
|
111
|
+
|
112
|
+
cmd "Perform a DELETE http request.", synopsis: "[URL]"
|
113
|
+
def execute_delete(argstr)
|
114
|
+
shell.request "DELETE", shell.interpolate(argstr)
|
115
|
+
end
|
116
|
+
|
117
|
+
cmd "Change current path which will be used to interpret relative urls.",
|
118
|
+
synopsis: "[PATH]"
|
119
|
+
def execute_cd(argstr)
|
120
|
+
shell.cd shell.interpolate(argstr), false
|
121
|
+
end
|
122
|
+
|
123
|
+
cmd "cd followed by get",
|
124
|
+
synopsis: "[PATH]"
|
125
|
+
def execute_cg(argstr)
|
126
|
+
shell.cd shell.interpolate(argstr), true
|
127
|
+
end
|
128
|
+
|
129
|
+
cmd "quit shell"
|
130
|
+
def execute_exit(argstr)
|
131
|
+
raise Quit
|
132
|
+
end
|
133
|
+
|
134
|
+
cmd "quit shell"
|
135
|
+
def execute_quit(argstr)
|
136
|
+
raise Quit
|
137
|
+
end
|
138
|
+
|
139
|
+
cmd "Request uuid(s) from CouchDB server.",
|
140
|
+
synopsis: "[COUNT]"
|
141
|
+
def execute_uuids(argstr)
|
142
|
+
count = argstr ? argstr.to_i : 1
|
143
|
+
shell.request "GET", "/_uuids?count=#{count}"
|
144
|
+
end
|
145
|
+
|
146
|
+
cmd "Echos ARG after interpolating $(...) expressions.",
|
147
|
+
synopsis: "[ARG]"
|
148
|
+
def execute_echo(argstr)
|
149
|
+
if argstr
|
150
|
+
shell.stdout.puts shell.interpolate(argstr)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
cmd "Evaluate EXPR and print the result in a compact form.",
|
155
|
+
synopsis: "EXPR"
|
156
|
+
def execute_print(argstr)
|
157
|
+
raise ShellError, "expression required" unless argstr
|
158
|
+
shell.stdout.puts shell.eval_expr(argstr)
|
159
|
+
end
|
160
|
+
|
161
|
+
cmd "Evaluate EXPR and print the result in a pretty form.",
|
162
|
+
synopsis: "EXPR"
|
163
|
+
def execute_format(argstr)
|
164
|
+
raise ShellError, "expression required" unless argstr
|
165
|
+
val = shell.eval_expr(argstr)
|
166
|
+
if val.respond_to?(:couch_shell_format_string!)
|
167
|
+
shell.stdout.puts val.couch_shell_format_string!
|
168
|
+
else
|
169
|
+
shell.stdout.puts val
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
cmd "Set URL of CouchDB server.",
|
174
|
+
synopsis: "[URL]"
|
175
|
+
def execute_server(argstr)
|
176
|
+
shell.server = argstr
|
177
|
+
end
|
178
|
+
|
179
|
+
cmd "Show full url for PATH after interpolation.",
|
180
|
+
synopsis: "[PATH]"
|
181
|
+
def execute_expand(argstr)
|
182
|
+
shell.stdout.puts shell.expand(shell.interpolate(argstr))
|
183
|
+
end
|
184
|
+
|
185
|
+
cmd "Execute COMMAND in your operating system's shell.",
|
186
|
+
synopsis: "COMMAND"
|
187
|
+
def execute_sh(argstr)
|
188
|
+
raise ShellError, "argument required" unless argstr
|
189
|
+
unless system(argstr)
|
190
|
+
shell.errmsg "command exited with status #{$?.exitstatus}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
cmd "Set member KEY of document at current path to VALUE.",
|
195
|
+
synopsis: "KEY VALUE"
|
196
|
+
def execute_member(argstr)
|
197
|
+
id, rev = nil, nil
|
198
|
+
json = shell.responses.current(&:json_value)
|
199
|
+
unless json && (id = json.attr_or_nil!("_id")) &&
|
200
|
+
(rev = json.attr_or_nil!("_rev")) &&
|
201
|
+
(shell.pathstack.size > 0) &&
|
202
|
+
(shell.pathstack.last == id.to_s)
|
203
|
+
raise ShellError,
|
204
|
+
"`cg' the desired document first, e.g.: `cg /my_db/my_doc_id'"
|
205
|
+
end
|
206
|
+
# TODO: read json string as attribute name if argstr starts with double
|
207
|
+
# quote
|
208
|
+
attr_name, new_valstr = argstr.split(/\s+/, 2)
|
209
|
+
unless attr_name && new_valstr
|
210
|
+
raise ShellError,
|
211
|
+
"attribute name and new value argument required"
|
212
|
+
end
|
213
|
+
if new_valstr == "remove"
|
214
|
+
json.delete_attr!(attr_name)
|
215
|
+
else
|
216
|
+
new_val = JsonValue.parse(new_valstr)
|
217
|
+
json.set_attr!(attr_name, new_val)
|
218
|
+
end
|
219
|
+
shell.request "PUT", "?rev=#{rev}", json.to_s
|
220
|
+
end
|
221
|
+
|
222
|
+
cmd "Set the USERNAME and password for authentication in requests.",
|
223
|
+
synopsis: "USERNAME",
|
224
|
+
doc_text: "Prompts for password."
|
225
|
+
def execute_user(argstr)
|
226
|
+
shell.prompt_msg("Password:", false)
|
227
|
+
shell.password = shell.read_secret
|
228
|
+
# we save the username only after the password was entered
|
229
|
+
# to allow cancellation during password input
|
230
|
+
shell.username = argstr
|
231
|
+
end
|
232
|
+
|
233
|
+
cmd "Use PLUGIN.",
|
234
|
+
synopsis: "PLUGIN"
|
235
|
+
def execute_plugin(argstr)
|
236
|
+
shell.plugin argstr
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "couch-shell/plugin"
|
4
|
+
|
5
|
+
module CouchShell
|
6
|
+
|
7
|
+
class CoreDesignsPlugin < Plugin
|
8
|
+
|
9
|
+
def all_designs_url
|
10
|
+
'/' + dbname! + '/_all_docs?startkey="_design/"&endkey="_design0"'
|
11
|
+
end
|
12
|
+
|
13
|
+
cmd "Get a list of design names in current database."
|
14
|
+
def execute_designs(argstr)
|
15
|
+
raise ShellError, "argument not allowed" if argstr
|
16
|
+
res = request!("GET", all_designs_url, nil, false)
|
17
|
+
res.json["rows"].each { |row|
|
18
|
+
shell.stdout.puts row["key"].sub(%r{\A_design/}, '')
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "couch-shell/plugin"
|
4
|
+
|
5
|
+
module CouchShell
|
6
|
+
|
7
|
+
class CoreEditPlugin < Plugin
|
8
|
+
|
9
|
+
def plugin_initialization
|
10
|
+
@edittext = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
var "Text saved by the last invocation of edit."
|
14
|
+
def lookup_edittext
|
15
|
+
@edittext or raise VarNotSet
|
16
|
+
end
|
17
|
+
|
18
|
+
cmd "Edit a document in your editor.",
|
19
|
+
synopsis: "[URL]"
|
20
|
+
def execute_edit(argstr)
|
21
|
+
url = shell.interpolate(argstr)
|
22
|
+
res = request! "GET", url, nil, false
|
23
|
+
doc = res.json_value.to_s(true)
|
24
|
+
new_doc = edittext!(doc)
|
25
|
+
if new_doc == doc
|
26
|
+
shell.msg "Document hasn't changed. Nothing to submit."
|
27
|
+
return
|
28
|
+
end
|
29
|
+
continue? "Press ENTER to PUT updated document on server " +
|
30
|
+
"or CTRL+C to cancel "
|
31
|
+
unless shell.request("PUT", url, new_doc).ok?
|
32
|
+
shell.msg "recover document text with `print edittext'"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "couch-shell/plugin"
|
4
|
+
|
5
|
+
module CouchShell
|
6
|
+
|
7
|
+
class CoreLucenePlugin < Plugin
|
8
|
+
|
9
|
+
def plugin_initialization
|
10
|
+
@ftitext = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
var "Text saved by the last invocation of editfti."
|
14
|
+
def lookup_ftitext
|
15
|
+
@ftitext or raise VarNotSet
|
16
|
+
end
|
17
|
+
|
18
|
+
cmd "Edit fulltext index function in your editor.",
|
19
|
+
synopsis: "DESIGN FULLTEXTINDEX"
|
20
|
+
def execute_editfti(argstr)
|
21
|
+
dbname = dbname!
|
22
|
+
design_name, index_name = argstr.split(/\s+/, 2) if argstr
|
23
|
+
if design_name.nil? || index_name.nil?
|
24
|
+
raise ShellError, "design and fulltext index name required"
|
25
|
+
end
|
26
|
+
res = request! "GET", "/#{dbname}/_design/#{design_name}", nil, false
|
27
|
+
design = res.json_value
|
28
|
+
index = design.fulltext[index_name] if design["fulltext"]
|
29
|
+
indexfun = index && index["index"]
|
30
|
+
new_indexfun = edittext!(indexfun ||
|
31
|
+
"function(doc) {\n var ret = Document.new();\n\n return ret;\n}\n")
|
32
|
+
@ftitext = new_indexfun
|
33
|
+
if new_indexfun == indexfun
|
34
|
+
shell.msg "Index function hasn't changed. Nothing to submit."
|
35
|
+
return
|
36
|
+
end
|
37
|
+
continue? "Press ENTER to submit #{indexfun ? 'updated' : 'new'} " +
|
38
|
+
"index function, CTRL+C to cancel "
|
39
|
+
if design["fulltext"].nil?
|
40
|
+
design.set_attr!("fulltext", {})
|
41
|
+
end
|
42
|
+
if index.nil?
|
43
|
+
design.fulltext.set_attr!(index_name, {})
|
44
|
+
index = design.fulltext[index_name]
|
45
|
+
end
|
46
|
+
index.set_attr!("index", new_indexfun)
|
47
|
+
unless shell.request("PUT", "/#{dbname}/_design/#{design_name}", design.to_s).ok?
|
48
|
+
shell.msg "recover index text with `print ftitext'"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "couch-shell/plugin"
|
4
|
+
|
5
|
+
module CouchShell
|
6
|
+
|
7
|
+
class CoreViewsPlugin < Plugin
|
8
|
+
|
9
|
+
def plugin_initialization
|
10
|
+
@viewtext = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
var "Text saved by the last invocation of editview."
|
14
|
+
def lookup_viewtext
|
15
|
+
@viewtext or raise VarNotSet
|
16
|
+
end
|
17
|
+
|
18
|
+
cmd "Edit map, and optionally reduce function in your editor.",
|
19
|
+
synopsis: "DESIGN VIEW"
|
20
|
+
def execute_editview(argstr)
|
21
|
+
ensure_at_database
|
22
|
+
design_name, view_name = argstr.split(/\s+/, 2)
|
23
|
+
if design_name.nil? || view_name.nil?
|
24
|
+
raise ShellError, "design and view name required"
|
25
|
+
end
|
26
|
+
shell.request "GET", "_design/#{design_name}", nil, false
|
27
|
+
return unless shell.responses.current(&:ok?)
|
28
|
+
design = shell.responses.current.json_value
|
29
|
+
view = design.views[view_name] if design["views"]
|
30
|
+
mapval = view && view["map"]
|
31
|
+
reduceval = view && view["reduce"]
|
32
|
+
t = Tempfile.new(["view", ".js"])
|
33
|
+
t.puts("map")
|
34
|
+
if mapval
|
35
|
+
t.puts mapval
|
36
|
+
else
|
37
|
+
t.puts "function(doc) {\n emit(doc._id, doc);\n}"
|
38
|
+
end
|
39
|
+
if reduceval || view.nil?
|
40
|
+
t.puts
|
41
|
+
t.puts("reduce")
|
42
|
+
if reduceval
|
43
|
+
t.puts reduceval
|
44
|
+
else
|
45
|
+
t.puts "function(keys, values, rereduce) {\n\n}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
t.close
|
49
|
+
continue?("Press ENTER to edit #{view ? 'existing' : 'new'} view, " +
|
50
|
+
"CTRL+C to cancel ")
|
51
|
+
unless system(shell.editor_bin!, t.path)
|
52
|
+
raise ShellError, "editing command failed with exit status #{$?.exitstatus}"
|
53
|
+
end
|
54
|
+
text = t.open.read
|
55
|
+
@viewtext = text
|
56
|
+
t.close
|
57
|
+
mapf = nil
|
58
|
+
reducef = nil
|
59
|
+
inmap = false
|
60
|
+
inreduce = false
|
61
|
+
i = 0
|
62
|
+
text.each_line { |line|
|
63
|
+
i += 1
|
64
|
+
case line
|
65
|
+
when /^map\s*(.*)$/
|
66
|
+
unless $1.empty?
|
67
|
+
shell.msg "recover view text with `print viewtext'"
|
68
|
+
raise ShellError, "invalid map line at line #{i}"
|
69
|
+
end
|
70
|
+
unless mapf.nil?
|
71
|
+
shell.msg "recover view text with `print viewtext'"
|
72
|
+
raise ShellError, "duplicate map line at line #{i}"
|
73
|
+
end
|
74
|
+
inreduce = false
|
75
|
+
inmap = true
|
76
|
+
mapf = ""
|
77
|
+
when /^reduce\s*(.*)$/
|
78
|
+
unless $1.empty?
|
79
|
+
shell.msg "recover view text with `print viewtext'"
|
80
|
+
raise ShellError, "invalid reduce line at line #{i}"
|
81
|
+
end
|
82
|
+
unless reducef.nil?
|
83
|
+
shell.msg "recover view text with `print viewtext'"
|
84
|
+
raise ShellError, "duplicate reduce line at line #{i}"
|
85
|
+
end
|
86
|
+
inmap = false
|
87
|
+
inreduce = true
|
88
|
+
reducef = ""
|
89
|
+
else
|
90
|
+
if inmap
|
91
|
+
mapf << line
|
92
|
+
elsif inreduce
|
93
|
+
reducef << line
|
94
|
+
elsif line =~ /^\s*$/
|
95
|
+
# ignore
|
96
|
+
else
|
97
|
+
shell.msg "recover view text with `print viewtext'"
|
98
|
+
raise ShellError, "unexpected content at line #{i}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
}
|
102
|
+
mapf.strip! if mapf
|
103
|
+
reducef.strip! if reducef
|
104
|
+
mapf = nil if mapf && mapf.empty?
|
105
|
+
reducef = nil if reducef && reducef.empty?
|
106
|
+
shell.prompt_msg "View parsed, following actions would be taken:"
|
107
|
+
if mapf && mapval.nil?
|
108
|
+
shell.prompt_msg " Add map function."
|
109
|
+
elsif mapf.nil? && mapval
|
110
|
+
shell.prompt_msg " Remove map function."
|
111
|
+
elsif mapf && mapval && mapf != mapval
|
112
|
+
shell.prompt_msg " Update map function."
|
113
|
+
end
|
114
|
+
if reducef && reduceval.nil?
|
115
|
+
shell.prompt_msg " Add reduce function."
|
116
|
+
elsif reducef.nil? && reduceval
|
117
|
+
shell.prompt_msg " Remove reduce function."
|
118
|
+
elsif reducef && reduceval && reducef != reduceval
|
119
|
+
shell.prompt_msg " Update reduce function."
|
120
|
+
end
|
121
|
+
continue? "Press ENTER to submit, CTRL+C to cancel "
|
122
|
+
if !design.respond_to?(:views)
|
123
|
+
design.set_attr!("views", {})
|
124
|
+
end
|
125
|
+
if view.nil?
|
126
|
+
design.views.set_attr!(view_name, {})
|
127
|
+
view = design.views[view_name]
|
128
|
+
end
|
129
|
+
if mapf.nil?
|
130
|
+
view.delete_attr!("map")
|
131
|
+
else
|
132
|
+
view.set_attr!("map", mapf)
|
133
|
+
end
|
134
|
+
if reducef.nil?
|
135
|
+
view.delete_attr!("reduce")
|
136
|
+
else
|
137
|
+
view.set_attr!("reduce", reducef)
|
138
|
+
end
|
139
|
+
shell.request "PUT", "_design/#{design_name}", design.to_s
|
140
|
+
unless shell.responses.current(&:ok?)
|
141
|
+
shell.msg "recover view text with `print viewtext'"
|
142
|
+
end
|
143
|
+
ensure
|
144
|
+
if t
|
145
|
+
t.close
|
146
|
+
t.unlink
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
cmd "Shortcut to GET a view.",
|
151
|
+
synopsis: "DESIGN/VIEW[?params]"
|
152
|
+
def execute_view(argstr)
|
153
|
+
if shell.pathstack.size != 1
|
154
|
+
raise ShellError, "current directory must be database"
|
155
|
+
end
|
156
|
+
design_name, view_name = argstr.split("/", 2)
|
157
|
+
if design_name.nil? || view_name.nil?
|
158
|
+
raise ShellError, "argument in the form DESIGN/VIEW required"
|
159
|
+
end
|
160
|
+
shell.request "GET", "_design/#{design_name}/_view/#{view_name}"
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|