simple_commander 0.0.1

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.byebug_history +19 -0
  3. data/DEVELOPMENT +15 -0
  4. data/Gemfile +3 -0
  5. data/History.rdoc +3 -0
  6. data/LICENSE +22 -0
  7. data/Manifest +109 -0
  8. data/README.md +1 -0
  9. data/Rakefile +13 -0
  10. data/bin/simple_commander +16 -0
  11. data/dir_glob.rb +16 -0
  12. data/ember_c +66 -0
  13. data/ideal_spec.rb +23 -0
  14. data/lib/simple_commander.rb +35 -0
  15. data/lib/simple_commander/blank.rb +7 -0
  16. data/lib/simple_commander/command.rb +224 -0
  17. data/lib/simple_commander/configure.rb +14 -0
  18. data/lib/simple_commander/core_ext.rb +2 -0
  19. data/lib/simple_commander/core_ext/array.rb +24 -0
  20. data/lib/simple_commander/core_ext/object.rb +8 -0
  21. data/lib/simple_commander/delegates.rb +25 -0
  22. data/lib/simple_commander/help_formatters.rb +49 -0
  23. data/lib/simple_commander/help_formatters/base.rb +24 -0
  24. data/lib/simple_commander/help_formatters/terminal.rb +19 -0
  25. data/lib/simple_commander/help_formatters/terminal/command_help.erb +35 -0
  26. data/lib/simple_commander/help_formatters/terminal/help.erb +36 -0
  27. data/lib/simple_commander/help_formatters/terminal_compact.rb +11 -0
  28. data/lib/simple_commander/help_formatters/terminal_compact/command_help.erb +27 -0
  29. data/lib/simple_commander/help_formatters/terminal_compact/help.erb +29 -0
  30. data/lib/simple_commander/import.rb +5 -0
  31. data/lib/simple_commander/methods.rb +11 -0
  32. data/lib/simple_commander/platform.rb +7 -0
  33. data/lib/simple_commander/runner.rb +477 -0
  34. data/lib/simple_commander/user_interaction.rb +527 -0
  35. data/lib/simple_commander/version.rb +3 -0
  36. data/simple_commander.gemspec +22 -0
  37. data/todo.yml +24 -0
  38. metadata +137 -0
