brew/Library/Homebrew/commands.rb
apainintheneck 1fe16a5e35 Address feedback
- Move HOMEBREW_TAP_DIRECTORY to startup/config.rb because this file
holds more of the directory constants
- Rename `Commands.cmd_directories` to `Commands.tap_cmd_directories`
to better express that the commands come from taps

This file has the directory constants while the other one has regexes.
Just better organization.
2024-08-10 17:52:26 -07:00

266 lines
7.7 KiB
Ruby

# typed: true
# frozen_string_literal: true
# Helper functions for commands.
module Commands
HOMEBREW_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"cmd").freeze
HOMEBREW_DEV_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"dev-cmd").freeze
# If you are going to change anything in below hash,
# be sure to also update appropriate case statement in brew.sh
HOMEBREW_INTERNAL_COMMAND_ALIASES = {
"ls" => "list",
"homepage" => "home",
"-S" => "search",
"up" => "update",
"ln" => "link",
"instal" => "install", # gem does the same
"uninstal" => "uninstall",
"post_install" => "postinstall",
"rm" => "uninstall",
"remove" => "uninstall",
"abv" => "info",
"dr" => "doctor",
"--repo" => "--repository",
"environment" => "--env",
"--config" => "config",
"-v" => "--version",
"lc" => "livecheck",
"tc" => "typecheck",
}.freeze
# This pattern is used to split descriptions at full stops. We only consider a
# dot as a full stop if it is either followed by a whitespace or at the end of
# the description. In this way we can prevent cutting off a sentence in the
# middle due to dots in URLs or paths.
DESCRIPTION_SPLITTING_PATTERN = /\.(?>\s|$)/
def self.valid_internal_cmd?(cmd)
require?(HOMEBREW_CMD_PATH/cmd)
end
def self.valid_internal_dev_cmd?(cmd)
require?(HOMEBREW_DEV_CMD_PATH/cmd)
end
def self.method_name(cmd)
cmd.to_s
.tr("-", "_")
.downcase
.to_sym
end
def self.args_method_name(cmd_path)
cmd_path_basename = basename_without_extension(cmd_path)
cmd_method_prefix = method_name(cmd_path_basename)
:"#{cmd_method_prefix}_args"
end
def self.internal_cmd_path(cmd)
[
HOMEBREW_CMD_PATH/"#{cmd}.rb",
HOMEBREW_CMD_PATH/"#{cmd}.sh",
].find(&:exist?)
end
def self.internal_dev_cmd_path(cmd)
[
HOMEBREW_DEV_CMD_PATH/"#{cmd}.rb",
HOMEBREW_DEV_CMD_PATH/"#{cmd}.sh",
].find(&:exist?)
end
# Ruby commands which can be `require`d without being run.
def self.external_ruby_v2_cmd_path(cmd)
path = which("#{cmd}.rb", tap_cmd_directories)
path if require?(path)
end
# Ruby commands which are run by being `require`d.
def self.external_ruby_cmd_path(cmd)
which("brew-#{cmd}.rb", PATH.new(ENV.fetch("PATH")).append(tap_cmd_directories))
end
def self.external_cmd_path(cmd)
which("brew-#{cmd}", PATH.new(ENV.fetch("PATH")).append(tap_cmd_directories))
end
def self.path(cmd)
internal_cmd = HOMEBREW_INTERNAL_COMMAND_ALIASES.fetch(cmd, cmd)
path ||= internal_cmd_path(internal_cmd)
path ||= internal_dev_cmd_path(internal_cmd)
path ||= external_ruby_v2_cmd_path(cmd)
path ||= external_ruby_cmd_path(cmd)
path ||= external_cmd_path(cmd)
path
end
def self.commands(external: true, aliases: false)
cmds = internal_commands
cmds += internal_developer_commands
cmds += external_commands if external
cmds += internal_commands_aliases if aliases
cmds.sort
end
# An array of all tap cmd directory {Pathname}s.
sig { returns(T::Array[Pathname]) }
def self.tap_cmd_directories
Pathname.glob HOMEBREW_TAP_DIRECTORY/"*/*/cmd"
end
def self.internal_commands_paths
find_commands HOMEBREW_CMD_PATH
end
def self.internal_developer_commands_paths
find_commands HOMEBREW_DEV_CMD_PATH
end
def self.official_external_commands_paths(quiet:)
require "tap"
OFFICIAL_CMD_TAPS.flat_map do |tap_name, cmds|
tap = Tap.fetch(tap_name)
tap.install(quiet:) unless tap.installed?
cmds.map(&method(:external_ruby_v2_cmd_path)).compact
end
end
def self.internal_commands
find_internal_commands(HOMEBREW_CMD_PATH).map(&:to_s)
end
def self.internal_developer_commands
find_internal_commands(HOMEBREW_DEV_CMD_PATH).map(&:to_s)
end
def self.internal_commands_aliases
HOMEBREW_INTERNAL_COMMAND_ALIASES.keys
end
def self.find_internal_commands(path)
find_commands(path).map(&:basename)
.map { basename_without_extension(_1) }
.uniq
end
def self.external_commands
tap_cmd_directories.flat_map do |path|
find_commands(path).select(&:executable?)
.map { basename_without_extension(_1) }
.map { |p| p.to_s.delete_prefix("brew-").strip }
end.map(&:to_s)
.sort
end
def self.basename_without_extension(path)
path.basename(path.extname)
end
def self.find_commands(path)
Pathname.glob("#{path}/*")
.select(&:file?)
.sort
end
def self.rebuild_internal_commands_completion_list
require "completions"
cmds = internal_commands + internal_developer_commands + internal_commands_aliases
cmds.reject! { |cmd| Homebrew::Completions::COMPLETIONS_EXCLUSION_LIST.include? cmd }
file = HOMEBREW_REPOSITORY/"completions/internal_commands_list.txt"
file.atomic_write("#{cmds.sort.join("\n")}\n")
end
def self.rebuild_commands_completion_list
require "completions"
# Ensure that the cache exists so we can build the commands list
HOMEBREW_CACHE.mkpath
cmds = commands(aliases: true) - Homebrew::Completions::COMPLETIONS_EXCLUSION_LIST
all_commands_file = HOMEBREW_CACHE/"all_commands_list.txt"
external_commands_file = HOMEBREW_CACHE/"external_commands_list.txt"
all_commands_file.atomic_write("#{cmds.sort.join("\n")}\n")
external_commands_file.atomic_write("#{external_commands.sort.join("\n")}\n")
end
sig { params(command: String).returns(T.nilable(T::Array[[String, String]])) }
def self.command_options(command)
return if command == "help"
path = self.path(command)
return if path.blank?
if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path))
cmd_parser.processed_options.filter_map do |short, long, _, desc, hidden|
next if hidden
[long || short, desc]
end
else
options = []
comment_lines = path.read.lines.grep(/^#:/)
return options if comment_lines.empty?
# skip the comment's initial usage summary lines
comment_lines.slice(2..-1).each do |line|
match_data = / (?<option>-[-\w]+) +(?<desc>.*)$/.match(line)
options << [match_data[:option], match_data[:desc]] if match_data
end
options
end
end
def self.command_description(command, short: false)
path = self.path(command)
return if path.blank?
if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path))
if short
cmd_parser.description&.split(DESCRIPTION_SPLITTING_PATTERN)&.first
else
cmd_parser.description
end
else
comment_lines = path.read.lines.grep(/^#:/)
# skip the comment's initial usage summary lines
comment_lines.slice(2..-1)&.each do |line|
match_data = /^#: (?<desc>\w.*+)$/.match(line)
next unless match_data
desc = match_data[:desc]
return T.must(desc).split(DESCRIPTION_SPLITTING_PATTERN).first if short
return desc
end
end
end
def self.named_args_type(command)
path = self.path(command)
return if path.blank?
cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path)
return if cmd_parser.blank?
Array(cmd_parser.named_args_type)
end
# Returns the conflicts of a given `option` for `command`.
def self.option_conflicts(command, option)
path = self.path(command)
return if path.blank?
cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path)
return if cmd_parser.blank?
cmd_parser.conflicts.map do |set|
set.map! { |s| s.tr "_", "-" }
set - [option] if set.include? option
end.flatten.compact
end
end