module Heroku::Command

Constants

BaseWithApp

Public Class Methods

command_aliases() click to toggle source
# File lib/heroku/command.rb, line 23
def self.command_aliases
  @@command_aliases ||= {}
end
commands() click to toggle source
# File lib/heroku/command.rb, line 19
def self.commands
  @@commands ||= {}
end
current_args() click to toggle source
# File lib/heroku/command.rb, line 51
def self.current_args
  @current_args
end
current_command() click to toggle source
# File lib/heroku/command.rb, line 43
def self.current_command
  @current_command
end
current_command=(new_current_command) click to toggle source
# File lib/heroku/command.rb, line 47
def self.current_command=(new_current_command)
  @current_command = new_current_command
end
current_options() click to toggle source
# File lib/heroku/command.rb, line 55
def self.current_options
  @current_options ||= {}
end
display_warnings() click to toggle source
# File lib/heroku/command.rb, line 93
def self.display_warnings
  unless warnings.empty?
    $stderr.puts(warnings.map {|warning| " !    #{warning}"}.join("\n"))
  end
end
extract_error(body, options={}) { |: "Internal server error.\nRun `heroku status` to check for known platform issues."| ... } click to toggle source
# File lib/heroku/command.rb, line 265
def self.extract_error(body, options={})
  default_error = block_given? ? yield : "Internal server error.\nRun `heroku status` to check for known platform issues."
  parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error
end
files() click to toggle source
# File lib/heroku/command.rb, line 27
def self.files
  @@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
end
global_option(name, *args, &blk) click to toggle source
# File lib/heroku/command.rb, line 99
def self.global_option(name, *args, &blk)
  # args.sort.reverse gives -l, --long order
  global_options << { :name => name.to_s, :args => args.sort.reverse, :proc => blk }
end
global_options() click to toggle source
# File lib/heroku/command.rb, line 59
def self.global_options
  @global_options ||= []
end
invalid_arguments() click to toggle source
# File lib/heroku/command.rb, line 63
def self.invalid_arguments
  @invalid_arguments
end
load() click to toggle source
# File lib/heroku/command.rb, line 12
def self.load
  Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
    require file
  end
  Heroku::Plugin.load!
end
namespaces() click to toggle source
# File lib/heroku/command.rb, line 31
def self.namespaces
  @@namespaces ||= {}
end
parse(cmd) click to toggle source
# File lib/heroku/command.rb, line 261
def self.parse(cmd)
  commands[cmd] || commands[command_aliases[cmd]]
end
parse_error_json(body) click to toggle source
# File lib/heroku/command.rb, line 277
def self.parse_error_json(body)
  json = json_decode(body.to_s) rescue false
  case json
  when Array
    json.first.join(' ') # message like [['base', 'message']]
  when Hash
    json['error']   # message like {'error' => 'message'}
  else
    nil
  end
end
parse_error_plain(body) click to toggle source
# File lib/heroku/command.rb, line 289
def self.parse_error_plain(body)
  return unless body.respond_to?(:headers) && body.headers[:content_type].to_s.include?("text/plain")
  body.to_s
end
parse_error_xml(body) click to toggle source
# File lib/heroku/command.rb, line 270
def self.parse_error_xml(body)
  xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
  msg = xml_errors.map { |a| a.text }.join(" / ")
  return msg unless msg.empty?