@@ -0,0 +1,527 @@
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 '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
+ # All special cases in HighLine::Question#convert, except those that implement #parse
328
+ (
329
+ [Float, Integer, String, Symbol, Regexp, Array, File, Pathname] +
330
+ # All Classes that respond to #parse
331
+ Object.constants.map do |const|
332
+ # Ignore constants that trigger deprecation warnings
333
+ Object.const_get(const) unless [:Config, :TimeoutError].include?(const)
334
+ end.select do |const|
335
+ const.class == Class && const.respond_to?(:parse)
336
+ end
337
+ ).each do |klass|
338
+ define_method "ask_for_#{klass.to_s.downcase}" do |prompt|
339
+ $terminal.ask(prompt, klass)
340
+ end
341
+ end
342
+ end
343
+
344
+ ##
345
+ # Substitute _hash_'s keys with their associated values in _str_.
346
+
347
+ def replace_tokens(str, hash) #:nodoc:
348
+ hash.inject(str) do |string, (key, value)|
349
+ string.gsub ":#{key}", value.to_s
350
+ end
351
+ end
352
+
353
+ ##
354
+ # = Progress Bar
355
+ #
356
+ # Terminal progress bar utility. In its most basic form
357
+ # requires that the developer specifies when the bar should
358
+ # be incremented. Note that a hash of tokens may be passed to
359
+ # #increment, (or returned when using Object#progress).
360
+ #
361
+ # uris = %w(
362
+ # http://vision-media.ca
363
+ # http://yahoo.com
364
+ # http://google.com
365
+ # )
366
+ #
367
+ # bar = Commander::UI::ProgressBar.new uris.length, options
368
+ # threads = []
369
+ # uris.each do |uri|
370
+ # threads << Thread.new do
371
+ # begin
372
+ # res = open uri
373
+ # bar.increment :uri => uri
374
+ # rescue Exception => e
375
+ # bar.increment :uri => "#{uri} failed"
376
+ # end
377
+ # end
378
+ # end
379
+ # threads.each { |t| t.join }
380
+ #
381
+ # The Object method #progress is also available:
382
+ #
383
+ # progress uris, :width => 10 do |uri|
384
+ # res = open uri
385
+ # { :uri => uri } # Can now use :uri within :format option
386
+ # end
387
+ #
388
+
389
+ class ProgressBar
390
+ ##
391
+ # Creates a new progress bar.
392
+ #
393
+ # === Options
394
+ #
395
+ # :title Title, defaults to "Progress"
396
+ # :width Width of :progress_bar
397
+ # :progress_str Progress string, defaults to "="
398
+ # :incomplete_str Incomplete bar string, defaults to '.'
399
+ # :format Defaults to ":title |:progress_bar| :percent_complete% complete "
400
+ # :tokens Additional tokens replaced within the format string
401
+ # :complete_message Defaults to "Process complete"
402
+ #
403
+ # === Tokens
404
+ #
405
+ # :title
406
+ # :percent_complete
407
+ # :progress_bar
408
+ # :step
409
+ # :steps_remaining
410
+ # :total_steps
411
+ # :time_elapsed
412
+ # :time_remaining
413
+ #
414
+
415
+ def initialize(total, options = {})
416
+ @total_steps, @step, @start_time = total, 0, Time.now
417
+ @title = options.fetch :title, 'Progress'
418
+ @width = options.fetch :width, 25
419
+ @progress_str = options.fetch :progress_str, '='
420
+ @incomplete_str = options.fetch :incomplete_str, '.'
421
+ @complete_message = options.fetch :complete_message, 'Process complete'
422
+ @format = options.fetch :format, ':title |:progress_bar| :percent_complete% complete '
423
+ @tokens = options.fetch :tokens, {}
424
+ end
425
+
426
+ ##
427
+ # Completion percentage.
428
+
429
+ def percent_complete
430
+ if @total_steps.zero?
431
+ 100
432
+ else
433
+ @step * 100 / @total_steps
434
+ end
435
+ end
436
+
437
+ ##
438
+ # Time that has elapsed since the operation started.
439
+
440
+ def time_elapsed
441
+ Time.now - @start_time
442
+ end
443
+
444
+ ##
445
+ # Estimated time remaining.
446
+
447
+ def time_remaining
448
+ (time_elapsed / @step) * steps_remaining
449
+ end
450
+
451
+ ##
452
+ # Number of steps left.
453
+
454
+ def steps_remaining
455
+ @total_steps - @step
456
+ end
457
+
458
+ ##
459
+ # Formatted progress bar.
460
+
461
+ def progress_bar
462
+ (@progress_str * (@width * percent_complete / 100)).ljust @width, @incomplete_str
463
+ end
464
+
465
+ ##
466
+ # Generates tokens for this step.
467
+
468
+ def generate_tokens
469
+ {
470
+ title: @title,
471
+ percent_complete: percent_complete,
472
+ progress_bar: progress_bar,
473
+ step: @step,
474
+ steps_remaining: steps_remaining,
475
+ total_steps: @total_steps,
476
+ time_elapsed: format('%0.2fs', time_elapsed),
477
+ time_remaining: @step > 0 ? format('%0.2fs', time_remaining) : '',
478
+ }.merge! @tokens
479
+ end
480
+
481
+ ##
482
+ # Output the progress bar.
483
+
484
+ def show
485
+ return if finished?
486
+ erase_line
487
+ if completed?
488
+ $terminal.say UI.replace_tokens(@complete_message, generate_tokens) if @complete_message.is_a? String
489
+ else
490
+ $terminal.say UI.replace_tokens(@format, generate_tokens) << ' '
491
+ end
492
+ end
493
+
494
+ ##
495
+ # Whether or not the operation is complete, and we have finished.
496
+
497
+ def finished?
498
+ @step == @total_steps + 1
499
+ end
500
+
501
+ ##
502
+ # Whether or not the operation has completed.
503
+
504
+ def completed?
505
+ @step == @total_steps
506
+ end
507
+
508
+ ##
509
+ # Increment progress. Optionally pass _tokens_ which
510
+ # can be displayed in the output format.
511
+
512
+ def increment(tokens = {})
513
+ @step += 1
514
+ @tokens.merge! tokens if tokens.is_a? Hash
515
+ show
516
+ end
517
+
518
+ ##
519
+ # Erase previous terminal line.
520
+
521
+ def erase_line
522
+ # highline does not expose the output stream
523
+ $terminal.instance_variable_get('@output').print "\r\e[K"
524
+ end
525
+ end
526
+ end
527
+ end