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.
- checksums.yaml +7 -0
- data/.byebug_history +19 -0
- data/DEVELOPMENT +15 -0
- data/Gemfile +3 -0
- data/History.rdoc +3 -0
- data/LICENSE +22 -0
- data/Manifest +109 -0
- data/README.md +1 -0
- data/Rakefile +13 -0
- data/bin/simple_commander +16 -0
- data/dir_glob.rb +16 -0
- data/ember_c +66 -0
- data/ideal_spec.rb +23 -0
- data/lib/simple_commander.rb +35 -0
- data/lib/simple_commander/blank.rb +7 -0
- data/lib/simple_commander/command.rb +224 -0
- data/lib/simple_commander/configure.rb +14 -0
- data/lib/simple_commander/core_ext.rb +2 -0
- data/lib/simple_commander/core_ext/array.rb +24 -0
- data/lib/simple_commander/core_ext/object.rb +8 -0
- data/lib/simple_commander/delegates.rb +25 -0
- data/lib/simple_commander/help_formatters.rb +49 -0
- data/lib/simple_commander/help_formatters/base.rb +24 -0
- data/lib/simple_commander/help_formatters/terminal.rb +19 -0
- data/lib/simple_commander/help_formatters/terminal/command_help.erb +35 -0
- data/lib/simple_commander/help_formatters/terminal/help.erb +36 -0
- data/lib/simple_commander/help_formatters/terminal_compact.rb +11 -0
- data/lib/simple_commander/help_formatters/terminal_compact/command_help.erb +27 -0
- data/lib/simple_commander/help_formatters/terminal_compact/help.erb +29 -0
- data/lib/simple_commander/import.rb +5 -0
- data/lib/simple_commander/methods.rb +11 -0
- data/lib/simple_commander/platform.rb +7 -0
- data/lib/simple_commander/runner.rb +477 -0
- data/lib/simple_commander/user_interaction.rb +527 -0
- data/lib/simple_commander/version.rb +3 -0
- data/simple_commander.gemspec +22 -0
- data/todo.yml +24 -0
- 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
|