cmdparse 1.0.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,9 @@
1
1
  #
2
2
  #--
3
3
  #
4
- # $Id: cmdparse.rb 328 2005-07-05 14:10:02Z thomas $
4
+ # $Id: cmdparse.rb 329 2005-08-14 15:39:05Z thomas $
5
5
  #
6
- # cmdparse: an advanced command line parser using optparse which supports commands
6
+ # cmdparse: advanced command line parser supporting commands
7
7
  # Copyright (C) 2004 Thomas Leitner
8
8
  #
9
9
  # This program is free software; you can redistribute it and/or modify it under the terms of the GNU
@@ -19,380 +19,408 @@
19
19
  #
20
20
  #++
21
21
  #
22
- # Look at the +CommandParser+ class for details and an example.
23
- #
24
22
 
25
- require 'optparse'
26
23
 
27
- # Some extension to the standard option parser class
28
- class OptionParser
24
+ # Namespace module for cmdparse.
25
+ module CmdParse
26
+
27
+ # The version of this cmdparse implemention
28
+ VERSION = [2, 0, 0]
29
+
30
+
31
+ # Base class for all cmdparse errors.
32
+ class ParseError < RuntimeError
33
+
34
+ # Sets the reason for a subclass.
35
+ def self.reason( reason, has_arguments = true )
36
+ (@@reason ||= {})[self] = [reason, has_arguments]
37
+ end
38
+
39
+ # Returns the reason plus the message.
40
+ def message
41
+ data = @@reason[self.class] || ['Unknown error', true]
42
+ data[0] + (data[1] ? ": " + super : '')
43
+ end
29
44
 
30
- if const_defined?( 'Officious' )
31
- Officious.delete( 'version' )
32
- Officious.delete( 'help' )
33
- else
34
- DefaultList.long.delete( 'version' )
35
- DefaultList.long.delete( 'help' )
36
45
  end
37
46
 
38
- # Returns the <tt>@banner</tt> value. Needed because the method <tt>OptionParser#banner</tt> does
39
- # not return the internal value of <tt>@banner</tt> but a modified one.
40
- def get_banner
41
- @banner
47
+ # This error is thrown when an invalid command is encountered.
48
+ class InvalidCommandError < ParseError
49
+ reason 'Invalid command'
42
50
  end
43
51
 
44
- # Returns the <tt>@program_name</tt> value. Needed because the method
45
- # <tt>OptionParser#program_name</tt> does not return the internal value of <tt>@program_name</tt>
46
- # but a modified one.
47
- def get_program_name
48
- @program_name
52
+ # This error is thrown when an invalid argument is encountered.
53
+ class InvalidArgumentError < ParseError
54
+ reason 'Invalid argument'
49
55
  end
50
56
 
51
- end
57
+ # This error is thrown when an invalid option is encountered.
58
+ class InvalidOptionError < ParseError
59
+ reason 'Invalid option'
60
+ end
52
61
 
62
+ # This error is thrown when no command was given and no default command was specified.
63
+ class NoCommandGivenError < ParseError
64
+ reason 'No command given', false
65
+ end
53
66
 
54
- # = CommandParser
55
- #
56
- # == Introduction
57
- #
58
- # +CommandParser+ is a class for analyzing the command line of a program. It uses the standard
59
- # +OptionParser+ class internally for parsing the options and additionally allows the
60
- # specification of commands. Programs which use commands as part of their command line interface
61
- # are, for example, Subversion's +svn+ program and Rubygem's +gem+ program.
62
- #
63
- # == Example
64
- #
65
- # require 'cmdparse'
66
- # require 'ostruct'
67
- #
68
- # class TestCommand < CommandParser::Command
69
- #
70
- # def initialize
71
- # super('test')
72
- # @internal = OpenStruct.new
73
- # @internal.function = nil
74
- # @internal.audible = false
75
- # options.separator "Options:"
76
- # options.on("-t", "--test FUNCTION", "Test only FUNCTION") do |func|
77
- # @internal.function = func
78
- # end
79
- # options.on("-a", "--[no-]audible", "Run audible") { |@internal.audible| }
80
- # end
81
- #
82
- # def description
83
- # "Executes various tests"
84
- # end
85
- #
86
- # def execute( commandParser, args )
87
- # puts "Test: "+ args.inspect
88
- # puts @internal.inspect
89
- # end
90
- #
91
- # end
92
- #
93
- # cmd = CommandParser.new
94
- # cmd.options do |opt|
95
- # opt.program_name = "testProgram"
96
- # opt.version = [0, 1, 0]
97
- # opt.release = "1.0"
98
- # opt.separator "Global options:"
99
- # opt.on("-r", "--require TEST", "Require the TEST")
100
- # opt.on("--delay N", Integer, "Delay test for N seconds before executing")
101
- # end
102
- # cmd.add_command TestCommand.new, true # sets this command as default command
103
- # cmd.add_command CommandParser::HelpCommand.new
104
- # cmd.add_command CommandParser::VersionCommand.new
105
- # cmd.parse!( ARGV )
106
- #
107
- class CommandParser
67
+ # This error is thrown when a command is added to another command which does not support commands.
68
+ class TakesNoCommandError < ParseError
69
+ reason 'This command takes no other commands', false
70
+ end
108
71
 
