Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide interface and individual namespaces for brew CLI commands #16815

Merged
merged 16 commits into from
Mar 18, 2024
51 changes: 51 additions & 0 deletions Library/Homebrew/abstract_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# typed: strong
# frozen_string_literal: true

module Homebrew
# Subclass this to implement a `brew` command. This is preferred to declaring a named function in the `Homebrew`
# module, because:
# - Each Command lives in an isolated namespace.
# - Each Command implements a defined interface.
# - `args` is available as an ivar, and thus does not need to be passed as an argument to helper methods.
#
# To subclass, implement a `run` method and provide a `cmd_args` block to document the command and its allowed args.
# To generate method signatures for command args, run `brew typecheck --update`.
class AbstractCommand
extend T::Helpers

abstract!

class << self
sig { returns(T.nilable(CLI::Parser)) }
attr_reader :parser

sig { returns(String) }
def command_name = T.must(name).split("::").fetch(-1).downcase

# @return the AbstractCommand subclass associated with the brew CLI command name.
sig { params(name: String).returns(T.nilable(T.class_of(AbstractCommand))) }
def command(name) = subclasses.find { _1.command_name == name }

private

sig { params(block: T.proc.bind(CLI::Parser).void).void }
def cmd_args(&block)
@parser = T.let(CLI::Parser.new(&block), T.nilable(CLI::Parser))
end
end

sig { returns(CLI::Args) }
attr_reader :args

sig { params(argv: T::Array[String]).void }
def initialize(argv = ARGV.freeze)
parser = self.class.parser
raise "Commands must include a `cmd_args` block" if parser.nil?

@args = T.let(parser.parse(argv), CLI::Args)
end

sig { abstract.void }
def run; end
end
end
8 changes: 7 additions & 1 deletion Library/Homebrew/brew.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

ENV["PATH"] = path.to_s

require "abstract_command"
require "commands"
require "settings"

Expand All @@ -83,7 +84,12 @@
end

if internal_cmd || Commands.external_ruby_v2_cmd_path(cmd)
Homebrew.send Commands.method_name(cmd)
cmd_class = Homebrew::AbstractCommand.command(T.must(cmd))
if cmd_class
cmd_class.new.run
else
Homebrew.public_send Commands.method_name(cmd)
end
elsif (path = Commands.external_ruby_cmd_path(cmd))
require?(path)
exit Homebrew.failed? ? 1 : 0
Expand Down
19 changes: 19 additions & 0 deletions Library/Homebrew/cli/args.rbi
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# typed: strict

# This file contains global args as defined in `Homebrew::CLI::Parser.global_options`
# `Command`-specific args are defined in the commands themselves, with type signatures
# generated by the `Tapioca::Compilers::Args` compiler.

class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def debug?; end

sig { returns(T::Boolean) }
def help?; end

sig { returns(T::Boolean) }
def quiet?; end

sig { returns(T::Boolean) }
def verbose?; end
end
4 changes: 3 additions & 1 deletion Library/Homebrew/cli/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,14 @@ def conflicts(*options)
@conflicts << options.map { |option| option_to_name(option) }
end

def option_to_name(option)
def self.option_to_name(option)
option.sub(/\A--?(\[no-\])?/, "")
.tr("-", "_")
.delete("=")
end

def option_to_name(option) = self.class.option_to_name(option)

def name_to_option(name)
if name.length == 1
"-#{name}"
Expand Down