brew/Library/Homebrew/commands.rb
Issy Long 45978435e7
rubocop: Use Sorbet/StrictSigil as it's better than comments
- Previously I thought that comments were fine to discourage people from
  wasting their time trying to bump things that used `undef` that Sorbet
  didn't support. But RuboCop is better at this since it'll complain if
  the comments are unnecessary.

- Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501.

- I've gone for a mixture of `rubocop:disable` for the files that can't
  be `typed: strict` (use of undef, required before everything else, etc)
  and `rubocop:todo` for everything else that should be tried to make
  strictly typed. There's no functional difference between the two as
  `rubocop:todo` is `rubocop:disable` with a different name.

- And I entirely disabled the cop for the docs/ directory since
  `typed: strict` isn't going to gain us anything for some Markdown
  linting config files.

- This means that now it's easier to track what needs to be done rather
  than relying on checklists of files in our big Sorbet issue:

```shell
$ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l
    268
```

- And this is confirmed working for new files:

```shell
$ git status
On branch use-rubocop-for-sorbet-strict-sigils
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Library/Homebrew/bad.rb
        Library/Homebrew/good.rb

nothing added to commit but untracked files present (use "git add" to track)

$ brew style
Offenses:

bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true.
^^^^^^^^^^^^^

1340 files inspected, 1 offense detected
```
2024-08-12 15:24:27 +01:00

266 lines
7.7 KiB
Ruby

# typed: true # rubocop:todo Sorbet/StrictSigil
# 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