109
- # The version of the command parser
110
- VERSION = [1, 0, 5]
111
72
 
112
- # This error is thrown when an invalid command is encountered.
113
- class InvalidCommandError < OptionParser::ParseError
114
- const_set( :Reason, 'invalid command'.freeze )
115
- end
73
+ # Base class for all parser wrappers.
74
+ class ParserWrapper
75
+
76
+ # Returns the parser instance for the object and, if a block is a given, yields the instance.
77
+ def instance
78
+ yield @instance if block_given?
79
+ @instance
80
+ end
81
+
82
+ # Parses the arguments in order, i.e. stops at the first non-option argument, and returns all
83
+ # remaining arguments.
84
+ def order( args )
85
+ raise InvalidOptionError.new( args[0] ) if args[0] =~ /^-/
86
+ args
87
+ end
88
+
89
+ # Permutes the arguments so that all options anywhere on the command line are parsed and the
90
+ # remaining non-options are returned.
91
+ def permute( args )
92
+ raise InvalidOptionError.new( args[0] ) if args.any? {|a| a =~ /^-/}
93
+ args
94
+ end
95
+
96
+ # Returns a summary string of the options.
97
+ def summarize
98
+ ""
99
+ end
116
100
 
117
- # This error is thrown when no command was given and no default command was specified.
118
- class NoCommandGivenError < OptionParser::ParseError
119
- const_set( :Reason, 'no command given'.freeze )
120
101
  end
121
102
 
103
+ # Require default option parser wrapper
104
+ require 'cmdparse/wrappers/optparse'
105
+
106
+
122
107
  # Base class for the commands. This class implements all needed methods so that it can be used by
123
- # the +OptionParser+ class.
108
+ # the +CommandParser+ class.
124
109
  class Command
125
110
 
126
111
  # The name of the command
127
112
  attr_reader :name
128
113
 
129
- # The command line options, an instance of +OptionParser+.
130
- attr_reader :options
114
+ # A short description of the command.
115
+ attr_accessor :short_desc
116
+
117
+ # A detailed description of the command
118
+ attr_accessor :description
119
+
120
+ # The wrapper for parsing the command line options.
121
+ attr_accessor :options
122
+
123
+ # Returns the name of the default command.
124
+ attr_reader :default_command
125
+
126
+ # Sets or returns the super command of this command. The super command is either a +Command+
127
+ # instance for normal commands or a +CommandParser+ instance for the root command.
128
+ attr_accessor :super_command
131
129
 
132
- # Initializes the command and assignes it a +name+.
133
- def initialize( name )
130
+ # Returns the list of commands for this command.
131
+ attr_reader :commands
132
+
133
+ # Initializes the command called +name+. The parameter +has_commands+ specifies if this command
134
+ # takes other commands as argument.
135
+ def initialize( name, has_commands )
134
136
  @name = name
135
- @options = OptionParser.new
137
+ @options = ParserWrapper.new
138
+ @has_commands = has_commands
139
+ @commands = {}
140
+ @default_command = nil
141
+ end
142
+
143
+ # Returns +true+ if this command supports sub commands.
144
+ def has_commands?
145
+ @has_commands
146
+ end
147
+
148
+ # Adds a command to the command list if this command takes other commands as argument. If the
149
+ # optional parameter +default+ is true, then this command is used when no command is specified
150
+ # on the command line.
151
+ def add_command( command, default = false )
152
+ raise TakesNoCommandError.new( @name ) if !has_commands?
153
+ @commands[command.name] = command
154
+ @default_command = command.name if default
155
+ command.super_command = self
156
+ command.init
136
157
  end
