cli-topic 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,569 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ #"The MIT License (MIT)
5
+
6
+ # Copyright © Heroku 2008 - 2014
7
+
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to
13
+ # permit persons to whom the Software is furnished to do so, subject to
14
+ # the following conditions:
15
+
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
26
+ require 'fileutils'
27
+ module Clitopic
28
+ module Helpers
29
+
30
+ extend self
31
+
32
+ def home_directory
33
+ Dir.home
34
+ end
35
+
36
+ def display(msg="", new_line=true)
37
+ if new_line
38
+ puts(msg)
39
+ else
40
+ print(msg)
41
+ end
42
+ $stdout.flush
43
+ end
44
+
45
+ def redisplay(line, line_break = false)
46
+ display("\r\e[0K#{line}", line_break)
47
+ end
48
+
49
+ def deprecate(message)
50
+ display "WARNING: #{message}"
51
+ end
52
+
53
+ def debug(*args)
54
+ $stderr.puts(*args) if debugging?
55
+ end
56
+
57
+ def stderr_puts(*args)
58
+ $stderr.puts(*args)
59
+ end
60
+
61
+ def stderr_print(*args)
62
+ $stderr.print(*args)
63
+ end
64
+
65
+ def debugging?
66
+ ENV['CLITOPIC_DEBUG']
67
+ end
68
+
69
+ def confirm(message="Are you sure you wish to continue? (y/n)")
70
+ display("#{message} ", false)
71
+ ['y', 'yes'].include?(ask.downcase)
72
+ end
73
+
74
+ def confirm_command(app_to_confirm = app, message=nil)
75
+ if confirmed_app = Clitopic::Commands.current_options[:confirm]
76
+ unless confirmed_app == app_to_confirm
77
+ raise(Clitopic::Command::CommandFailed, "Confirmed app #{confirmed_app} did not match the selected app #{app_to_confirm}.")
78
+ end
79
+ return true
80
+ else
81
+ display
82
+ message ||= "WARNING: Destructive Action\nThis command will affect the app: #{app_to_confirm}"
83
+ message << "\nTo proceed, type \"#{app_to_confirm}\" or re-run this command with --confirm #{app_to_confirm}"
84
+ output_with_bang(message)
85
+ display
86
+ display "> ", false
87
+ if ask.downcase != app_to_confirm
88
+ error("Confirmation did not match #{app_to_confirm}. Aborted.")
89
+ else
90
+ true
91
+ end
92
+ end
93
+ end
94
+
95
+ def format_date(date)
96
+ date = Time.parse(date).utc if date.is_a?(String)
97
+ date.strftime("%Y-%m-%d %H:%M %Z").gsub('GMT', 'UTC')
98
+ end
99
+
100
+ def ask
101
+ $stdin.gets.to_s.strip
102
+ end
103
+
104
+ def shell(cmd)
105
+ FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
106
+ end
107
+
108
+ def run_command(command, args=[])
109
+ Clitopic::Command.run(command, args)
110
+ end
111
+
112
+ def retry_on_exception(*exceptions)
113
+ retry_count = 0
114
+ begin
115
+ yield
116
+ rescue *exceptions => ex
117
+ raise ex if retry_count >= 3
118
+ sleep 3
119
+ retry_count += 1
120
+ retry
121
+ end
122
+ end
123
+
124
+ def has_git?
125
+ %x{ git --version }
126
+ $?.success?
127
+ end
128
+
129
+ def git(args)
130
+ return "" unless has_git?
131
+ flattened_args = [args].flatten.compact.join(" ")
132
+ %x{ git #{flattened_args} 2>&1 }.strip
133
+ end
134
+
135
+ def time_ago(since)
136
+ if since.is_a?(String)
137
+ since = Time.parse(since)
138
+ end
139
+
140
+ elapsed = Time.now - since
141
+
142
+ message = since.strftime("%Y/%m/%d %H:%M:%S")
143
+ if elapsed <= 60
144
+ message << " (~ #{elapsed.floor}s ago)"
145
+ elsif elapsed <= (60 * 60)
146
+ message << " (~ #{(elapsed / 60).floor}m ago)"
147
+ elsif elapsed <= (60 * 60 * 25)
148
+ message << " (~ #{(elapsed / 60 / 60).floor}h ago)"
149
+ end
150
+ message
151
+ end
152
+
153
+ def time_remaining(from, to)
154
+ secs = (to - from).to_i
155
+ mins = secs / 60
156
+ hours = mins / 60
157
+ return "#{hours}h #{mins % 60}m" if hours > 0
158
+ return "#{mins}m #{secs % 60}s" if mins > 0
159
+ return "#{secs}s" if secs >= 0
160
+ end
161
+
162
+ def truncate(text, length)
163
+ return "" if text.nil?
164
+ if text.size > length
165
+ text[0, length - 2] + '..'
166
+ else
167
+ text
168
+ end
169
+ end
170
+
171
+ @@kb = 1024
172
+ @@mb = 1024 * @@kb
173
+ @@gb = 1024 * @@mb
174
+ def format_bytes(amount)
175
+ amount = amount.to_i
176
+ return '(empty)' if amount == 0
177
+ return amount if amount < @@kb
178
+ return "#{(amount / @@kb).round}k" if amount < @@mb
179
+ return "#{(amount / @@mb).round}M" if amount < @@gb
180
+ return "#{(amount / @@gb).round}G"
181
+ end
182
+
183
+ def quantify(string, num)
184
+ "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ]
185
+ end
186
+
187
+ def has_git_remote?(remote)
188
+ git('remote').split("\n").include?(remote) && $?.success?
189
+ end
190
+
191
+ def create_git_remote(remote, url)
192
+ return if has_git_remote? remote
193
+ git "remote add #{remote} #{url}"
194
+ display "Git remote #{remote} added" if $?.success?
195
+ end
196
+
197
+ def longest(items)
198
+ items.map { |i| i.to_s.length }.sort.last
199
+ end
200
+
201
+ def display_table(objects, columns, headers)
202
+ lengths = []
203
+ columns.each_with_index do |column, index|
204
+ header = headers[index]
205
+ lengths << longest([header].concat(objects.map { |o| o[column].to_s }))
206
+ end
207
+ lines = lengths.map {|length| "-" * length}
208
+ lengths[-1] = 0 # remove padding from last column
209
+ display_row headers, lengths
210
+ display_row lines, lengths
211
+ objects.each do |row|
212
+ display_row columns.map { |column| row[column] }, lengths
213
+ end
214
+ end
215
+
216
+ def display_row(row, lengths)
217
+ row_data = []
218
+ row.zip(lengths).each do |column, length|
219
+ format = column.is_a?(Fixnum) ? "%#{length}s" : "%-#{length}s"
220
+ row_data << format % column
221
+ end
222
+ display(row_data.join(" "))
223
+ end
224
+
225
+ def json_encode(object)
226
+ JSON.generate(object)
227
+ end
228
+
229
+ def json_decode(json)
230
+ JSON.parse(json)
231
+ rescue JSON::ParserError
232
+ nil
233
+ end
234
+
235
+ def set_buffer(enable)
236
+ with_tty do
237
+ if enable
238
+ `stty icanon echo`
239
+ else
240
+ `stty -icanon -echo`
241
+ end
242
+ end
243
+ end
244
+
245
+ def with_tty(&block)
246
+ return unless $stdin.isatty
247
+ begin
248
+ yield
249
+ rescue
250
+ # fails on windows
251
+ end
252
+ end
253
+
254
+ def get_terminal_environment
255
+ { "TERM" => ENV["TERM"], "COLUMNS" => `tput cols`.strip, "LINES" => `tput lines`.strip }
256
+ rescue
257
+ { "TERM" => ENV["TERM"] }
258
+ end
259
+
260
+ def fail(message)
261
+ raise Clitopic::Command::CommandFailed, message
262
+ end
263
+
264
+ ## DISPLAY HELPERS
265
+
266
+ def action(message, options={})
267
+ display("#{in_message(message, options)}... ", false)
268
+ Clitopic::Helpers.error_with_failure = true
269
+ ret = yield
270
+ Clitopic::Helpers.error_with_failure = false
271
+ display((options[:success] || "done"), false)
272
+ if @status
273
+ display(", #{@status}", false)
274
+ @status = nil
275
+ end
276
+ display
277
+ ret
278
+ end
279
+
280
+ def in_message(message, options={})
281
+ message = "#{message} in space #{options[:space]}" if options[:space]
282
+ message = "#{message} in organization #{org}" if options[:org]
283
+ message
284
+ end
285
+
286
+ def status(message)
287
+ @status = message
288
+ end
289
+
290
+ def format_with_bang(message)
291
+ return '' if message.to_s.strip == ""
292
+ " ! " + message.split("\n").join("\n ! ")
293
+ end
294
+
295
+ def output_with_bang(message="", new_line=true)
296
+ return if message.to_s.strip == ""
297
+ display(format_with_bang(message), new_line)
298
+ end
299
+
300
+ def error(message, report=false)
301
+ if Clitopic::Helpers.error_with_failure
302
+ display("failed")
303
+ Clitopic::Helpers.error_with_failure = false
304
+ end
305
+ $stderr.puts(format_with_bang(message))
306
+ exit(1)
307
+ end
308
+
309
+ def self.error_with_failure
310
+ @@error_with_failure ||= false
311
+ end
312
+
313
+ def self.error_with_failure=(new_error_with_failure)
314
+ @@error_with_failure = new_error_with_failure
315
+ end
316
+
317
+ def self.included_into
318
+ @@included_into ||= []
319
+ end
320
+
321
+ def self.extended_into
322
+ @@extended_into ||= []
323
+ end
324
+
325
+ def self.included(base)
326
+ included_into << base
327
+ end
328
+
329
+ def self.extended(base)
330
+ extended_into << base
331
+ end
332
+
333
+ def display_header(message="", new_line=true)
334
+ return if message.to_s.strip == ""
335
+ display("=== " + message.to_s.split("\n").join("\n=== "), new_line)
336
+ end
337
+
338
+ def display_object(object)
339
+ case object
340
+ when Array
341
+ # list of objects
342
+ object.each do |item|
343
+ display_object(item)
344
+ end
345
+ when Hash
346
+ # if all values are arrays, it is a list with headers
347
+ # otherwise it is a single header with pairs of data
348
+ if object.values.all? {|value| value.is_a?(Array)}
349
+ object.keys.sort_by {|key| key.to_s}.each do |key|
350
+ display_header(key)
351
+ display_object(object[key])
352
+ hputs
353
+ end
354
+ end
355
+ else
356
+ hputs(object.to_s)
357
+ end
358
+ end
359
+
360
+ def hputs(string='')
361
+ Kernel.puts(string)
362
+ end
363
+
364
+ def hprint(string='')
365
+ Kernel.print(string)
366
+ $stdout.flush
367
+ end
368
+
369
+ def spinner(ticks)
370
+ %w(/ - \\ |)[ticks % 4]
371
+ end
372
+
373
+ def launchy(message, url)
374
+ action(message) do
375
+ require("launchy")
376
+ launchy = Launchy.open(url)
377
+ if launchy.respond_to?(:join)
378
+ launchy.join
379
+ end
380
+ end
381
+ end
382
+
383
+ # produces a printf formatter line for an array of items
384
+ # if an individual line item is an array, it will create columns
385
+ # that are lined-up
386
+ #
387
+ # line_formatter(["foo", "barbaz"]) # => "%-6s"
388
+ # line_formatter(["foo", "barbaz"], ["bar", "qux"]) # => "%-3s %-6s"
389
+ #
390
+ def line_formatter(array)
391
+ if array.any? {|item| item.is_a?(Array)}
392
+ cols = []
393
+ array.each do |item|
394
+ if item.is_a?(Array)
395
+ item.each_with_index { |val,idx| cols[idx] = [cols[idx]||0, (val || '').length].max }
396
+ end
397
+ end
398
+ cols.map { |col| "%-#{col}s" }.join(" ")
399
+ else
400
+ "%s"
401
+ end
402
+ end
403
+
404
+ def styled_array(array, options={})
405
+ fmt = line_formatter(array)
406
+ array = array.sort unless options[:sort] == false
407
+ array.each do |element|
408
+ display((fmt % element).rstrip)
409
+ end
410
+ display
411
+ end
412
+
413
+ def format_error(error, message='Clitopic client internal error.')
414
+ formatted_error = []
415
+ formatted_error << " ! #{message}"
416
+ formatted_error << ' ! Search for help at: https://help.clitopic.com"'
417
+ formatted_error << ' ! Or report a bug at: https://github.com/ant31/clitopic/issues/new'
418
+ formatted_error << ''
419
+
420
+ command = ARGV.map do |arg|
421
+ if arg.include?(' ')
422
+ arg = %{"#{arg}"}
423
+ else
424
+ arg
425
+ end
426
+ end.join(' ')
427
+ formatted_error << " Command: #{command}"
428
+
429
+ if http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
430
+ formatted_error << " HTTP Proxy: #{http_proxy}"
431
+ end
432
+ if https_proxy = ENV['https_proxy'] || ENV['HTTPS_PROXY']
433
+ formatted_error << " HTTPS Proxy: #{https_proxy}"
434
+ end
435
+ formatted_error << " Version: #{Clitopic.version}"
436
+ formatted_error << " Error: #{error.message} (#{error.class})"
437
+ formatted_error << " Backtrace:\n" + "#{error.backtrace.join("\n")}".indent(17) unless error.backtrace.nil?
438
+ formatted_error << "\n"
439
+ formatted_error << " More information in #{error_log_path}"
440
+ formatted_error << "\n"
441
+ formatted_error.join("\n")
442
+ end
443
+
444
+ def styled_error(error, message='Clitopic client internal error.')
445
+ if Clitopic::Helpers.error_with_failure
446
+ display("failed")
447
+ Clitopic::Helpers.error_with_failure = false
448
+ end
449
+ error_log(message, error.message, error.backtrace.join("\n"))
450
+ $stderr.puts(format_error(error, message))
451
+ rescue => e
452
+ $stderr.puts e, e.backtrace, error, error.backtrace
453
+ end
454
+
455
+ def error_log(*obj)
456
+ FileUtils.mkdir_p(File.dirname(error_log_path))
457
+ File.open(error_log_path, 'a') do |file|
458
+ file.write(obj.join("\n") + "\n")
459
+ end
460
+ end
461
+
462
+ def error_log_path
463
+ File.join(home_directory, '.clitopic', 'error.log')
464
+ end
465
+
466
+ def styled_header(header)
467
+ display("=== #{header}")
468
+ end
469
+
470
+ def styled_hash(hash, keys=nil)
471
+ max_key_length = hash.keys.map {|key| key.to_s.length}.max + 2
472
+ keys ||= hash.keys.sort {|x,y| x.to_s <=> y.to_s}
473
+ keys.each do |key|
474
+ case value = hash[key]
475
+ when Array
476
+ if value.empty?
477
+ next
478
+ else
479
+ elements = value.sort {|x,y| x.to_s <=> y.to_s}
480
+ display("#{key}: ".ljust(max_key_length), false)
481
+ display(elements[0])
482
+ elements[1..-1].each do |element|
483
+ display("#{' ' * max_key_length}#{element}")
484
+ end
485
+ if elements.length > 1
486
+ display
487
+ end
488
+ end
489
+ when nil
490
+ next
491
+ else
492
+ display("#{key}: ".ljust(max_key_length), false)
493
+ display(value)
494
+ end
495
+ end
496
+ end
497
+
498
+ def string_distance(first, last)
499
+ distances = [] # 0x0s
500
+ 0.upto(first.length) do |index|
501
+ distances << [index] + [0] * last.length
502
+ end
503
+ distances[0] = 0.upto(last.length).to_a
504
+ 1.upto(last.length) do |last_index|
505
+ 1.upto(first.length) do |first_index|
506
+ first_char = first[first_index - 1, 1]
507
+ last_char = last[last_index - 1, 1]
508
+ if first_char == last_char
509
+ distances[first_index][last_index] = distances[first_index - 1][last_index - 1] # noop
510
+ else
511
+ distances[first_index][last_index] = [
512
+ distances[first_index - 1][last_index], # deletion
513
+ distances[first_index][last_index - 1], # insertion
514
+ distances[first_index - 1][last_index - 1] # substitution
515
+ ].min + 1 # cost
516
+ if first_index > 1 && last_index > 1
517
+ first_previous_char = first[first_index - 2, 1]
518
+ last_previous_char = last[last_index - 2, 1]
519
+ if first_char == last_previous_char && first_previous_char == last_char
520
+ distances[first_index][last_index] = [
521
+ distances[first_index][last_index],
522
+ distances[first_index - 2][last_index - 2] + 1 # transposition
523
+ ].min
524
+ end
525
+ end
526
+ end
527
+ end
528
+ end
529
+ distances[first.length][last.length]
530
+ end
531
+
532
+ def display_suggestion(actual, possibilities, allowed_distance=4)
533
+ suggestions = suggestion(actual, possibilities, allowed_distance)
534
+ if suggestions.length == 1
535
+ "Perhaps you meant:\n" + suggestions.first.indent(20)
536
+ elsif suggestions.length > 1
537
+ "Perhaps you meant:\n" + "#{suggestions.map {|suggestion| "- #{suggestion}"}.join("\n").indent(2)}."
538
+ else
539
+ nil
540
+ end
541
+ end
542
+
543
+ def suggestion(actual, possibilities, allowed_distance=4)
544
+ distances = Hash.new {|hash,key| hash[key] = []}
545
+ begin_with = []
546
+ possibilities.each do |suggestion|
547
+ if suggestion.start_with?(actual)
548
+ distances[0] << suggestion
549
+ else
550
+ distances[string_distance(actual, suggestion)] << suggestion
551
+ end
552
+ end
553
+
554
+ minimum_distance = distances.keys.min
555
+
556
+ if minimum_distance < allowed_distance
557
+ suggestions = distances[minimum_distance].sort
558
+ return suggestions
559
+ else
560
+ []
561
+ end
562
+ end
563
+
564
+ # cheap deep clone
565
+ def deep_clone(obj)
566
+ json_decode(json_encode(obj))
567
+ end
568
+ end
569
+ end
@@ -0,0 +1,15 @@
1
+ module Clitopic
2
+ module Parser
3
+ module Dummy
4
+
5
+ def help
6
+ puts parse
7
+ end
8
+
9
+ def parse(args)
10
+ puts "i'm parsing #{args}"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ require 'optparse'
2
+ require 'clitopic/commands'
3
+
4
+ module Clitopic
5
+ module Parser
6
+ module OptParser
7
+ def process_options(parser, opts)
8
+ opts.each do |option|
9
+ parser.on(*option[:args]) do |value|
10
+ if option[:proc]
11
+ option[:proc].call(value)
12
+ end
13
+ name = option[:name]
14
+ if options.has_key?(name) && options[name].is_a?(Array)
15
+ options[name] += value
16
+ else
17
+ puts "Warning: already defined option: --#{option[:name]} #{options[name]}" if options.has_key?(name) && option[:default] == nil
18
+ options[name] = value
19
+ end
20
+ end
21
+ end
22
+ options
23
+ end
24
+
25
+ def parser
26
+ @opt_parser = OptionParser.new do |parser|
27
+ # remove OptionParsers Officious['version'] to avoid conflicts
28
+ # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
29
+ parser.banner = self.banner unless self.banner.nil?
30
+ parser.base.long.delete('version')
31
+ process_options(parser, self.cmd_options)
32
+
33
+ if !self.topic.nil? && self.topic.topic_options.size > 0
34
+ parser.separator ""
35
+ parser.separator "Topic options:"
36
+ process_options(parser, self.topic.topic_options)
37
+ end
38
+
39
+ parser.separator ""
40
+ parser.separator "Common options:"
41
+ process_options(parser, Clitopic::Commands.global_options)
42
+
43
+ # No argument, shows at tail. This will print an options summary.
44
+ # Try it and see!
45
+ parser.on_tail("-h", "--help", "Show this message") do
46
+ puts parser
47
+ exit 0
48
+ end
49
+ end
50
+ return @opt_parser
51
+ end
52
+
53
+ def help
54
+ parser.to_s
55
+ end
56
+
57
+ def parse(args)
58
+ @invalid_options ||= []
59
+ parser.order!(args)
60
+ @arguments = args
61
+ Clitopic::Commands.validate_arguments!(@invalid_options)
62
+ return @options, @arguments
63
+ rescue OptionParser::InvalidOption => ex
64
+ @invalid_options << ex.args.first
65
+ retry
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), "parser", "*.rb")].each do |file|
2
+ require file
3
+ end