murano-cli-commander 4.4.10

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +44 -0
  5. data/.rubocop_todo.yml +77 -0
  6. data/.travis.yml +14 -0
  7. data/DEVELOPMENT +15 -0
  8. data/Gemfile +3 -0
  9. data/History.rdoc +446 -0
  10. data/LICENSE +22 -0
  11. data/Manifest +38 -0
  12. data/README.md +475 -0
  13. data/Rakefile +13 -0
  14. data/bin/murano-cli-commander +104 -0
  15. data/lib/murano-cli-commander.rb +35 -0
  16. data/lib/murano-cli-commander/blank.rb +7 -0
  17. data/lib/murano-cli-commander/command.rb +214 -0
  18. data/lib/murano-cli-commander/configure.rb +14 -0
  19. data/lib/murano-cli-commander/core_ext.rb +2 -0
  20. data/lib/murano-cli-commander/core_ext/array.rb +24 -0
  21. data/lib/murano-cli-commander/core_ext/object.rb +8 -0
  22. data/lib/murano-cli-commander/delegates.rb +25 -0
  23. data/lib/murano-cli-commander/help_formatters.rb +49 -0
  24. data/lib/murano-cli-commander/help_formatters/base.rb +24 -0
  25. data/lib/murano-cli-commander/help_formatters/terminal.rb +19 -0
  26. data/lib/murano-cli-commander/help_formatters/terminal/command_help.erb +35 -0
  27. data/lib/murano-cli-commander/help_formatters/terminal/help.erb +36 -0
  28. data/lib/murano-cli-commander/help_formatters/terminal_compact.rb +11 -0
  29. data/lib/murano-cli-commander/help_formatters/terminal_compact/command_help.erb +27 -0
  30. data/lib/murano-cli-commander/help_formatters/terminal_compact/help.erb +29 -0
  31. data/lib/murano-cli-commander/import.rb +5 -0
  32. data/lib/murano-cli-commander/methods.rb +11 -0
  33. data/lib/murano-cli-commander/platform.rb +7 -0
  34. data/lib/murano-cli-commander/runner.rb +455 -0
  35. data/lib/murano-cli-commander/user_interaction.rb +551 -0
  36. data/lib/murano-cli-commander/version.rb +3 -0
  37. data/murano-cli-commander.gemspec +36 -0
  38. data/spec/command_spec.rb +169 -0
  39. data/spec/configure_spec.rb +37 -0
  40. data/spec/core_ext/array_spec.rb +18 -0
  41. data/spec/core_ext/object_spec.rb +19 -0
  42. data/spec/help_formatters/terminal_compact_spec.rb +69 -0
  43. data/spec/help_formatters/terminal_spec.rb +67 -0
  44. data/spec/methods_spec.rb +61 -0
  45. data/spec/runner_spec.rb +646 -0
  46. data/spec/spec_helper.rb +78 -0
  47. data/spec/ui_spec.rb +30 -0
  48. metadata +163 -0
