commander 3.3.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/History.rdoc +275 -0
- data/Manifest +36 -0
- data/README.rdoc +357 -0
- data/Rakefile +16 -0
- data/bin/commander +55 -0
- data/commander.gemspec +35 -0
- data/lib/commander.rb +31 -0
- data/lib/commander/blank.rb +8 -0
- data/lib/commander/command.rb +210 -0
- data/lib/commander/core_ext.rb +3 -0
- data/lib/commander/core_ext/array.rb +25 -0
- data/lib/commander/core_ext/object.rb +11 -0
- data/lib/commander/delegates.rb +13 -0
- data/lib/commander/help_formatters.rb +8 -0
- data/lib/commander/help_formatters/base.rb +18 -0
- data/lib/commander/help_formatters/terminal.rb +20 -0
- data/lib/commander/help_formatters/terminal/command_help.erb +35 -0
- data/lib/commander/help_formatters/terminal/help.erb +36 -0
- data/lib/commander/help_formatters/terminal_compact.rb +12 -0
- data/lib/commander/help_formatters/terminal_compact/command_help.erb +27 -0
- data/lib/commander/help_formatters/terminal_compact/help.erb +29 -0
- data/lib/commander/import.rb +10 -0
- data/lib/commander/runner.rb +386 -0
- data/lib/commander/user_interaction.rb +422 -0
- data/lib/commander/version.rb +4 -0
- data/spec/command_spec.rb +125 -0
- data/spec/core_ext/array_spec.rb +14 -0
- data/spec/core_ext/object_spec.rb +20 -0
- data/spec/help_formatters/base_spec.rb +24 -0
- data/spec/help_formatters/terminal_spec.rb +66 -0
- data/spec/runner_spec.rb +326 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/ui_spec.rb +11 -0
- data/tasks/docs.rake +18 -0
- data/tasks/gemspec.rake +3 -0
- data/tasks/spec.rake +25 -0
- metadata +127 -0
@@ -0,0 +1,422 @@
|
|
1
|
+
|
2
|
+
module Commander
|
3
|
+
|
4
|
+
##
|
5
|
+
# = User Interaction
|
6
|
+
#
|
7
|
+
# Commander's user interaction module mixes in common
|
8
|
+
# methods which extend HighLine's functionality such
|
9
|
+
# as a #password method rather than calling #ask directly.
|
10
|
+
|
11
|
+
module UI
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
#--
|
16
|
+
# Auto include growl when available.
|
17
|
+
#++
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'growl'
|
21
|
+
rescue LoadError
|
22
|
+
# Do nothing
|
23
|
+
else
|
24
|
+
include Growl
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Ask the user for a password. Specify a custom
|
29
|
+
# _message_ other than 'Password: ' or override the
|
30
|
+
# default _mask_ of '*'.
|
31
|
+
|
32
|
+
def password message = 'Password: ', mask = '*'
|
33
|
+
pass = ask(message) { |q| q.echo = mask }
|
34
|
+
pass = password message, mask if pass.nil? || pass.empty?
|
35
|
+
pass
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Choose from a set array of _choices_.
|
40
|
+
|
41
|
+
def choose message, *choices
|
42
|
+
say message
|
43
|
+
super *choices
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# 'Log' an _action_ to the terminal. This is typically used
|
48
|
+
# for verbose output regarding actions performed. For example:
|
49
|
+
#
|
50
|
+
# create path/to/file.rb
|
51
|
+
# remove path/to/old_file.rb
|
52
|
+
# remove path/to/old_file2.rb
|
53
|
+
#
|
54
|
+
|
55
|
+
def log action, *args
|
56
|
+
say '%15s %s' % [action, args.join(' ')]
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Speak _message_ using _voice_ which defaults
|
61
|
+
# to 'Alex', which is one of the better voices.
|
62
|
+
#
|
63
|
+
# === Examples
|
64
|
+
#
|
65
|
+
# speak 'What is your favorite food? '
|
66
|
+
# food = ask 'favorite food?: '
|
67
|
+
# speak "wow, I like #{food} too. We have so much alike."
|
68
|
+
#
|
69
|
+
# === Notes
|
70
|
+
#
|
71
|
+
# * MacOS only
|
72
|
+
#
|
73
|
+
|
74
|
+
def speak message, voice = :Alex
|
75
|
+
Thread.new { applescript "say #{message.inspect} using #{voice.to_s.inspect}" }
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Converse with speech recognition.
|
80
|
+
#
|
81
|
+
# Currently a "poorman's" DSL to utilize applescript and
|
82
|
+
# the MacOS speech recognition server.
|
83
|
+
#
|
84
|
+
# === Examples
|
85
|
+
#
|
86
|
+
# case converse 'What is the best food?', :cookies => 'Cookies', :unknown => 'Nothing'
|
87
|
+
# when :cookies
|
88
|
+
# speak 'o.m.g. you are awesome!'
|
89
|
+
# else
|
90
|
+
# case converse 'That is lame, shall I convince you cookies are the best?', :yes => 'Ok', :no => 'No', :maybe => 'Maybe another time'
|
91
|
+
# when :yes
|
92
|
+
# speak 'Well you see, cookies are just fantastic.'
|
93
|
+
# else
|
94
|
+
# speak 'Ok then, bye.'
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# === Notes
|
99
|
+
#
|
100
|
+
# * MacOS only
|
101
|
+
#
|
102
|
+
|
103
|
+
def converse prompt, responses = {}
|
104
|
+
i, commands = 0, responses.map { |key, value| value.inspect }.join(',')
|
105
|
+
statement = responses.inject '' do |statement, (key, value)|
|
106
|
+
statement << (((i += 1) == 1 ?
|
107
|
+
%(if response is "#{value}" then\n):
|
108
|
+
%(else if response is "#{value}" then\n))) <<
|
109
|
+
%(do shell script "echo '#{key}'"\n)
|
110
|
+
end
|
111
|
+
applescript(%(
|
112
|
+
tell application "SpeechRecognitionServer"
|
113
|
+
set response to listen for {#{commands}} with prompt "#{prompt}"
|
114
|
+
#{statement}
|
115
|
+
end if
|
116
|
+
end tell
|
117
|
+
)).strip.to_sym
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Execute apple _script_.
|
122
|
+
|
123
|
+
def applescript script
|
124
|
+
`osascript -e "#{ script.gsub('"', '\"') }"`
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Normalize IO streams, allowing for redirection of
|
129
|
+
# +input+ and/or +output+, for example:
|
130
|
+
#
|
131
|
+
# $ foo # => read from terminal I/O
|
132
|
+
# $ foo in # => read from 'in' file, output to terminal output stream
|
133
|
+
# $ foo in out # => read from 'in' file, output to 'out' file
|
134
|
+
# $ foo < in > out # => equivalent to above (essentially)
|
135
|
+
#
|
136
|
+
# Optionally a +block+ may be supplied, in which case
|
137
|
+
# IO will be reset once the block has executed.
|
138
|
+
#
|
139
|
+
# === Examples
|
140
|
+
#
|
141
|
+
# command :foo do |c|
|
142
|
+
# c.syntax = 'foo [input] [output]'
|
143
|
+
# c.when_called do |args, options|
|
144
|
+
# # or io(args.shift, args.shift)
|
145
|
+
# io *args
|
146
|
+
# str = $stdin.gets
|
147
|
+
# puts 'input was: ' + str.inspect
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
|
152
|
+
def io input = nil, output = nil, &block
|
153
|
+
$stdin = File.new(input) if input
|
154
|
+
$stdout = File.new(output, 'r+') if output
|
155
|
+
if block
|
156
|
+
yield
|
157
|
+
reset_io
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Reset IO to initial constant streams.
|
163
|
+
|
164
|
+
def reset_io
|
165
|
+
$stdin, $stdout = STDIN, STDOUT
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# Prompt _editor_ for input. Optionally supply initial
|
170
|
+
# _input_ which is written to the editor.
|
171
|
+
#
|
172
|
+
# The _editor_ defaults to the EDITOR environment variable
|
173
|
+
# when present, or 'mate' for TextMate.
|
174
|
+
#
|
175
|
+
# === Examples
|
176
|
+
#
|
177
|
+
# ask_editor # => prompts EDITOR with no input
|
178
|
+
# ask_editor('foo') # => prompts EDITOR with default text of 'foo'
|
179
|
+
# ask_editor('foo', :mate) # => prompts TextMate with default text of 'foo'
|
180
|
+
#
|
181
|
+
|
182
|
+
def ask_editor input = nil, editor = ENV['EDITOR'] || 'mate'
|
183
|
+
IO.popen(editor.to_s, 'w+') do |pipe|
|
184
|
+
pipe.puts input.to_s unless input.nil?
|
185
|
+
pipe.close_write
|
186
|
+
pipe.read
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Enable paging of output after called.
|
192
|
+
|
193
|
+
def enable_paging
|
194
|
+
return unless $stdout.tty?
|
195
|
+
read, write = IO.pipe
|
196
|
+
|
197
|
+
if Kernel.fork
|
198
|
+
$stdin.reopen read
|
199
|
+
read.close; write.close
|
200
|
+
Kernel.select [$stdin]
|
201
|
+
ENV['LESS'] = 'FSRX'
|
202
|
+
pager = ENV['PAGER'] || 'less'
|
203
|
+
exec pager rescue exec '/bin/sh', '-c', pager
|
204
|
+
else
|
205
|
+
$stdout.reopen write
|
206
|
+
$stderr.reopen write if $stderr.tty?
|
207
|
+
read.close; write.close
|
208
|
+
return
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
##
|
213
|
+
# Output progress while iterating _arr_.
|
214
|
+
#
|
215
|
+
# === Examples
|
216
|
+
#
|
217
|
+
# uris = %w( http://vision-media.ca http://google.com )
|
218
|
+
# progress uris, :format => "Remaining: :time_remaining" do |uri|
|
219
|
+
# res = open uri
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
|
223
|
+
def progress arr, options = {}, &block
|
224
|
+
bar = ProgressBar.new arr.length, options
|
225
|
+
arr.each { |v| bar.increment yield(v) }
|
226
|
+
end
|
227
|
+
|
228
|
+
##
|
229
|
+
# Implements ask_for_CLASS methods.
|
230
|
+
|
231
|
+
module AskForClass
|
232
|
+
def method_missing meth, *args, &block
|
233
|
+
case meth.to_s
|
234
|
+
when /^ask_for_([\w]+)/ ; $terminal.ask(args.first, Kernel.const_get($1.capitalize))
|
235
|
+
else super
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Substitute _hash_'s keys with their associated values in _str_.
|
242
|
+
|
243
|
+
def replace_tokens str, hash #:nodoc:
|
244
|
+
hash.inject str do |str, (key, value)|
|
245
|
+
str.gsub ":#{key}", value.to_s
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
##
|
250
|
+
# = Progress Bar
|
251
|
+
#
|
252
|
+
# Terminal progress bar utility. In its most basic form
|
253
|
+
# requires that the developer specifies when the bar should
|
254
|
+
# be incremented. Note that a hash of tokens may be passed to
|
255
|
+
# #increment, (or returned when using Object#progress).
|
256
|
+
#
|
257
|
+
# uris = %w(
|
258
|
+
# http://vision-media.ca
|
259
|
+
# http://yahoo.com
|
260
|
+
# http://google.com
|
261
|
+
# )
|
262
|
+
#
|
263
|
+
# bar = Commander::UI::ProgressBar.new uris.length, options
|
264
|
+
# threads = []
|
265
|
+
# uris.each do |uri|
|
266
|
+
# threads << Thread.new do
|
267
|
+
# begin
|
268
|
+
# res = open uri
|
269
|
+
# bar.increment :uri => uri
|
270
|
+
# rescue Exception => e
|
271
|
+
# bar.increment :uri => "#{uri} failed"
|
272
|
+
# end
|
273
|
+
# end
|
274
|
+
# end
|
275
|
+
# threads.each { |t| t.join }
|
276
|
+
#
|
277
|
+
# The Object method #progress is also available:
|
278
|
+
#
|
279
|
+
# progress uris, :width => 10 do |uri|
|
280
|
+
# res = open uri
|
281
|
+
# { :uri => uri } # Can now use :uri within :format option
|
282
|
+
# end
|
283
|
+
#
|
284
|
+
|
285
|
+
class ProgressBar
|
286
|
+
|
287
|
+
##
|
288
|
+
# Creates a new progress bar.
|
289
|
+
#
|
290
|
+
# === Options
|
291
|
+
#
|
292
|
+
# :title Title, defaults to "Progress"
|
293
|
+
# :width Width of :progress_bar
|
294
|
+
# :progress_str Progress string, defaults to "="
|
295
|
+
# :incomplete_str Incomplete bar string, defaults to '.'
|
296
|
+
# :format Defaults to ":title |:progress_bar| :percent_complete% complete "
|
297
|
+
# :tokens Additional tokens replaced within the format string
|
298
|
+
# :complete_message Defaults to "Process complete"
|
299
|
+
#
|
300
|
+
# === Tokens
|
301
|
+
#
|
302
|
+
# :title
|
303
|
+
# :percent_complete
|
304
|
+
# :progress_bar
|
305
|
+
# :step
|
306
|
+
# :steps_remaining
|
307
|
+
# :total_steps
|
308
|
+
# :time_elapsed
|
309
|
+
# :time_remaining
|
310
|
+
#
|
311
|
+
|
312
|
+
def initialize total, options = {}
|
313
|
+
@total_steps, @step, @start_time = total, 0, Time.now
|
314
|
+
@title = options.fetch :title, 'Progress'
|
315
|
+
@width = options.fetch :width, 25
|
316
|
+
@progress_str = options.fetch :progress_str, '='
|
317
|
+
@incomplete_str = options.fetch :incomplete_str, '.'
|
318
|
+
@complete_message = options.fetch :complete_message, 'Process complete'
|
319
|
+
@format = options.fetch :format, ':title |:progress_bar| :percent_complete% complete '
|
320
|
+
@tokens = options.fetch :tokens, {}
|
321
|
+
end
|
322
|
+
|
323
|
+
##
|
324
|
+
# Completion percentage.
|
325
|
+
|
326
|
+
def percent_complete
|
327
|
+
@step * 100 / @total_steps
|
328
|
+
end
|
329
|
+
|
330
|
+
##
|
331
|
+
# Time that has elapsed since the operation started.
|
332
|
+
|
333
|
+
def time_elapsed
|
334
|
+
Time.now - @start_time
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Estimated time remaining.
|
339
|
+
|
340
|
+
def time_remaining
|
341
|
+
(time_elapsed / @step) * steps_remaining
|
342
|
+
end
|
343
|
+
|
344
|
+
##
|
345
|
+
# Number of steps left.
|
346
|
+
|
347
|
+
def steps_remaining
|
348
|
+
@total_steps - @step
|
349
|
+
end
|
350
|
+
|
351
|
+
##
|
352
|
+
# Formatted progress bar.
|
353
|
+
|
354
|
+
def progress_bar
|
355
|
+
(@progress_str * (@width * percent_complete / 100)).ljust @width, @incomplete_str
|
356
|
+
end
|
357
|
+
|
358
|
+
##
|
359
|
+
# Generates tokens for this step.
|
360
|
+
|
361
|
+
def generate_tokens
|
362
|
+
{
|
363
|
+
:title => @title,
|
364
|
+
:percent_complete => percent_complete,
|
365
|
+
:progress_bar => progress_bar,
|
366
|
+
:step => @step,
|
367
|
+
:steps_remaining => steps_remaining,
|
368
|
+
:total_steps => @total_steps,
|
369
|
+
:time_elapsed => "%0.2fs" % time_elapsed,
|
370
|
+
:time_remaining => "%0.2fs" % time_remaining,
|
371
|
+
}.
|
372
|
+
merge! @tokens
|
373
|
+
end
|
374
|
+
|
375
|
+
##
|
376
|
+
# Output the progress bar.
|
377
|
+
|
378
|
+
def show
|
379
|
+
unless finished?
|
380
|
+
erase_line
|
381
|
+
if completed?
|
382
|
+
$terminal.say UI.replace_tokens(@complete_message, generate_tokens) if @complete_message.is_a? String
|
383
|
+
else
|
384
|
+
$terminal.say UI.replace_tokens(@format, generate_tokens) << ' '
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
##
|
390
|
+
# Whether or not the operation is complete, and we have finished.
|
391
|
+
|
392
|
+
def finished?
|
393
|
+
@step == @total_steps + 1
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
# Whether or not the operation has completed.
|
398
|
+
|
399
|
+
def completed?
|
400
|
+
@step == @total_steps
|
401
|
+
end
|
402
|
+
|
403
|
+
##
|
404
|
+
# Increment progress. Optionally pass _tokens_ which
|
405
|
+
# can be displayed in the output format.
|
406
|
+
|
407
|
+
def increment tokens = {}
|
408
|
+
@step += 1
|
409
|
+
@tokens.merge! tokens if tokens.is_a? Hash
|
410
|
+
show
|
411
|
+
end
|
412
|
+
|
413
|
+
##
|
414
|
+
# Erase previous terminal line.
|
415
|
+
|
416
|
+
def erase_line
|
417
|
+
# highline does not expose the output stream
|
418
|
+
$terminal.instance_variable_get('@output').print "\r\e[K"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
|
2
|
+
describe Commander::Command do
|
3
|
+
|
4
|
+
before :each do
|
5
|
+
mock_terminal
|
6
|
+
create_test_command
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'Options' do
|
10
|
+
before :each do
|
11
|
+
@options = Commander::Command::Options.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should act like an open struct" do
|
15
|
+
@options.send = 'mail'
|
16
|
+
@options.call = true
|
17
|
+
@options.send.should == 'mail'
|
18
|
+
@options.call.should == true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should allow __send__ to function as always" do
|
22
|
+
@options.send = 'foo'
|
23
|
+
@options.__send__(:send).should == 'foo'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#option" do
|
28
|
+
it "should add options" do
|
29
|
+
lambda { @command.option '--recursive' }.should change(@command.options, :length).from(1).to(2)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should allow procs as option handlers" do
|
33
|
+
@command.option('--recursive') { |recursive| recursive.should be_true }
|
34
|
+
@command.run '--recursive'
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should allow usage of common method names" do
|
38
|
+
@command.option '--open file'
|
39
|
+
@command.when_called { |_, options| options.open.should == 'foo' }
|
40
|
+
@command.run '--open', 'foo'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#run" do
|
45
|
+
describe "should invoke #when_called" do
|
46
|
+
it "with arguments seperated from options" do
|
47
|
+
@command.when_called { |args, options| args.join(' ').should == 'just some args' }
|
48
|
+
@command.run '--verbose', 'just', 'some', 'args'
|
49
|
+
end
|
50
|
+
|
51
|
+
it "calling the #call method by default when an object is called" do
|
52
|
+
object = mock 'Object'
|
53
|
+
object.should_receive(:call).once
|
54
|
+
@command.when_called object
|
55
|
+
@command.run 'foo'
|
56
|
+
end
|
57
|
+
|
58
|
+
it "calling an arbitrary method when an object is called" do
|
59
|
+
object = mock 'Object'
|
60
|
+
object.should_receive(:foo).once
|
61
|
+
@command.when_called object, :foo
|
62
|
+
@command.run 'foo'
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should raise an error when no handler is present" do
|
66
|
+
lambda { @command.when_called }.should raise_error(ArgumentError)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "should populate options with" do
|
71
|
+
it "boolean values" do
|
72
|
+
@command.option '--[no-]toggle'
|
73
|
+
@command.when_called { |_, options| options.toggle.should be_true }
|
74
|
+
@command.run '--toggle'
|
75
|
+
@command.when_called { |_, options| options.toggle.should be_false }
|
76
|
+
@command.run '--no-toggle'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "manditory arguments" do
|
80
|
+
@command.option '--file FILE'
|
81
|
+
@command.when_called { |_, options| options.file.should == 'foo' }
|
82
|
+
@command.run '--file', 'foo'
|
83
|
+
lambda { @command.run '--file' }.should raise_error(OptionParser::MissingArgument)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "optional arguments" do
|
87
|
+
@command.option '--use-config [file] '
|
88
|
+
@command.when_called { |_, options| options.use_config.should == 'foo' }
|
89
|
+
@command.run '--use-config', 'foo'
|
90
|
+
@command.when_called { |_, options| options.use_config.should be_nil }
|
91
|
+
@command.run '--use-config'
|
92
|
+
@command.option '--interval N', Integer
|
93
|
+
@command.when_called { |_, options| options.interval.should == 5 }
|
94
|
+
@command.run '--interval', '5'
|
95
|
+
lambda { @command.run '--interval', 'invalid' }.should raise_error(OptionParser::InvalidArgument)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "lists" do
|
99
|
+
@command.option '--fav COLORS', Array
|
100
|
+
@command.when_called { |_, options| options.fav.should == ['red', 'green', 'blue'] }
|
101
|
+
@command.run '--fav', 'red,green,blue'
|
102
|
+
end
|
103
|
+
|
104
|
+
it "lists with multi-word items" do
|
105
|
+
@command.option '--fav MOVIES', Array
|
106
|
+
@command.when_called { |_, options| options.fav.should == ['super\ bad', 'nightmare'] }
|
107
|
+
@command.run '--fav', 'super\ bad,nightmare'
|
108
|
+
end
|
109
|
+
|
110
|
+
it "defaults" do
|
111
|
+
@command.option '--files LIST', Array
|
112
|
+
@command.option '--interval N', Integer
|
113
|
+
@command.when_called do |_, options|
|
114
|
+
options.default \
|
115
|
+
:files => ['foo', 'bar'],
|
116
|
+
:interval => 5
|
117
|
+
options.files.should == ['foo', 'bar']
|
118
|
+
options.interval.should == 15
|
119
|
+
end
|
120
|
+
@command.run '--interval', '15'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|