require "optparse" require "shellwords" require "extend/optparse" require "hbc/cli/base" 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" require "hbc/cli/search" require "hbc/cli/style" require "hbc/cli/uninstall" require "hbc/cli/--version" require "hbc/cli/zap" require "hbc/cli/internal_use_base" require "hbc/cli/internal_audit_modified_casks" require "hbc/cli/internal_appcast_checkpoint" 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", # aliases from Homebrew that we don't (yet) support # 'ln' => 'link', # 'configure' => 'diy', # '--repo' => '--repository', # 'environment' => '--env', # '-c1' => '--config', }.freeze OPTIONS = { "--caskroom=" => :caskroom=, "--appdir=" => :appdir=, "--colorpickerdir=" => :colorpickerdir=, "--prefpanedir=" => :prefpanedir=, "--qlplugindir=" => :qlplugindir=, "--dictionarydir=" => :dictionarydir=, "--fontdir=" => :fontdir=, "--servicedir=" => :servicedir=, "--input_methoddir=" => :input_methoddir=, "--internet_plugindir=" => :internet_plugindir=, "--audio_unit_plugindir=" => :audio_unit_plugindir=, "--vst_plugindir=" => :vst_plugindir=, "--vst3_plugindir=" => :vst3_plugindir=, "--screen_saverdir=" => :screen_saverdir=, }.freeze FLAGS = { ["--[no-]binaries", :binaries] => true, ["--debug", :debug] => false, ["--verbose", :verbose] => false, ["--outdated", :outdated] => false, ["--help", :help] => false, }.freeze FLAGS.each do |(_, method), default_value| instance_variable_set(:"@#{method}", default_value) define_singleton_method(:"#{method}=") do |arg| instance_variable_set(:"@#{method}", arg) end define_singleton_method(:"#{method}?") do instance_variable_get(:"@#{method}") end end def self.command_classes @command_classes ||= constants.map(&method(:const_get)) .select { |sym| sym.respond_to?(:run) } .sort_by(&:command_name) end def self.commands @commands ||= command_classes.map(&:command_name) end def self.lookup_command(command_string) @lookup ||= Hash[commands.zip(command_classes)] command_string = ALIASES.fetch(command_string, command_string) @lookup.fetch(command_string, command_string) end def self.should_init?(command) (command.is_a? Class) && (command < CLI::Base) && command.needs_init? end 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").to_s # 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 end end def self.process(arguments) unless ENV["MACOS_VERSION"].nil? MacOS.full_version = ENV["MACOS_VERSION"] end command_string, *rest = *arguments rest = process_options(rest) command = help? ? "help" : lookup_command(command_string) Hbc.default_tap.install unless Hbc.default_tap.installed? Hbc.init if should_init?(command) run_command(command, *rest) rescue CaskError, CaskSha256MismatchError, ArgumentError => e msg = e.message msg << e.backtrace.join("\n") if debug? onoe msg exit 1 rescue StandardError, ScriptError, NoMemoryError => e msg = "#{e.message}\n" msg << Utils.error_message_with_suggestions msg << e.backtrace.join("\n") onoe msg exit 1 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}" 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 end def self.parser # If you modify these arguments, please update USAGE.md @parser ||= OptionParser.new do |opts| opts.on("--language STRING") do # handled in OS::Mac end OPTIONS.each do |option, method| opts.on("#{option}" "PATH", Pathname) do |path| Hbc.public_send(method, path) end end opts.on("--binarydir=PATH") do opoo <<-EOS.undent Option --binarydir is obsolete! Homebrew-Cask now uses the same location as your Homebrew installation for executable links. EOS end FLAGS.keys.each do |flag, method| opts.on(flag) do |bool| send(:"#{method}=", bool) end end opts.on("--version") do raise OptionParser::InvalidOption # override default handling of --version end end end def self.process_options(args) all_args = Shellwords.shellsplit(ENV["HOMEBREW_CASK_OPTS"] || "") + args remaining = [] until all_args.empty? begin head = all_args.shift remaining.concat(parser.parse([head])) rescue OptionParser::InvalidOption remaining << head retry rescue OptionParser::MissingArgument raise ArgumentError, "The option '#{head}' requires an argument." rescue OptionParser::AmbiguousOption raise ArgumentError, "There is more than one possible option that starts with '#{head}'." end end # for compat with Homebrew, not certain if this is desirable self.verbose = true if ARGV.verbose? self.debug = true if ARGV.debug? remaining 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}" end def purpose puts <<-EOS.undent brew-cask provides a friendly homebrew-style CLI workflow for the administration of macOS applications distributed as binaries. EOS end def usage max_command_len = CLI.commands.map(&:length).max 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") end def help "" end def _help_for(klass) klass.respond_to?(:help) ? klass.help : nil end end end end