137
158
 
138
- # For sorting commands by name
159
+ # For sorting commands by name.
139
160
  def <=>( other )
140
161
  @name <=> other.name
141
162
  end
142
163
 
143
- # Should be overridden by specific implementations. This method is called after the command is
144
- # added to a +CommandParser+ instance.
145
- def init( commandParser )
164
+ # Returns the +CommandParser+ instance for this command or +nil+ if this command was not
165
+ # assigned to a +CommandParser+ instance.
166
+ def commandparser
167
+ cmd = super_command
168
+ cmd = cmd.super_command while !cmd.nil? && !cmd.kind_of?( CommandParser )
169
+ cmd
146
170
  end
147
171
 
148
- # Default method for showing the help for the command.
149
- def show_help( commandParser )
150
- @options.program_name = commandParser.options.program_name if @options.get_program_name.nil?
151
- puts "#{@name}: #{description}"
152
- puts usage
153
- puts ""
154
- puts options.summarize
172
+ # Returns a list of super commands, ie.:
173
+ # [command, super_command, super_super_command, ...]
174
+ def super_commands
175
+ cmds = []
176
+ cmd = self
177
+ while !cmd.nil? && !cmd.super_command.kind_of?( CommandParser )
178
+ cmds << cmd
179
+ cmd = cmd.super_command
180
+ end
181
+ cmds
182
+ end
183
+
184
+ # This method is called when the command is added to a +Command+ instance.
185
+ def init; end
186
+
187
+ # Set the given +block+ as execution block. See also: +execute+.
188
+ def set_execution_block( &block )
189
+ @exec_block = block
155
190
  end
156
191
 
157
- # Should be overridden by specific implementations. Defines the description of the command.
158
- def description
159
- '<no description given>'
192
+ # Invokes the block set by +set_execution_block+. This method is called by the +CommandParser+
193
+ # instance if this command was specified on the command line.
194
+ def execute( args )
195
+ @exec_block.call( args )
160
196
  end
161
197
 
162
- # Defines the usage line for the command. Can be overridden if a more specific usage line is needed.
198
+ # Defines the usage line for the command.
163
199
  def usage
164
- "Usage: #{@options.program_name} [global options] #{@name} [options] args"
200
+ tmp = "Usage: #{commandparser.program_name}"
201
+ tmp << " [options] " if !commandparser.options.instance_of?( ParserWrapper )
202
+ tmp << super_commands.reverse.collect do |c|
203
+ t = c.name
204
+ t << " [options]" if !c.options.instance_of?( ParserWrapper )
205
+ t
206
+ end.join(' ')
207
+ tmp << (has_commands? ? " COMMAND [options] [ARGS]" : " [ARGS]")
208
+ end
209
+
210
+ # Default method for showing the help for the command.
211
+ def show_help
212
+ puts "#{@name}: #{short_desc}"
213
+ puts description if description
214
+ puts
215
+ puts usage
216
+ puts
217
+ if has_commands?
218
+ list_commands
219
+ puts
220
+ end
221
+ unless (summary = options.summarize).empty?
222
+ puts summary
223
+ puts
224
+ end
165
225
  end
166
226
 
167
- # Must be overridden by specific implementations. This method is called by the +CommandParser+
168
- # if this command was specified on the command line.
169
- def execute( commandParser, args )
170
- raise NotImplementedError
227
+ #######
228
+ private
229
+ #######
230
+
231
+ def list_commands( level = 1, command = self )
232
+ puts "Available commands:" if level == 1
233
+ command.commands.sort.each do |name, cmd|
234
+ print " "*level + name.ljust( 15 ) + cmd.short_desc.to_s
235
+ print " (=default command)" if name == command.default_command
236
+ print "\n"
237
+ list_commands( level + 1, cmd ) if cmd.has_commands?
238
+ end
171
239
  end
172
240
 
173
241
  end
174
242
 
175
243
 
