runssh 0.1.1 → 0.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.
- data/.autotest +12 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +42 -0
- data/README.rdoc +28 -9
- data/Rakefile +5 -33
- data/autotest/discover.rb +1 -0
- data/bin/runssh +2 -0
- data/lib/runsshlib/cli.rb +79 -32
- data/lib/runsshlib/config_file.rb +61 -13
- data/lib/runsshlib/ssh_backend.rb +15 -10
- data/lib/runsshlib/ssh_host_def.rb +49 -0
- data/lib/runsshlib/version.rb +27 -0
- data/lib/runsshlib.rb +7 -8
- data/runssh.gemspec +37 -0
- data/spec/fixtures/runssh.yml +24 -19
- data/spec/fixtures/runssh_v_none.yml +19 -0
- data/spec/runsshlib/cli_spec.rb +126 -62
- data/spec/runsshlib/config_file_spec.rb +151 -38
- data/spec/runsshlib/ssh_backend_spec.rb +37 -35
- data/spec/runsshlib/ssh_host_def_spec.rb +91 -0
- data/spec/spec_helper.rb +5 -0
- metadata +103 -20
@@ -0,0 +1,27 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2010 Haim Ashkenazi
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU General Public License
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
7
|
+
# of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
#
|
18
|
+
|
19
|
+
module RunSSHLib
|
20
|
+
module Version
|
21
|
+
MAJOR = 0
|
22
|
+
MINOR = 2
|
23
|
+
BUILD = 0
|
24
|
+
|
25
|
+
STRING = [MAJOR, MINOR, BUILD].compact.join('.')
|
26
|
+
end
|
27
|
+
end
|
data/lib/runsshlib.rb
CHANGED
@@ -19,6 +19,9 @@
|
|
19
19
|
require 'runsshlib/cli'
|
20
20
|
require 'runsshlib/config_file'
|
21
21
|
require 'runsshlib/ssh_backend'
|
22
|
+
require 'runsshlib/ssh_host_def'
|
23
|
+
require 'runsshlib/version'
|
24
|
+
require 'highline'
|
22
25
|
|
23
26
|
# Main RunSSHLib module.
|
24
27
|
module RunSSHLib
|
@@ -31,14 +34,10 @@ module RunSSHLib
|
|
31
34
|
# Indicates invalid command
|
32
35
|
class InvalidSubCommandError < StandardError; end
|
33
36
|
|
37
|
+
# Indicates older config version.
|
38
|
+
# message should contain only the older config version!
|
39
|
+
class OlderConfigVersionError < StandardError; end
|
40
|
+
|
34
41
|
# A placeholder for host definitions
|
35
42
|
HostDef = Struct.new(:name, :login)
|
36
|
-
|
37
|
-
module Version
|
38
|
-
MAJOR = 0
|
39
|
-
MINOR = 1
|
40
|
-
BUILD = 1
|
41
|
-
|
42
|
-
STRING = [MAJOR, MINOR, BUILD].compact.join('.')
|
43
|
-
end
|
44
43
|
end
|
data/runssh.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "runsshlib/version"
|
4
|
+
|
5
|
+
spec = Gem::Specification.new do |s|
|
6
|
+
s.name = 'runssh'
|
7
|
+
s.version = RunSSHLib::Version::STRING
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.author = 'Haim Ashkenazi'
|
10
|
+
s.email = 'haim@babysnakes.org'
|
11
|
+
s.homepage = 'http://github.com/babysnakes/runssh'
|
12
|
+
|
13
|
+
s.summary = "CLI utility to bookmark multiple ssh connections with hierarchy."
|
14
|
+
s.description = <<EOF
|
15
|
+
Runssh is a command line utility to help bookmark many
|
16
|
+
ssh connections in heirarchial groups.
|
17
|
+
EOF
|
18
|
+
|
19
|
+
s.required_ruby_version = '>= 1.8.7'
|
20
|
+
s.add_dependency('trollop', '~> 1.16.2')
|
21
|
+
s.add_dependency('highline', '~> 1.6.1')
|
22
|
+
s.add_development_dependency('rspec', "~> 2.1.0")
|
23
|
+
s.add_development_dependency('rcov', '~> 0.9.9')
|
24
|
+
s.add_development_dependency('autotest', '~> 4.4.4')
|
25
|
+
s.add_development_dependency('autotest-fsevent', '~> 0.2.3')
|
26
|
+
s.add_development_dependency('autotest-growl', '~> 0.2.6')
|
27
|
+
|
28
|
+
s.has_rdoc = true
|
29
|
+
s.extra_rdoc_files = ['README.rdoc']
|
30
|
+
s.rdoc_options << '--main' << 'README.rdoc'
|
31
|
+
s.rdoc_options << '-c' << 'UTF-8'
|
32
|
+
|
33
|
+
s.files = `git ls-files`.split("\n")
|
34
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
35
|
+
s.executables << 'runssh'
|
36
|
+
s.require_paths = ['lib']
|
37
|
+
end
|
data/spec/fixtures/runssh.yml
CHANGED
@@ -1,19 +1,24 @@
|
|
1
|
-
---
|
2
|
-
:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
:
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
1
|
+
---
|
2
|
+
VERSION: 1.0
|
3
|
+
:cust1:
|
4
|
+
:dc1:
|
5
|
+
:host1: !ruby/object:RunSSHLib::SshHostDef
|
6
|
+
definition:
|
7
|
+
:login: user1
|
8
|
+
:host_name: a.host.com
|
9
|
+
:host2: !ruby/object:RunSSHLib::SshHostDef
|
10
|
+
definition:
|
11
|
+
:login: user1
|
12
|
+
:host_name: b.host.com
|
13
|
+
:dc2:
|
14
|
+
:host1: !ruby/object:RunSSHLib::SshHostDef
|
15
|
+
definition:
|
16
|
+
:login: user3
|
17
|
+
:host_name: c.host.com
|
18
|
+
:cust2:
|
19
|
+
:dc:
|
20
|
+
:internal:
|
21
|
+
:somehost: !ruby/object:RunSSHLib::SshHostDef
|
22
|
+
definition:
|
23
|
+
:login: otheruser
|
24
|
+
:host_name: a.example.com
|
@@ -0,0 +1,19 @@
|
|
1
|
+
---
|
2
|
+
:cust1:
|
3
|
+
:dc1:
|
4
|
+
:host2: !ruby/struct:RunSSHLib::HostDef
|
5
|
+
name: b.host.com
|
6
|
+
login: user1
|
7
|
+
:host1: !ruby/struct:RunSSHLib::HostDef
|
8
|
+
name: a.host.com
|
9
|
+
login: user1
|
10
|
+
:dc2:
|
11
|
+
:host1: !ruby/struct:RunSSHLib::HostDef
|
12
|
+
name: c.host.com
|
13
|
+
login: user3
|
14
|
+
:cust2:
|
15
|
+
:dc:
|
16
|
+
:internal:
|
17
|
+
:somehost: !ruby/struct:RunSSHLib::HostDef
|
18
|
+
name: a.example.com
|
19
|
+
login: otheruser
|
data/spec/runsshlib/cli_spec.rb
CHANGED
@@ -15,9 +15,10 @@
|
|
15
15
|
# along with this program; if not, write to the Free Software
|
16
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
17
|
#
|
18
|
-
require 'lib/runsshlib'
|
19
18
|
require 'spec_helper'
|
19
|
+
require 'runsshlib'
|
20
20
|
require 'stringio'
|
21
|
+
require 'yaml'
|
21
22
|
|
22
23
|
describe "The CLI interface" do
|
23
24
|
# a shortcut to verify the help for print command
|
@@ -74,6 +75,36 @@ describe "The CLI interface" do
|
|
74
75
|
end.to exit_abnormaly
|
75
76
|
@buffer.should match(/invalid command/)
|
76
77
|
end
|
78
|
+
|
79
|
+
it "should display right message upon older configuration error" do
|
80
|
+
dump_config Hash.new
|
81
|
+
expect do
|
82
|
+
cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} print ?))
|
83
|
+
end.to exit_abnormaly
|
84
|
+
@buffer.should match(/--update-config/)
|
85
|
+
@buffer.should match(/.none/)
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "with --update_config" do
|
89
|
+
let(:config_v_none) do
|
90
|
+
YAML.load_file(File.join(File.dirname(__FILE__), '..',
|
91
|
+
'fixtures', 'runssh_v_none.yml'))
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should accept --update-config as argument" do
|
95
|
+
cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should not fail upon initialization if config is of older version" do
|
99
|
+
dump_config config_v_none
|
100
|
+
cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should not initialize @config object after initialization" do
|
104
|
+
cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
105
|
+
cli.instance_variable_get(:@c).should be_nil
|
106
|
+
end
|
107
|
+
end
|
77
108
|
end
|
78
109
|
|
79
110
|
describe "main help" do
|
@@ -97,8 +128,43 @@ describe "The CLI interface" do
|
|
97
128
|
cli.run
|
98
129
|
end
|
99
130
|
|
131
|
+
it "should run run_update_config when called with --update-config" do
|
132
|
+
@cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
133
|
+
@cli.should_receive(:run_update_config)
|
134
|
+
@cli.run
|
135
|
+
end
|
136
|
+
|
137
|
+
context "update_config" do
|
138
|
+
let(:cf) { double('ConfigFile') }
|
139
|
+
|
140
|
+
it "should initialize ConfigFile with old_version=true and run update_config" do
|
141
|
+
cf.should_receive(:update_config)
|
142
|
+
RunSSHLib::ConfigFile.should_receive(:new).with(TMP_FILE, true).
|
143
|
+
and_return(cf)
|
144
|
+
@cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
145
|
+
@cli.run
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should inform the user of success and backup file" do
|
149
|
+
backup_path = "/path/to/backup_file"
|
150
|
+
cf.should_receive(:update_config).and_return(backup_path)
|
151
|
+
RunSSHLib::ConfigFile.should_receive(:new).and_return(cf)
|
152
|
+
@cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
153
|
+
@cli.run
|
154
|
+
@buffer.should include(backup_path)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should inform the user if no backup was required" do
|
158
|
+
cf.should_receive(:update_config).and_return(nil)
|
159
|
+
RunSSHLib::ConfigFile.should_receive(:new).and_return(cf)
|
160
|
+
@cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} --update-config))
|
161
|
+
@cli.run
|
162
|
+
@buffer.should include("No update was performed")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
100
166
|
describe "with subcommand" do
|
101
|
-
|
167
|
+
context "shell" do
|
102
168
|
before(:each) do
|
103
169
|
@shell_cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} shell))
|
104
170
|
end
|
@@ -121,15 +187,39 @@ describe "The CLI interface" do
|
|
121
187
|
options = @shell_cli.instance_variable_get :@options
|
122
188
|
options.should have_key(:login)
|
123
189
|
end
|
190
|
+
|
191
|
+
it "should not overwrite nil arguments with saved ones when merging" do
|
192
|
+
import_fixtures
|
193
|
+
RunSSHLib::SshBackend.should_receive(:shell).
|
194
|
+
with(hash_including(
|
195
|
+
:host_name => "a.example.com",
|
196
|
+
:login => "otheruser")).
|
197
|
+
and_return(nil)
|
198
|
+
cli = RunSSHLib::CLI.new(
|
199
|
+
%W(-f #{TMP_FILE} shell cust2 dc internal somehost))
|
200
|
+
cli.run
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should correctly call SshBackend.shell with merged definition" do
|
204
|
+
import_fixtures
|
205
|
+
RunSSHLib::SshBackend.should_receive(:shell).
|
206
|
+
with(hash_including(:host_name => "a.example.com",
|
207
|
+
:login => "someuser")).
|
208
|
+
and_return(nil)
|
209
|
+
cli = RunSSHLib::CLI.new(
|
210
|
+
%W(-f #{TMP_FILE} shell -l someuser cust2 dc internal somehost))
|
211
|
+
cli.run
|
212
|
+
end
|
124
213
|
|
125
|
-
it "should correctly
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
214
|
+
it "should correctly parse remote command (indicated by --)" do
|
215
|
+
import_fixtures
|
216
|
+
RunSSHLib::SshBackend.should_receive(:shell).
|
217
|
+
with(hash_including(:remote_cmd => "ls -l /tmp")).
|
218
|
+
and_return(nil)
|
219
|
+
cli = RunSSHLib::CLI.new(
|
220
|
+
%W(-f #{TMP_FILE} shell -l someuser cust2 dc internal somehost
|
221
|
+
-- ls -l /tmp)
|
222
|
+
)
|
133
223
|
cli.run
|
134
224
|
end
|
135
225
|
end
|
@@ -146,12 +236,14 @@ describe "The CLI interface" do
|
|
146
236
|
it "should have all required arguments" do
|
147
237
|
options = @add_cli.instance_variable_get :@options
|
148
238
|
options.should have_key(:host_name)
|
149
|
-
options.should have_key(:
|
239
|
+
options.should have_key(:login)
|
150
240
|
end
|
151
241
|
|
152
242
|
it "should invoke the add_host_def" do
|
153
243
|
@add_cli.instance_variable_get(:@c).should_receive(:add_host_def).
|
154
|
-
with([:one], :two, RunSSHLib::
|
244
|
+
with([:one], :two, RunSSHLib::SshHostDef.new({
|
245
|
+
:host_name => 'host',:login => nil
|
246
|
+
}))
|
155
247
|
@add_cli.run
|
156
248
|
end
|
157
249
|
end
|
@@ -160,6 +252,9 @@ describe "The CLI interface" do
|
|
160
252
|
before(:each) do
|
161
253
|
@d_cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} del))
|
162
254
|
@d_cli.instance_variable_get(:@c).stub(:delete_path)
|
255
|
+
@hl = double('HighLine')
|
256
|
+
HighLine.stub(:new) {@hl}
|
257
|
+
@hl.stub(:agree)
|
163
258
|
end
|
164
259
|
|
165
260
|
it "should parse 'd' as del" do
|
@@ -167,18 +262,18 @@ describe "The CLI interface" do
|
|
167
262
|
end
|
168
263
|
|
169
264
|
it "should verify the deletion" do
|
170
|
-
@
|
265
|
+
@hl.should_receive(:agree).with(/Are you sure/)
|
171
266
|
@d_cli.run
|
172
267
|
end
|
173
268
|
|
174
269
|
it "should perform the deletion upon confirmation" do
|
175
|
-
@
|
270
|
+
@hl.should_receive(:agree).and_return(true)
|
176
271
|
@d_cli.instance_variable_get(:@c).should_receive(:delete_path)
|
177
272
|
@d_cli.run
|
178
273
|
end
|
179
274
|
|
180
275
|
it "should cancel the deletion if not confirmed" do
|
181
|
-
@
|
276
|
+
@hl.should_receive(:agree).and_return(false)
|
182
277
|
@d_cli.instance_variable_get(:@c).should_not_receive(:delete_path)
|
183
278
|
@d_cli.run
|
184
279
|
@buffer.should match(/cancel/)
|
@@ -186,7 +281,7 @@ describe "The CLI interface" do
|
|
186
281
|
|
187
282
|
it "should pass the right path to delete_path" do
|
188
283
|
cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} del one two three))
|
189
|
-
|
284
|
+
@hl.should_receive(:agree).and_return(true)
|
190
285
|
cli.instance_variable_get(:@c).should_receive(:delete_path).
|
191
286
|
with([:one, :two, :three])
|
192
287
|
cli.run
|
@@ -205,19 +300,22 @@ describe "The CLI interface" do
|
|
205
300
|
it "should have all required argumants" do
|
206
301
|
options = @update_cli.instance_variable_get :@options
|
207
302
|
options.should have_key(:host_name)
|
208
|
-
options.should have_key(:
|
303
|
+
options.should have_key(:login)
|
209
304
|
end
|
210
305
|
|
211
306
|
it "should invoke update_host_def" do
|
212
307
|
config = @update_cli.instance_variable_get :@c
|
213
308
|
config.should_receive(:update_host_def).
|
214
|
-
with([:root], RunSSHLib::
|
309
|
+
with([:root], RunSSHLib::SshHostDef.new({
|
310
|
+
:host_name => "newhost", :login => nil
|
311
|
+
}))
|
215
312
|
@update_cli.run
|
216
313
|
end
|
217
314
|
end
|
218
315
|
|
219
316
|
describe "print" do
|
220
317
|
before(:each) do
|
318
|
+
import_fixtures
|
221
319
|
@p_cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} print cust2 dc internal somehost))
|
222
320
|
end
|
223
321
|
|
@@ -225,19 +323,10 @@ describe "The CLI interface" do
|
|
225
323
|
@p_cli.send(:extract_subcommand, ['p']).should eql('print')
|
226
324
|
end
|
227
325
|
|
228
|
-
it "should print correctly host definition
|
326
|
+
it "should print correctly host definition" do
|
229
327
|
@p_cli.run
|
328
|
+
@buffer.should match(/Host definition for: somehost/)
|
230
329
|
@buffer.should match(/host: a.example.com/)
|
231
|
-
@buffer.should match(/user: otheruser/)
|
232
|
-
end
|
233
|
-
|
234
|
-
it "should print correctly host definition without user" do
|
235
|
-
c = RunSSHLib::ConfigFile.new("#{TMP_FILE}")
|
236
|
-
c.add_host_def([:three, :four], :five, RunSSHLib::HostDef.new('anewhost'))
|
237
|
-
cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} print three four five))
|
238
|
-
cli.run
|
239
|
-
@buffer.should match(/host: anewhost/)
|
240
|
-
@buffer.should match(/user: current user/)
|
241
330
|
end
|
242
331
|
end
|
243
332
|
|
@@ -245,6 +334,9 @@ describe "The CLI interface" do
|
|
245
334
|
before(:each) do
|
246
335
|
@i_cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} import -i inputfile))
|
247
336
|
@i_cli.instance_variable_get(:@c).stub(:import)
|
337
|
+
@hl = double('HighLine')
|
338
|
+
HighLine.stub(:new) {@hl}
|
339
|
+
@hl.stub(:agree)
|
248
340
|
end
|
249
341
|
|
250
342
|
it "should parse 'i' as import" do
|
@@ -257,25 +349,25 @@ describe "The CLI interface" do
|
|
257
349
|
end
|
258
350
|
|
259
351
|
it "should verify the import with the user" do
|
260
|
-
@
|
352
|
+
@hl.should_receive(:agree).with(/OVERWRITES/)
|
261
353
|
@i_cli.run
|
262
354
|
end
|
263
355
|
|
264
356
|
it "should run import upon confirmation" do
|
265
|
-
@
|
357
|
+
@hl.should_receive(:agree).and_return(true)
|
266
358
|
@i_cli.instance_variable_get(:@c).should_receive(:import)
|
267
359
|
@i_cli.run
|
268
360
|
end
|
269
361
|
|
270
362
|
it "should cancel if not confirmed" do
|
271
|
-
@
|
363
|
+
@hl.should_receive(:agree).and_return(false)
|
272
364
|
@i_cli.instance_variable_get(:@c).should_not_receive(:import)
|
273
365
|
@i_cli.run
|
274
366
|
@buffer.should match(/cancel/)
|
275
367
|
end
|
276
368
|
|
277
369
|
it "should pass the right argument to import" do
|
278
|
-
@
|
370
|
+
@hl.should_receive(:agree).and_return(true)
|
279
371
|
@i_cli.instance_variable_get(:@c).should_receive(:import).
|
280
372
|
with('inputfile')
|
281
373
|
@i_cli.run
|
@@ -298,37 +390,9 @@ describe "The CLI interface" do
|
|
298
390
|
end
|
299
391
|
end
|
300
392
|
end
|
301
|
-
|
302
|
-
describe "verify_yn" do
|
303
|
-
before(:each) do
|
304
|
-
@verify_cli = RunSSHLib::CLI.new(%W(-f #{TMP_FILE} print))
|
305
|
-
end
|
306
|
-
|
307
|
-
it "should parse 'y' as true" do
|
308
|
-
stdin = "y\n"
|
309
|
-
$stdin = StringIO.open(stdin, 'r')
|
310
|
-
@verify_cli.send(:verify_yn, 'question').should be_true
|
311
|
-
end
|
312
|
-
|
313
|
-
it "should parse all other as false" do
|
314
|
-
tests = ["n", "\n", "Y\n", "maybe", "\n"]
|
315
|
-
tests.each do |test_phrase|
|
316
|
-
stdin = test_phrase
|
317
|
-
$stdin = StringIO.open(stdin, 'r')
|
318
|
-
@verify_cli.send(:verify_yn, 'question').should be_false
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
it "should add postfix to the question" do
|
323
|
-
stdin = "n"
|
324
|
-
$stdin = StringIO.open(stdin, 'r')
|
325
|
-
@verify_cli.send(:verify_yn, 'are you sure')
|
326
|
-
@buffer.should eql('are you sure (y/n)? ')
|
327
|
-
end
|
328
|
-
end
|
329
393
|
end
|
330
394
|
|
331
|
-
after(:
|
395
|
+
after(:each) do
|
332
396
|
cleanup_tmp_file
|
333
397
|
end
|
334
398
|
end
|