Skip to content
Snippets Groups Projects
cli.rb 7.57 KiB
Newer Older
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "optparse"
require "shellwords"

require "extend/optparse"
Markus Reiter's avatar
Markus Reiter committed
require "hbc/cli/options"
require "hbc/cli/abstract_command"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "hbc/cli/audit"
require "hbc/cli/cat"
require "hbc/cli/cleanup"
require "hbc/cli/create"
require "hbc/cli/doctor"
require "hbc/cli/edit"
require "hbc/cli/fetch"
require "hbc/cli/home"
require "hbc/cli/info"
require "hbc/cli/install"
require "hbc/cli/list"
require "hbc/cli/outdated"
require "hbc/cli/reinstall"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "hbc/cli/search"
require "hbc/cli/style"
require "hbc/cli/uninstall"
require "hbc/cli/--version"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "hbc/cli/zap"

require "hbc/cli/abstract_internal_command"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "hbc/cli/internal_audit_modified_casks"
require "hbc/cli/internal_appcast_checkpoint"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "hbc/cli/internal_checkurl"
require "hbc/cli/internal_dump"
require "hbc/cli/internal_help"
require "hbc/cli/internal_stanza"

module Hbc
  class CLI
    ALIASES = {
      "ls"       => "list",
      "homepage" => "home",
      "-S"       => "search",    # verb starting with "-" is questionable
      "up"       => "update",
      "instal"   => "install",   # gem does the same
      "uninstal" => "uninstall",
      "rm"       => "uninstall",
      "remove"   => "uninstall",
      "abv"      => "info",
      "dr"       => "doctor",
    }.freeze
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

Markus Reiter's avatar
Markus Reiter committed
    include Options

    option "--appdir=PATH",               ->(value) { Hbc.appdir               = value }
    option "--colorpickerdir=PATH",       ->(value) { Hbc.colorpickerdir       = value }
    option "--prefpanedir=PATH",          ->(value) { Hbc.prefpanedir          = value }
    option "--qlplugindir=PATH",          ->(value) { Hbc.qlplugindir          = value }
    option "--dictionarydir=PATH",        ->(value) { Hbc.dictionarydir        = value }
    option "--fontdir=PATH",              ->(value) { Hbc.fontdir              = value }
    option "--servicedir=PATH",           ->(value) { Hbc.servicedir           = value }
    option "--input_methoddir=PATH",      ->(value) { Hbc.input_methoddir      = value }
    option "--internet_plugindir=PATH",   ->(value) { Hbc.internet_plugindir   = value }
    option "--audio_unit_plugindir=PATH", ->(value) { Hbc.audio_unit_plugindir = value }
    option "--vst_plugindir=PATH",        ->(value) { Hbc.vst_plugindir        = value }
    option "--vst3_plugindir=PATH",       ->(value) { Hbc.vst3_plugindir       = value }
    option "--screen_saverdir=PATH",      ->(value) { Hbc.screen_saverdir      = value }

    option "--help", :help, false

    # handled in OS::Mac
    option "--language a,b,c", ->(*) { raise OptionParser::InvalidOption }

    # override default handling of --version
    option "--version", ->(*) { raise OptionParser::InvalidOption }
    def self.command_classes
      @command_classes ||= constants.map(&method(:const_get))
                                    .select { |klass| klass.respond_to?(:run) }
                                    .reject(&:abstract?)
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

    def self.commands
      @commands ||= command_classes.map(&:command_name)
    end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

Markus Reiter's avatar
Markus Reiter committed
    def self.lookup_command(command_name)
      @lookup ||= Hash[commands.zip(command_classes)]