176
- # The default help command.It adds the options "-h" and "--help" to the global +CommandParser+
177
- # options. When specified on the command line, it can show the main help or an individual command
178
- # help.
244
+ # The default help command. It adds the options "-h" and "--help" to the global options of the
245
+ # associated +CommandParser+. When the command is specified on the command line, it can show the
246
+ # main help or individual command help.
179
247
  class HelpCommand < Command
180
248
 
181
249
  def initialize
182
- super( 'help' )
250
+ super( 'help', false )
251
+ self.short_desc = 'Provide help for individual commands'
252
+ self.description = 'This command prints the program help if no arguments are given. ' \
253
+ 'If one or more command names are given as arguments, these arguments are interpreted ' \
254
+ 'as a hierachy of commands and the help for the right most command is show.'
183
255
  end
184
256
 
185
- def init( commandParser )
186
- commandParser.options do |opt|
187
- opt.on_tail( "-h", "--help [command]", "Show help" ) do |cmd|
188
- execute( commandParser, cmd.nil? ? [] : [cmd] )
257
+ def init
258
+ case commandparser.main_command.options
259
+ when OptionParserWrapper
260
+ commandparser.main_command.options.instance do |opt|
261
+ opt.on_tail( "-h", "--help", "Show help" ) do
262
+ execute( [] )
263
+ end
189
264
  end
190
265
  end
191
266
  end
192
267
 
193
- def description
194
- 'Provides help for the individual commands'
195
- end
196
-
197
268
  def usage
198
- "Usage: #{@options.program_name} help COMMAND"
269
+ "Usage: #{commandparser.program_name} help [COMMAND SUBCOMMAND ...]"
199
270
  end
200
271
 
201
- def execute( commandParser, args )
272
+ def execute( args )
202
273
  if args.length > 0
203
- if commandParser.commands.include?( args[0] )
204
- commandParser.commands[args[0]].show_help( commandParser )
274
+ cmd = commandparser.main_command
275
+ arg = args.shift
276
+ while !arg.nil? && cmd.commands.keys.include?( arg )
277
+ cmd = cmd.commands[arg]
278
+ arg = args.shift
279
+ end
280
+ if arg.nil?
281
+ cmd.show_help
205
282
  else
206
- raise OptionParser::InvalidArgument, args[0]
283
+ raise InvalidArgumentError, args.unshift( arg ).join(' ')
207
284
  end
208
285
  else
209
- show_program_help( commandParser )
286
+ show_program_help
210
287
  end
211
288
  exit
212
289
  end
213
290
 
291
+ #######
214
292
  private
293
+ #######
215
294
 
216
- def show_program_help( commandParser )
217
- if commandParser.options.get_banner.nil?
218
- puts "Usage: #{commandParser.options.program_name} [global options] <command> [options] [args]"
219
- else
220
- puts commandParser.options.banner
221
- end
295
+ def show_program_help
296
+ puts "Usage: #{commandparser.program_name} [options] COMMAND [options] [COMMAND [options] ...] [args]"
222
297
  puts ""
223
- puts "Available commands:"
224
- width = commandParser.commands.keys.max {|a,b| a.length <=> b.length }.length
225
- commandParser.commands.sort.each do |name, command|
226
- print commandParser.options.summary_indent + name.ljust( width + 4 ) + command.description
227
- print " (=default command)" if name == commandParser.default
228
- print "\n"
229
- end
298
+ list_commands( 1, commandparser.main_command )
230
299
  puts ""
231
- puts commandParser.options.summarize
300
+ puts commandparser.main_command.options.summarize
301
+ puts
232
302
  end
233
303
 
234
304
  end
235
305
 
236
306
 
237
- # The default version command. It adds the options "-v" and "--version" to the global
238
- # +CommandParser+ options. When specified on the command line, it shows the version of the
239
- # program. The output can be controlled by options.
307
+ # The default version command. It adds the options "-v" and "--version" to the global options of
308
+ # the associated +CommandParser+. When specified on the command line, it shows the version of the
309
+ # program.
240
310
  class VersionCommand < Command
241
311
 
242
312
  def initialize
243
- super( 'version' )
244
- @fullversion = false
245
- options.separator "Options:"
246
- options.on( "-f", "--full", "Show the full version string" ) { @fullversion = true }
313
+ super( 'version', false )
314
+ self.short_desc = "Show the version of the program"
247
315
  end
