brew/Library/Homebrew/commands.rb

266 lines
7.7 KiB
Ruby
Raw Normal View History

2023-03-02 18:21:53 -08:00
# typed: true
# frozen_string_literal: true
2020-08-14 03:12:03 +02:00
# Helper functions for commands.
module Commands
HOMEBREW_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"cmd").freeze
HOMEBREW_DEV_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"dev-cmd").freeze
2023-02-01 13:51:18 +01:00
# 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", 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(cmd_directories))
end
def self.external_cmd_path(cmd)
which("brew-#{cmd}", PATH.new(ENV.fetch("PATH")).append(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.cmd_directories
Pathname.glob HOMEBREW_TAP_DIRECTORY/"*/*/cmd"
end
def self.internal_commands_paths
2021-01-25 09:18:10 +00:00
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)
2024-03-07 16:20:20 +00:00
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
cmd_directories.flat_map do |path|
find_commands(path).select(&:executable?)
.map { basename_without_extension(_1) }
2020-07-20 13:18:09 -04:00
.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"
2020-08-19 17:12:32 +01:00
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
2021-01-25 09:18:10 +00:00
cmds = commands(aliases: true) - Homebrew::Completions::COMPLETIONS_EXCLUSION_LIST
2021-01-24 01:59:31 -05:00
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
2024-03-29 19:12:25 -07:00
sig { params(command: String).returns(T.nilable(T::Array[[String, String]])) }
def self.command_options(command)
2024-03-29 19:12:25 -07:00
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|
2021-06-08 22:02:32 -04:00
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|
2023-03-02 18:21:53 -08:00
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)
2021-01-24 01:59:31 -05:00
path = self.path(command)
return if path.blank?
if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path))
2021-01-24 16:42:28 -05:00
if short
2024-03-30 16:47:21 -07:00
cmd_parser.description&.split(DESCRIPTION_SPLITTING_PATTERN)&.first
2021-01-24 16:42:28 -05:00
else
cmd_parser.description
end
2021-01-24 01:59:31 -05:00
else
comment_lines = path.read.lines.grep(/^#:/)
# skip the comment's initial usage summary lines
comment_lines.slice(2..-1)&.each do |line|
2023-03-02 18:21:53 -08:00
match_data = /^#: (?<desc>\w.*+)$/.match(line)
next unless match_data
2021-01-24 16:42:28 -05:00
desc = match_data[:desc]
return T.must(desc).split(DESCRIPTION_SPLITTING_PATTERN).first if short
return desc
2021-01-24 01:59:31 -05:00
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