Markus Reiter's avatar
Markus Reiter committed
      command_name = ALIASES.fetch(command_name, command_name)
      @lookup.fetch(command_name, command_name)
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

    def self.should_init?(command)
      command.is_a?(Class) && !command.abstract? && command.needs_init?
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

    def self.run_command(command, *rest)
      if command.respond_to?(:run)
        # usual case: built-in command verb
        command.run(*rest)
      elsif require?(which("brewcask-#{command}.rb"))
        # external command as Ruby library on PATH, Homebrew-style
      elsif command.to_s.include?("/") && require?(command.to_s)
        # external command as Ruby library with literal path, useful
        # for development and troubleshooting
        sym = File.basename(command.to_s, ".rb").capitalize
        klass = begin
                  const_get(sym)
                rescue NameError
                  nil
                end
        if klass.respond_to?(:run)
          # invoke "run" on a Ruby library which follows our coding conventions
          # other Ruby libraries must do everything via "require"
          klass.run(*rest)
        end
      elsif which("brewcask-#{command}")
        # arbitrary external executable on PATH, Homebrew-style
        exec "brewcask-#{command}", *ARGV[1..-1]
      elsif Pathname.new(command.to_s).executable? &&
            command.to_s.include?("/") &&
            !command.to_s.match(/\.rb$/)
        # arbitrary external executable with literal path, useful
        # for development and troubleshooting
        exec command, *ARGV[1..-1]
      else
        # failure
        NullCommand.new(command).run
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end
    end

Markus Reiter's avatar
Markus Reiter committed
    def self.run(*args)
      new(*args).run
    end

    def initialize(*args)
      @args = process_options(*args)
    end

    def run
      command_name, *args = *@args
      command = help? ? "help" : self.class.lookup_command(command_name)

      MacOS.full_version = ENV["MACOS_VERSION"] unless ENV["MACOS_VERSION"].nil?
      Hbc.default_tap.install unless Hbc.default_tap.installed?
Markus Reiter's avatar
Markus Reiter committed
      Hbc.init if self.class.should_init?(command)
      self.class.run_command(command, *args)
    rescue CaskError, CaskSha256MismatchError, ArgumentError, OptionParser::InvalidOption => e
      msg = e.message
      msg << e.backtrace.join("\n") if ARGV.debug?
      onoe msg
      exit 1
    rescue StandardError, ScriptError, NoMemoryError => e
      msg << Utils.error_message_with_suggestions
      msg << e.backtrace.join("\n")
      onoe msg
      exit 1
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
    end

    def self.nice_listing(cask_list)
      cask_taps = {}
      cask_list.each do |c|
        user, repo, token = c.split "/"
        repo.sub!(/^homebrew-/i, "")
        cask_taps[token] ||= []
        cask_taps[token].push "#{user}/#{repo}"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end
      list = []
      cask_taps.each do |token, taps|
        if taps.length == 1
          list.push token
        else
          taps.each { |r| list.push [r, token].join "/" }
        end
      end
      list.sort
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
    end

Markus Reiter's avatar
Markus Reiter committed
    def process_options(*args)
      all_args = Shellwords.shellsplit(ENV["HOMEBREW_CASK_OPTS"] || "") + args
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

Markus Reiter's avatar
Markus Reiter committed
      non_options = []
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

Markus Reiter's avatar
Markus Reiter committed
      if idx = all_args.index("--")
        non_options += all_args.drop(idx)
        all_args = all_args.first(idx)
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end

Markus Reiter's avatar
Markus Reiter committed
      remaining = all_args.select do |arg|
Markus Reiter's avatar
Markus Reiter committed
          !process_arguments([arg]).empty?
        rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::AmbiguousOption
          true
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end

Markus Reiter's avatar
Markus Reiter committed
      remaining + non_options
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
    end

    class NullCommand
      def initialize(attempted_verb)
        @attempted_verb = attempted_verb
      end

      def run(*_args)
        purpose
        usage

        return if @attempted_verb.to_s.strip.empty?
        return if @attempted_verb == "help"

        raise ArgumentError, "Unknown command: #{@attempted_verb}"
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end

      def purpose
        puts <<-EOS.undent
          brew-cask provides a friendly homebrew-style CLI workflow for the
          administration of macOS applications distributed as binaries.
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def usage
        max_command_len = CLI.commands.map(&:length).max
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

        puts "Commands:\n\n"
        CLI.command_classes.each do |klass|
          next unless klass.visible
          puts "    #{klass.command_name.ljust(max_command_len)}  #{_help_for(klass)}"
        end
        puts %Q(\nSee also "man brew-cask")
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end

AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def _help_for(klass)
        klass.respond_to?(:help) ? klass.help : nil
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
    end
  end
end