248
316
 
249
- def init( commandParser )
250
- commandParser.options do |opt|
251
- opt.on_tail( "--version", "-v", "Show the version of the program" ) do
252
- execute( commandParser, [] )
317
+ def init
318
+ case commandparser.main_command.options
319
+ when OptionParserWrapper
320
+ commandparser.main_command.options.instance do |opt|
321
+ opt.on_tail( "--version", "-v", "Show the version of the program" ) do
322
+ execute( [] )
323
+ end
253
324
  end
254
325
  end
255
326
  end
256
327
 
257
- def description
258
- "Shows the version of the program"
259
- end
260
-
261
328
  def usage
262
- "Usage: #{@options.program_name} version [options]"
329
+ "Usage: #{commandparser.program_name} version"
263
330
  end
264
331
 
265
- def execute( commandParser, args )
266
- if @fullversion
267
- version = commandParser.options.ver
268
- else
269
- version = commandParser.options.version
270
- version = version.join( '.' ) if version.instance_of? Array
271
- end
272
- version = "<NO VERSION SPECIFIED>" if version.nil?
332
+ def execute( args )
333
+ version = commandparser.program_version
334
+ version = version.join( '.' ) if version.instance_of?( Array )
273
335
  puts version
274
336
  exit
275
337
  end
276
338
 
277
339
  end
278
340
 
279
- # Holds the registered commands.
280
- attr_reader :commands
281
-
282
- # Returns the name of the default command.
283
- attr_reader :default
284
341
 
285
- # Are Exceptions be handled gracefully? I.e. by printing error message and help screen?
286
- attr_reader :handleExceptions
342
+ # The main class for creating a command based CLI program.
343
+ class CommandParser
287
344
 
288
- # Create a new CommandParser object. The optional argument +handleExceptions+ specifies if the
289
- # object should handle exceptions gracefully.
290
- def initialize( handleExceptions = false )
291
- @options = OptionParser.new
292
- @commands = {}
293
- @default = nil
294
- @parsed = {}
295
- @handleExceptions = handleExceptions
296
- end
345
+ # The top level command representing the program itself.
346
+ attr_reader :main_command
297
347
 
298
- # If called with a block, this method yields the global options of the +CommandParser+. If no
299
- # block is specified, it returns the global options.
300
- def options # :yields: options
301
- if block_given?
302
- yield @options
303
- else
304
- @options
305
- end
306
- end
348
+ # The name of the program.
349
+ attr_accessor :program_name
307
350
 
308
- # Adds a command to the command list. If the optional parameter +default+ is true, then this
309
- # command is used when no command is specified on the command line.
310
- def add_command( command, default = false )
311
- @commands[command.name] = command
312
- @default = command.name if default
313
- command.init( self )
314
- end
351
+ # The version of the program.
352
+ attr_accessor :program_version
315
353
 
316
- # Parses the global options.
317
- def parse_global_options!( args )
318
- @options.order!( args )
319
- end
354
+ # Are Exceptions be handled gracefully? I.e. by printing error message and the help screen?
355
+ attr_reader :handle_exceptions
320
356
 
321
- # Parses the command.
322
- def parse_command!( args )
323
- @parsed[:command] = args.shift
324
- if @parsed[:command].nil?
325
- if @default.nil?
326
- raise NoCommandGivenError
327
- else
328
- @parsed[:command] = @default
329
- end
330
- else
331
- raise InvalidCommandError.new( @parsed[:command] ) unless commands.include?( @parsed[:command] )
357
+ # Create a new CommandParser object. The optional argument +handleExceptions+ specifies if the
358
+ # object should handle exceptions gracefully.
359
+ def initialize( handleExceptions = false )
360
+ @main_command = Command.new( 'mainCommand', true )
361
+ @main_command.super_command = self
362
+ @program_name = $0
363
+ @program_version = "0.0.0"
364
+ @handle_exceptions = handleExceptions
332
365
  end
333
- end
334
366
 
335
- # Parses the local options. Attention: The command has to be parsed (invoke method
336
- # +parse_command!+) before this method can be invoked.
337
- def parse_local_options!( args )
338
- if @parsed[:command]
339
- commands[@parsed[:command]].options.permute!( args ) unless commands[@parsed[:command]].options.nil?
367
+ # Returns the wrapper for parsing the global options.
368
+ def options
369
+ @main_command.options
340
370
  end