@@ -0,0 +1,551 @@
1
+ require 'tempfile'
2
+ require 'shellwords'
3
+
4
+ module Commander
5
+ ##
6
+ # = User Interaction
7
+ #
8
+ # Commander's user interaction module mixes in common
9
+ # methods which extend HighLine's functionality such
10
+ # as a #password method rather than calling #ask directly.
11
+
12
+ module UI
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 = nil, *choices, &block)
42
+ say message if message
43
+ super(*choices, &block)
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 format('%15s %s', action, args.join(' '))
57
+ end
58
+
59
+ ##
60
+ # 'Say' something using the OK color (green).
61
+ #
62
+ # === Examples
63
+ # say_ok 'Everything is fine'
64
+ # say_ok 'It is ok', 'This is ok too'
65
+ #
66
+
67
+ def say_ok(*args)
68
+ args.each do |arg|
69
+ say $terminal.color(arg, :green)
70
+ end
71
+ end
72
+
73
+ ##
74
+ # 'Say' something using the WARNING color (yellow).
75
+ #
76
+ # === Examples
77
+ # say_warning 'This is a warning'
78
+ # say_warning 'Be careful', 'Think about it'
79
+ #
80
+
81
+ def say_warning(*args)
82
+ args.each do |arg|
83
+ say $terminal.color(arg, :yellow)
84
+ end
85
+ end
86
+
87
+ ##
88
+ # 'Say' something using the ERROR color (red).
89
+ #
90
+ # === Examples
91
+ # say_error 'Everything is not fine'
92
+ # say_error 'It is not ok', 'This is not ok too'
93
+ #
94
+
95
+ def say_error(*args)
96
+ args.each do |arg|
97
+ say $terminal.color(arg, :red)
98
+ end
99
+ end
100
+
101
+ ##
102
+ # 'Say' something using the specified color
103
+ #
104
+ # === Examples
105
+ # color 'I am blue', :blue
106
+ # color 'I am bold', :bold
107
+ # color 'White on Red', :white, :on_red
108
+ #
109
+ # === Notes
110
+ # You may use:
111
+ # * color: black blue cyan green magenta red white yellow
112
+ # * style: blink bold clear underline
113
+ # * highligh: on_<color>
114
+
115
+ def color(*args)
116
+ say $terminal.color(*args)
117
+ end
118
+
119
+ ##
120
+ # Speak _message_ using _voice_ at a speaking rate of _rate_
121
+ #
122
+ # Voice defaults to 'Alex', which is one of the better voices.
123
+ # Speaking rate defaults to 175 words per minute
124
+ #
125
+ # === Examples
126
+ #
127
+ # speak 'What is your favorite food? '
128
+ # food = ask 'favorite food?: '
129
+ # speak "Wow, I like #{food} too. We have so much in common."
130
+ # speak "I like #{food} as well!", "Victoria", 190
131
+ #
132
+ # === Notes
133
+ #
134
+ # * MacOS only
135
+ #
136
+
137
+ def speak(message, voice = :Alex, rate = 175)
138
+ Thread.new { applescript "say #{message.inspect} using #{voice.to_s.inspect} speaking rate #{rate}" }
139
+ end
140
+
141
+ ##
142
+ # Converse with speech recognition.
143
+ #
144
+ # Currently a "poorman's" DSL to utilize applescript and
145
+ # the MacOS speech recognition server.
146
+ #
147
+ # === Examples
148
+ #
149
+ # case converse 'What is the best food?', :cookies => 'Cookies', :unknown => 'Nothing'
150
+ # when :cookies
151
+ # speak 'o.m.g. you are awesome!'
152
+ # else
153
+ # case converse 'That is lame, shall I convince you cookies are the best?', :yes => 'Ok', :no => 'No', :maybe => 'Maybe another time'
154
+ # when :yes
155
+ # speak 'Well you see, cookies are just fantastic.'
156
+ # else
157
+ # speak 'Ok then, bye.'
158
+ # end
159
+ # end
160
+ #
161
+ # === Notes
162
+ #
163
+ # * MacOS only
164
+ #
165
+
166
+ def converse(prompt, responses = {})
167
+ i, commands = 0, responses.map { |_key, value| value.inspect }.join(',')
168
+ statement = responses.inject '' do |inner_statement, (key, value)|
169
+ inner_statement <<
170
+ (
171
+ (i += 1) == 1 ?
172
+ %(if response is "#{value}" then\n) :
173
+ %(else if response is "#{value}" then\n)
174
+ ) <<
175
+ %(do shell script "echo '#{key}'"\n)
176
+ end
177
+ applescript(
178
+ %(
179
+ tell application "SpeechRecognitionServer"
180
+ set response to listen for {#{commands}} with prompt "#{prompt}"
181
+ #{statement}
182
+ end if
183
+ end tell
184
+ )
185
+ ).strip.to_sym
186
+ end
187
+
188
+ ##
189
+ # Execute apple _script_.
190
+
191
+ def applescript(script)
192
+ `osascript -e "#{ script.gsub('"', '\"') }"`
193
+ end
194
+
195
+ ##
196
+ # Normalize IO streams, allowing for redirection of
197
+ # +input+ and/or +output+, for example:
198
+ #
199
+ # $ foo # => read from terminal I/O
200
+ # $ foo in # => read from 'in' file, output to terminal output stream
201
+ # $ foo in out # => read from 'in' file, output to 'out' file
202
+ # $ foo < in > out # => equivalent to above (essentially)
203
+ #
204
+ # Optionally a +block+ may be supplied, in which case
205
+ # IO will be reset once the block has executed.
206
+ #
207
+ # === Examples
208
+ #
209
+ # command :foo do |c|
210
+ # c.syntax = 'foo [input] [output]'
211
+ # c.when_called do |args, options|
212
+ # # or io(args.shift, args.shift)
213
+ # io *args
214
+ # str = $stdin.gets
215
+ # puts 'input was: ' + str.inspect
216
+ # end
217
+ # end
218
+ #
219
+
220
+ def io(input = nil, output = nil, &block)
221
+ $stdin = File.new(input) if input
222
+ $stdout = File.new(output, 'r+') if output
223
+ return unless block
224
+ yield
225
+ reset_io
226
+ end
227
+
228
+ ##
229
+ # Reset IO to initial constant streams.
230
+
231
+ def reset_io
232
+ $stdin, $stdout = STDIN, STDOUT
233
+ end
234
+
235
+ ##
236
+ # Find an editor available in path. Optionally supply the _preferred_
237
+ # editor. Returns the name as a string, nil if none is available.
238
+
239
+ def available_editor(preferred = nil)
240
+ [preferred, ENV['EDITOR'], 'mate -w', 'vim', 'vi', 'emacs', 'nano', 'pico']
241
+ .compact
242
+ .find { |name| system("hash #{name.split.first} 2>&-") }
243
+ end
244
+
245
+ ##
246
+ # Prompt an editor for input. Optionally supply initial
247
+ # _input_ which is written to the editor.
248
+ #
249
+ # _preferred_editor_ can be hinted.
250
+ #
251
+ # === Examples
252
+ #
253
+ # ask_editor # => prompts EDITOR with no input
254
+ # ask_editor('foo') # => prompts EDITOR with default text of 'foo'
255
+ # ask_editor('foo', 'mate -w') # => prompts TextMate with default text of 'foo'
256
+ #
257
+
258
+ def ask_editor(input = nil, preferred_editor = nil)
259
+ editor = available_editor preferred_editor
260
+ program = Commander::Runner.instance.program(:name).downcase rescue 'murano-cli-commander'
261
+ tmpfile = Tempfile.new program
262
+ begin
263
+ tmpfile.write input if input
264
+ tmpfile.close
265
+ system("#{editor} #{tmpfile.path.shellescape}") ? IO.read(tmpfile.path) : nil
266
+ ensure
267
+ tmpfile.unlink
268
+ end
269
+ end
270
+
271
+ ##
272
+ # Enable paging of output after called.
273
+
274
+ def enable_paging
275
+ return unless $stdout.tty?
276
+ return unless Process.respond_to? :fork
277
+ read, write = IO.pipe
278
+
279
+ # Kernel.fork is not supported on all platforms and configurations.
280
+ # As of Ruby 1.9, `Process.respond_to? :fork` should return false on
281
+ # configurations that don't support it, but versions before 1.9 don't
282
+ # seem to do this reliably and instead raise a NotImplementedError
283
+ # (which is rescued below).
284
+
285
+ if Kernel.fork
286
+ $stdin.reopen read
287
+ write.close
288
+ read.close
289
+ Kernel.select [$stdin]
290
+ ENV['LESS'] = 'FSRX' unless ENV.key? 'LESS'
291
+ pager = ENV['PAGER'] || 'less'
292
+ exec pager rescue exec '/bin/sh', '-c', pager
293
+ else
294
+ # subprocess
295
+ $stdout.reopen write
296
+ $stderr.reopen write if $stderr.tty?
297
+ write.close
298
+ read.close
299
+ end
300
+ rescue NotImplementedError
301
+ ensure
302
+ write.close if write && !write.closed?
303
+ read.close if read && !read.closed?
304
+ end
305
+
306
+ ##
307
+ # Output progress while iterating _arr_.
308
+ #
309
+ # === Examples
310
+ #
311
+ # uris = %w( http://vision-media.ca http://google.com )
312
+ # progress uris, :format => "Remaining: :time_remaining" do |uri|
313
+ # res = open uri
314
+ # end
315
+ #
316
+
317
+ def progress(arr, options = {})
318
+ bar = ProgressBar.new arr.length, options
319
+ bar.show
320
+ arr.each { |v| bar.increment yield(v) }
321
+ end
322
+
323
+ ##
324
+ # Implements ask_for_CLASS methods.
325
+
326
+ module AskForClass
327
+ DEPRECATED_CONSTANTS = [:Config, :TimeoutError, :MissingSourceFile, :NIL, :TRUE, :FALSE, :Fixnum, :Bignum, :Data].freeze
328
+
329
+ # define methods for common classes
330
+ [Float, Integer, String, Symbol, Regexp, Array, File, Pathname].each do |klass|
331
+ define_method "ask_for_#{klass.to_s.downcase}" do |prompt|
332
+ $terminal.ask(prompt, klass)
333
+ end
334
+ end
335
+
336
+ def method_missing(method_name, *arguments, &block)
337
+ if method_name.to_s =~ /^ask_for_(.*)/
338
+ if arguments.count != 1
339
+ fail ArgumentError, "wrong number of arguments (given #{arguments.count}, expected 1)"
340
+ end
341
+ prompt = arguments.first
342
+ requested_class = Regexp.last_match[1]
343
+
344
+ # All Classes that respond to #parse
345
+ # Ignore constants that trigger deprecation warnings
346
+ available_classes = (Object.constants - DEPRECATED_CONSTANTS).map do |const|
347
+ Object.const_get(const)
348
+ end.select do |const|
349
+ const.class == Class && const.respond_to?(:parse)
350
+ end
351
+
352
+ klass = available_classes.find { |k| k.to_s.downcase == requested_class }
353
+ if klass
354
+ $terminal.ask(prompt, klass)
355
+ else
356
+ super
357
+ end
358
+ else
359
+ super
360
+ end
361
+ end
362
+
363
+ def respond_to_missing?(method_name, include_private = false)
364
+ method_name.to_s.start_with?('ask_for_') || super
365
+ end
366
+ end
367
+
368
+ ##
369
+ # Substitute _hash_'s keys with their associated values in _str_.
370
+
371
+ def replace_tokens(str, hash) #:nodoc:
372
+ hash.inject(str) do |string, (key, value)|
373
+ string.gsub ":#{key}", value.to_s
374
+ end
375
+ end
376
+
377
+ ##
378
+ # = Progress Bar
379
+ #
380
+ # Terminal progress bar utility. In its most basic form
381
+ # requires that the developer specifies when the bar should
382
+ # be incremented. Note that a hash of tokens may be passed to
383
+ # #increment, (or returned when using Object#progress).
384
+ #
385
+ # uris = %w(
386
+ # http://vision-media.ca
387
+ # http://yahoo.com
388
+ # http://google.com
389
+ # )
390
+ #
391
+ # bar = Commander::UI::ProgressBar.new uris.length, options
392
+ # threads = []
393
+ # uris.each do |uri|
394
+ # threads << Thread.new do
395
+ # begin
396
+ # res = open uri
397
+ # bar.increment :uri => uri
398
+ # rescue Exception => e
399
+ # bar.increment :uri => "#{uri} failed"
400
+ # end
401
+ # end
402
+ # end
403
+ # threads.each { |t| t.join }
404
+ #
405
+ # The Object method #progress is also available:
406
+ #
407
+ # progress uris, :width => 10 do |uri|
408
+ # res = open uri
409
+ # { :uri => uri } # Can now use :uri within :format option
410
+ # end
411
+ #
412
+
413
+ class ProgressBar
414
+ ##
415
+ # Creates a new progress bar.
416
+ #
417
+ # === Options
418
+ #
419
+ # :title Title, defaults to "Progress"
420
+ # :width Width of :progress_bar
421
+ # :progress_str Progress string, defaults to "="
422
+ # :incomplete_str Incomplete bar string, defaults to '.'
423
+ # :format Defaults to ":title |:progress_bar| :percent_complete% complete "
424
+ # :tokens Additional tokens replaced within the format string
425
+ # :complete_message Defaults to "Process complete"
426
+ #
427
+ # === Tokens
428
+ #
429
+ # :title
430
+ # :percent_complete
431
+ # :progress_bar
432
+ # :step
433
+ # :steps_remaining
434
+ # :total_steps
435
+ # :time_elapsed
436
+ # :time_remaining
437
+ #
438
+
439
+ def initialize(total, options = {})
440
+ @total_steps, @step, @start_time = total, 0, Time.now
441
+ @title = options.fetch :title, 'Progress'
442
+ @width = options.fetch :width, 25
443
+ @progress_str = options.fetch :progress_str, '='
444
+ @incomplete_str = options.fetch :incomplete_str, '.'
445
+ @complete_message = options.fetch :complete_message, 'Process complete'
446
+ @format = options.fetch :format, ':title |:progress_bar| :percent_complete% complete '
447
+ @tokens = options.fetch :tokens, {}
448
+ end
449
+
450
+ ##
451
+ # Completion percentage.
452
+
453
+ def percent_complete
454
+ if @total_steps.zero?
455
+ 100
456
+ else
457
+ @step * 100 / @total_steps
458
+ end
459
+ end
460
+
461
+ ##
462
+ # Time that has elapsed since the operation started.
463
+
464
+ def time_elapsed
465
+ Time.now - @start_time
466
+ end
467
+
468
+ ##
469
+ # Estimated time remaining.
470
+
471
+ def time_remaining
472
+ (time_elapsed / @step) * steps_remaining
473
+ end
474
+
475
+ ##
476
+ # Number of steps left.
477
+
478
+ def steps_remaining
479
+ @total_steps - @step
480
+ end
481
+
482
+ ##
483
+ # Formatted progress bar.
484
+
485
+ def progress_bar
486
+ (@progress_str * (@width * percent_complete / 100)).ljust @width, @incomplete_str
487
+ end
488
+
489
+ ##
490
+ # Generates tokens for this step.
491
+
492
+ def generate_tokens
493
+ {
494
+ title: @title,
495
+ percent_complete: percent_complete,
496
+ progress_bar: progress_bar,
497
+ step: @step,
498
+ steps_remaining: steps_remaining,
499
+ total_steps: @total_steps,
500
+ time_elapsed: format('%0.2fs', time_elapsed),
501
+ time_remaining: @step > 0 ? format('%0.2fs', time_remaining) : '',
502
+ }.merge! @tokens
503
+ end
504
+
505
+ ##
506
+ # Output the progress bar.
507
+
508
+ def show
509
+ return if finished?
510
+ erase_line
511
+ if completed?
512
+ $terminal.say UI.replace_tokens(@complete_message, generate_tokens) if @complete_message.is_a? String
513
+ else
514
+ $terminal.say UI.replace_tokens(@format, generate_tokens) << ' '
515
+ end
516
+ end
517
+
518
+ ##
519
+ # Whether or not the operation is complete, and we have finished.
520
+
521
+ def finished?
522
+ @step == @total_steps + 1
523
+ end
524
+
525
+ ##
526
+ # Whether or not the operation has completed.
527
+
528
+ def completed?
529
+ @step == @total_steps
530
+ end
531
+
532
+ ##
533
+ # Increment progress. Optionally pass _tokens_ which
534
+ # can be displayed in the output format.
535
+
536
+ def increment(tokens = {})
537
+ @step += 1
538
+ @tokens.merge! tokens if tokens.is_a? Hash
539
+ show
540
+ end
541
+
542
+ ##
543
+ # Erase previous terminal line.
544
+
545
+ def erase_line
546
+ # highline does not expose the output stream
547
+ $terminal.instance_variable_get('@output').print "\r\e[K"
548
+ end
549
+ end
550
+ end
551
+ end