rescue Exception
end
prepare_run(cmd, args=[]) click to toggle source
# File lib/heroku/command.rb, line 112
def self.prepare_run(cmd, args=[])
  command = parse(cmd)

  if args.include?('-h') || args.include?('--help')
    args.unshift(cmd) unless cmd =~ /^-.*/
    cmd = 'help'
    command = parse(cmd)
  end

  if cmd == '--version'
    cmd = 'version'
    command = parse(cmd)
  end

  @current_command = cmd
  @anonymized_args, @normalized_args = [], []

  opts = {}
  invalid_options = []

  parser = OptionParser.new do |parser|
    # remove OptionParsers Officious['version'] to avoid conflicts
    # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
    parser.base.long.delete('version')
    (global_options + (command && command[:options] || [])).each do |option|
      parser.on(*option[:args]) do |value|
        if option[:proc]
          option[:proc].call(value)
        end
        opts[option[:name].gsub('-', '_').to_sym] = value
        ARGV.join(' ') =~ /(#{option[:args].map {|arg| arg.split(' ', 2).first}.join('|')})/
        @anonymized_args << "#{$1} _"
        @normalized_args << "#{option[:args].last.split(' ', 2).first} _"
      end
    end
  end

  begin
    parser.order!(args) do |nonopt|
      invalid_options << nonopt
      @anonymized_args << '!'
      @normalized_args << '!'
    end
  rescue OptionParser::InvalidOption => ex
    invalid_options << ex.args.first
    @anonymized_args << '!'
    @normalized_args << '!'
    retry
  end

  args.concat(invalid_options)

  @current_args = args
  @current_options = opts
  @invalid_arguments = invalid_options

  @anonymous_command = [ARGV.first, *@anonymized_args].join(' ')
  begin
    usage_directory = "#{home_directory}/.heroku/usage"
    FileUtils.mkdir_p(usage_directory)
    usage_file = usage_directory << "/#{Heroku::VERSION}"
    usage = if File.exists?(usage_file)
      json_decode(File.read(usage_file))
    else
      {}
    end
    usage[@anonymous_command] ||= 0
    usage[@anonymous_command] += 1
    File.write(usage_file, json_encode(usage) + "\n")
  rescue
    # usage writing is not important, allow failures
  end

  if command
    command_instance = command[:klass].new(args.dup, opts.dup)

    if !@normalized_args.include?('--app _') && (implied_app = command_instance.app rescue nil)
      @normalized_args << '--app _'
    end
    @normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ')

    [ command_instance, command[:method] ]
  else
    error([
      "`#{cmd}` is not a heroku command.",
      suggestion(cmd, commands.keys + command_aliases.keys),
      "See `heroku help` for a list of available commands."
    ].compact.join("\n"))
  end
end
register_command(command) click to toggle source
# File lib/heroku/command.rb, line 35
def self.register_command(command)
  commands[command[:command]] = command
end
register_namespace(namespace) click to toggle source
# File lib/heroku/command.rb, line 39
def self.register_namespace(namespace)
  namespaces[namespace[:name]] = namespace
end
run(cmd, arguments=[]) click to toggle source
# File lib/heroku/command.rb, line 203
def self.run(cmd, arguments=[])
  begin
    object, method = prepare_run(cmd, arguments.dup)
    object.send(method)
  rescue Interrupt, StandardError, SystemExit => error
    # load likely error classes, as they may not be loaded yet due to defered loads
    require 'heroku-api'
    require 'rest_client'
    raise(error)
  end
rescue Heroku::API::Errors::Unauthorized, RestClient::Unauthorized
  puts "Authentication failure"
  unless ENV['HEROKU_API_KEY']
    run "login"
    retry
  end
rescue Heroku::API::Errors::VerificationRequired, RestClient::PaymentRequired => e
  retry if Heroku::Helpers.confirm_billing
rescue Heroku::API::Errors::NotFound => e
  error extract_error(e.response.body) {
    e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
  }
rescue RestClient::ResourceNotFound => e
  error extract_error(e.http_body) {
    e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
  }
rescue Heroku::API::Errors::Locked => e
  app = e.response.headers[:x_confirmation_required]
  if confirm_command(app, extract_error(e.response.body))
    arguments << '--confirm' << app
    retry
  end
rescue RestClient::Locked => e
  app = e.response.headers[:x_confirmation_required]
  if confirm_command(app, extract_error(e.http_body))
    arguments << '--confirm' << app
    retry
  end
rescue Heroku::API::Errors::Timeout, RestClient::RequestTimeout
  error "API request timed out. Please try again, or contact support@heroku.com if this issue persists."
rescue Heroku::API::Errors::ErrorWithResponse => e
  error extract_error(e.response.body)
rescue RestClient::RequestFailed => e
  error extract_error(e.http_body)
rescue CommandFailed => e
  error e.message
rescue OptionParser::ParseError
  commands[cmd] ? run("help", [cmd]) : run("help")
rescue Excon::Errors::SocketError => e
  if e.message == 'getaddrinfo: nodename nor servname provided, or not known (SocketError)'
    error("Unable to connect to Heroku API, please check internet connectivity and try again.")
  else
    raise(e)
  end
ensure
  display_warnings
end
shift_argument() click to toggle source
# File lib/heroku/command.rb, line 67
def self.shift_argument
  # dup argument to get a non-frozen string
  @invalid_arguments.shift.dup rescue nil
end
validate_arguments!() click to toggle source
# File lib/heroku/command.rb, line 72
def self.validate_arguments!
  unless invalid_arguments.empty?
    arguments = invalid_arguments.map {|arg| "\"#{arg}\""}
    if arguments.length == 1
      message = "Invalid argument: #{arguments.first}"
    elsif arguments.length > 1
      message = "Invalid arguments: "
      message << arguments[0...-1].join(", ")
      message << " and "
      message << arguments[-1]
    end
    $stderr.puts(format_with_bang(message))
    run(current_command, ["--help"])
    exit(1)
  end
end
warnings() click to toggle source
# File lib/heroku/command.rb, line 89
def self.warnings
  @warnings ||= []
end