341
- end
342
371
 
343
- # Calls +parse+ - implemented to mimic OptionParser
344
- def permute( args ); parse( args ); end
345
- # Calls +parse!+ - implemented to mimic OptionParser
346
- def permute!( args ); parse!( args ); end
347
- # Calls +parse+ - implemented to mimic OptionParser
348
- def order( args ); parse( args ); end
349
- # Calls +parse!+ - implemented to mimic OptionParser
350
- def order!( args ); parse!( args ); end
351
- # see CommandParser#parse!
352
- def parse( args ); parse!( args.dup ); end
353
-
354
- # Parses the given argument. First it tries to parse global arguments if given. After that the
355
- # command name is analyzied and the options for the specific commands parsed. If +execCommand+ is
356
- # true, the command is executed immediately. If false, the <tt>CommandParser#execute</tt> has to
357
- # be called to execute the command. The optional +parse+ parameter specifies what should be
358
- # parsed. If <tt>:global</tt> is included in the +parse+ array, global options are parsed; if
359
- # <tt>:command</tt> is included, the command is parsed and if <tt>:local</tt> is included, the
360
- # local options are parsed.
361
- def parse!( args, execCommand = true, parse = [:global, :command, :local] )
362
- begin
363
- context = :global
364
- parse_global_options!( args ) if parse.include?( :global )
365
- parse_command!( args ) if parse.include?( :command )
366
-
367
- context = :local
368
- parse_local_options!( args ) if parse.include?( :local )
369
- rescue OptionParser::ParseError => e
370
- handle_exception( e, context )
372
+ # Sets the wrapper for parsing the global options.
373
+ def options=( wrapper )
374
+ @main_command.options = wrapper
371
375
  end
372
376
 
373
- @parsed[:args] = args
374
- execute if execCommand
375
- end
376
-
377
- # Executes the command. The method +CommandParser#parse!+ has to be called before this one!
378
- def execute
379
- begin
380
- commands[@parsed[:command]].execute( self, @parsed[:args] ) if @parsed[:command]
381
- rescue OptionParser::ParseError => e
382
- handle_exception( e, :local )
377
+ # Adds a top level command.
378
+ def add_command( *args )
379
+ @main_command.add_command( *args )
383
380
  end
384
- end
385
381
 
386
- private
382
+ # Parses the command line arguments. If a block is specified, the current hierarchy level and
383
+ # the name of the current command is yielded after the options for the level have been parsed.
384
+ def parse( argv = ARGV ) # :yields: level, commandName
385
+ level = 0
386
+ command = @main_command
387
+
388
+ while !command.nil?
389
+ argv = if command.has_commands? || ENV.include?( 'POSIXLY_CORRECT' )
390
+ command.options.order( argv )
391
+ else
392
+ command.options.permute( argv )
393
+ end
394
+ yield( level, command.name ) if block_given?
395
+
396
+ if command.has_commands?
397
+ cmdName, argv = argv[0], argv[1..-1] || []
398
+
399
+ if cmdName.nil?
400
+ if command.default_command.nil?
401
+ raise NoCommandGivenError
402
+ else
403
+ cmdName = command.default_command
404
+ end
405
+ else
406
+ raise InvalidCommandError.new( cmdName ) unless command.commands.include?( cmdName )
407
+ end
408
+
409
+ command = command.commands[cmdName]
410
+ level += 1
411
+ else
412
+ command.execute( argv )
413
+ command = nil
414
+ end
415
+ end
416
+ rescue ParseError, OptionParser::ParseError => e
417
+ raise if !@handle_exceptions
418
+ puts "Error while parsing command line:\n " + e.message
419
+ puts
420
+ @main_command.commands['help'].execute( command.super_commands.reverse.collect {|c| c.name} ) if @main_command.commands['help']
421
+ exit
422
+ end
387
423
 
388
- def handle_exception( exception, context )
389
- raise unless @handleExceptions
390
- s = (context == :global ? "global" : "command specific")
391
- puts "Error parsing #{s} options:\n " + exception.message
392
- puts
393
- commands['help'].execute( self, (context == :global ? [] : [@parsed[:command]]) ) if commands['help']
394
- exit
395
424
  end
396
425
 
397
426
  end
398
-