commander 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+
2
+ module Commander
3
+ VERSION = '3.3.0'
4
+ 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