Merge pull request #16815 from dduugg/abstract-command

Provide interface and individual namespaces for brew CLI commands
This commit is contained in:
Douglas Eichelberger 2024-03-18 08:11:52 -07:00 committed by GitHub
commit 2cc3ce9bb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 514 additions and 333 deletions

View File

@ -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

View File

@ -59,6 +59,7 @@ begin
ENV["PATH"] = path.to_s
require "abstract_command"
require "commands"
require "settings"
@ -83,7 +84,12 @@ begin
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

View File

@ -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

View File

@ -248,12 +248,14 @@ module Homebrew
@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}"

View File

@ -1,6 +1,7 @@
# typed: true
# frozen_string_literal: true
require "abstract_command"
require "metafiles"
require "formula"
require "cli/parser"
@ -8,226 +9,234 @@ require "cask/list"
require "system_command"
module Homebrew
extend SystemCommand::Mixin
module Cmd
class List < AbstractCommand
include SystemCommand::Mixin
sig { returns(CLI::Parser) }
def self.list_args
Homebrew::CLI::Parser.new do
description <<~EOS
List all installed formulae and casks.
If <formula> is provided, summarise the paths within its current keg.
If <cask> is provided, list its artifacts.
EOS
switch "--formula", "--formulae",
description: "List only formulae, or treat all named arguments as formulae."
switch "--cask", "--casks",
description: "List only casks, or treat all named arguments as casks."
switch "--full-name",
description: "Print formulae with fully-qualified names. Unless `--full-name`, `--versions` " \
"or `--pinned` are passed, other options (i.e. `-1`, `-l`, `-r` and `-t`) are " \
"passed to `ls`(1) which produces the actual output."
switch "--versions",
description: "Show the version number for installed formulae, or only the specified " \
"formulae if <formula> are provided."
switch "--multiple",
depends_on: "--versions",
description: "Only show formulae with multiple versions installed."
switch "--pinned",
description: "List only pinned formulae, or only the specified (pinned) " \
"formulae if <formula> are provided. See also `pin`, `unpin`."
# passed through to ls
switch "-1",
description: "Force output to be one entry per line. " \
"This is the default when output is not to a terminal."
switch "-l",
description: "List formulae and/or casks in long format. " \
"Has no effect when a formula or cask name is passed as an argument."
switch "-r",
description: "Reverse the order of the formulae and/or casks sort to list the oldest entries first. " \
"Has no effect when a formula or cask name is passed as an argument."
switch "-t",
description: "Sort formulae and/or casks by time modified, listing most recently modified first. " \
"Has no effect when a formula or cask name is passed as an argument."
cmd_args do
description <<~EOS
List all installed formulae and casks.
If <formula> is provided, summarise the paths within its current keg.
If <cask> is provided, list its artifacts.
EOS
switch "--formula", "--formulae",
description: "List only formulae, or treat all named arguments as formulae."
switch "--cask", "--casks",
description: "List only casks, or treat all named arguments as casks."
switch "--full-name",
description: "Print formulae with fully-qualified names. Unless `--full-name`, `--versions` " \
"or `--pinned` are passed, other options (i.e. `-1`, `-l`, `-r` and `-t`) are " \
"passed to `ls`(1) which produces the actual output."
switch "--versions",
description: "Show the version number for installed formulae, or only the specified " \
"formulae if <formula> are provided."
switch "--multiple",
depends_on: "--versions",
description: "Only show formulae with multiple versions installed."
switch "--pinned",
description: "List only pinned formulae, or only the specified (pinned) " \
"formulae if <formula> are provided. See also `pin`, `unpin`."
# passed through to ls
switch "-1",
description: "Force output to be one entry per line. " \
"This is the default when output is not to a terminal."
switch "-l",
description: "List formulae and/or casks in long format. " \
"Has no effect when a formula or cask name is passed as an argument."
switch "-r",
description: "Reverse the order of the formulae and/or casks sort to list the oldest entries first. " \
"Has no effect when a formula or cask name is passed as an argument."
switch "-t",
description: "Sort formulae and/or casks by time modified, listing most recently modified first. " \
"Has no effect when a formula or cask name is passed as an argument."
conflicts "--formula", "--cask"
conflicts "--pinned", "--cask"
conflicts "--multiple", "--cask"
conflicts "--pinned", "--multiple"
["-1", "-l", "-r", "-t"].each do |flag|
conflicts "--versions", flag
conflicts "--pinned", flag
end
["--versions", "--pinned", "-l", "-r", "-t"].each do |flag|
conflicts "--full-name", flag
conflicts "--formula", "--cask"
conflicts "--pinned", "--cask"
conflicts "--multiple", "--cask"
conflicts "--pinned", "--multiple"
["-1", "-l", "-r", "-t"].each do |flag|
conflicts "--versions", flag
conflicts "--pinned", flag
end
["--versions", "--pinned", "-l", "-r", "-t"].each do |flag|
conflicts "--full-name", flag
end
named_args [:installed_formula, :installed_cask]
end
named_args [:installed_formula, :installed_cask]
end
end
sig { override.void }
def run
if args.full_name?
unless args.cask?
formula_names = args.no_named? ? Formula.installed : args.named.to_resolved_formulae
full_formula_names = formula_names.map(&:full_name).sort(&tap_and_name_comparison)
full_formula_names = Formatter.columns(full_formula_names) unless args.public_send(:"1?")
puts full_formula_names if full_formula_names.present?
end
if args.cask? || (!args.formula? && args.no_named?)
cask_names = if args.no_named?
Cask::Caskroom.casks
else
args.named.to_formulae_and_casks(only: :cask, method: :resolve)
end
# The cast is because `Keg`` does not define `full_name`
full_cask_names = T.cast(cask_names, T::Array[T.any(Formula, Cask::Cask)])
.map(&:full_name).sort(&tap_and_name_comparison)
full_cask_names = Formatter.columns(full_cask_names) unless args.public_send(:"1?")
puts full_cask_names if full_cask_names.present?
end
elsif args.pinned?
filtered_list
elsif args.versions?
filtered_list unless args.cask?
list_casks if args.cask? || (!args.formula? && !args.multiple? && args.no_named?)
elsif args.no_named?
ENV["CLICOLOR"] = nil
def self.list
args = list_args.parse
ls_args = []
ls_args << "-1" if args.public_send(:"1?")
ls_args << "-l" if args.l?
ls_args << "-r" if args.r?
ls_args << "-t" if args.t?
if args.full_name?
unless args.cask?
formula_names = args.no_named? ? Formula.installed : args.named.to_resolved_formulae
full_formula_names = formula_names.map(&:full_name).sort(&tap_and_name_comparison)
full_formula_names = Formatter.columns(full_formula_names) unless args.public_send(:"1?")
puts full_formula_names if full_formula_names.present?
if !args.cask? && HOMEBREW_CELLAR.exist? && HOMEBREW_CELLAR.children.any?
ohai "Formulae" if $stdout.tty? && !args.formula?
safe_system "ls", *ls_args, HOMEBREW_CELLAR
puts if $stdout.tty? && !args.formula?
end
if !args.formula? && Cask::Caskroom.any_casks_installed?
ohai "Casks" if $stdout.tty? && !args.cask?
safe_system "ls", *ls_args, Cask::Caskroom.path
end
else
kegs, casks = args.named.to_kegs_to_casks
if args.verbose? || !$stdout.tty?
find_args = %w[-not -type d -not -name .DS_Store -print]
system_command! "find", args: kegs.map(&:to_s) + find_args, print_stdout: true if kegs.present?
system_command! "find", args: casks.map(&:caskroom_path) + find_args, print_stdout: true if casks.present?
else
kegs.each { |keg| PrettyListing.new keg } if kegs.present?
list_casks if casks.present?
end
end
end
if args.cask? || (!args.formula? && args.no_named?)
cask_names = if args.no_named?
private
def filtered_list
names = if args.no_named?
Formula.racks
else
racks = args.named.map { |n| Formulary.to_rack(n) }
racks.select do |rack|
Homebrew.failed = true unless rack.exist?
rack.exist?
end
end
if args.pinned?
pinned_versions = {}
names.sort.each do |d|
keg_pin = (HOMEBREW_PINNED_KEGS/d.basename.to_s)
pinned_versions[d] = keg_pin.readlink.basename.to_s if keg_pin.exist? || keg_pin.symlink?
end
pinned_versions.each do |d, version|
puts d.basename.to_s.concat(args.versions? ? " #{version}" : "")
end
else # --versions without --pinned
names.sort.each do |d|
versions = d.subdirs.map { |pn| pn.basename.to_s }
next if args.multiple? && versions.length < 2
puts "#{d.basename} #{versions * " "}"
end
end
end
def list_casks
casks = if args.no_named?
Cask::Caskroom.casks
else
args.named.to_formulae_and_casks(only: :cask, method: :resolve)
end
full_cask_names = cask_names.map(&:full_name).sort(&tap_and_name_comparison)
full_cask_names = Formatter.columns(full_cask_names) unless args.public_send(:"1?")
puts full_cask_names if full_cask_names.present?
end
elsif args.pinned?
filtered_list(args:)
elsif args.versions?
filtered_list(args:) unless args.cask?
list_casks(args:) if args.cask? || (!args.formula? && !args.multiple? && args.no_named?)
elsif args.no_named?
ENV["CLICOLOR"] = nil
ls_args = []
ls_args << "-1" if args.public_send(:"1?")
ls_args << "-l" if args.l?
ls_args << "-r" if args.r?
ls_args << "-t" if args.t?
if !args.cask? && HOMEBREW_CELLAR.exist? && HOMEBREW_CELLAR.children.any?
ohai "Formulae" if $stdout.tty? && !args.formula?
safe_system "ls", *ls_args, HOMEBREW_CELLAR
puts if $stdout.tty? && !args.formula?
end
if !args.formula? && Cask::Caskroom.any_casks_installed?
ohai "Casks" if $stdout.tty? && !args.cask?
safe_system "ls", *ls_args, Cask::Caskroom.path
end
else
kegs, casks = args.named.to_kegs_to_casks
if args.verbose? || !$stdout.tty?
find_args = %w[-not -type d -not -name .DS_Store -print]
system_command! "find", args: kegs.map(&:to_s) + find_args, print_stdout: true if kegs.present?
system_command! "find", args: casks.map(&:caskroom_path) + find_args, print_stdout: true if casks.present?
else
kegs.each { |keg| PrettyListing.new keg } if kegs.present?
list_casks(args:) if casks.present?
end
end
end
def self.filtered_list(args:)
names = if args.no_named?
Formula.racks
else
racks = args.named.map { |n| Formulary.to_rack(n) }
racks.select do |rack|
Homebrew.failed = true unless rack.exist?
rack.exist?
end
end
if args.pinned?
pinned_versions = {}
names.sort.each do |d|
keg_pin = (HOMEBREW_PINNED_KEGS/d.basename.to_s)
pinned_versions[d] = keg_pin.readlink.basename.to_s if keg_pin.exist? || keg_pin.symlink?
end
pinned_versions.each do |d, version|
puts d.basename.to_s.concat(args.versions? ? " #{version}" : "")
end
else # --versions without --pinned
names.sort.each do |d|
versions = d.subdirs.map { |pn| pn.basename.to_s }
next if args.multiple? && versions.length < 2
puts "#{d.basename} #{versions * " "}"
end
end
end
def self.list_casks(args:)
casks = if args.no_named?
Cask::Caskroom.casks
else
args.named.dup.delete_if do |n|
Homebrew.failed = true unless Cask::Caskroom.path.join(n).exist?
!Cask::Caskroom.path.join(n).exist?
end.to_formulae_and_casks(only: :cask)
end
return if casks.blank?
Cask::List.list_casks(
*casks,
one: args.public_send(:"1?"),
full_name: args.full_name?,
versions: args.versions?,
)
end
end
class PrettyListing
def initialize(path)
Pathname.new(path).children.sort_by { |p| p.to_s.downcase }.each do |pn|
case pn.basename.to_s
when "bin", "sbin"
pn.find { |pnn| puts pnn unless pnn.directory? }
when "lib"
print_dir pn do |pnn|
# dylibs have multiple symlinks and we don't care about them
(pnn.extname == ".dylib" || pnn.extname == ".pc") && !pnn.symlink?
end
when ".brew"
next # Ignore .brew
else
if pn.directory?
if pn.symlink?
puts "#{pn} -> #{pn.readlink}"
else
print_dir pn
filtered_args = args.named.dup.delete_if do |n|
Homebrew.failed = true unless Cask::Caskroom.path.join(n).exist?
!Cask::Caskroom.path.join(n).exist?
end
elsif Metafiles.list?(pn.basename.to_s)
puts pn
# NamedAargs subclasses array
T.cast(filtered_args, Homebrew::CLI::NamedArgs).to_formulae_and_casks(only: :cask)
end
return if casks.blank?
Cask::List.list_casks(
*casks,
one: args.public_send(:"1?"),
full_name: args.full_name?,
versions: args.versions?,
)
end
end
class PrettyListing
def initialize(path)
Pathname.new(path).children.sort_by { |p| p.to_s.downcase }.each do |pn|
case pn.basename.to_s
when "bin", "sbin"
pn.find { |pnn| puts pnn unless pnn.directory? }
when "lib"
print_dir pn do |pnn|
# dylibs have multiple symlinks and we don't care about them
(pnn.extname == ".dylib" || pnn.extname == ".pc") && !pnn.symlink?
end
when ".brew"
next # Ignore .brew
else
if pn.directory?
if pn.symlink?
puts "#{pn} -> #{pn.readlink}"
else
print_dir pn
end
elsif Metafiles.list?(pn.basename.to_s)
puts pn
end
end
end
end
private
def print_dir(root)
dirs = []
remaining_root_files = []
other = ""
root.children.sort.each do |pn|
if pn.directory?
dirs << pn
elsif block_given? && yield(pn)
puts pn
other = "other "
elsif pn.basename.to_s != ".DS_Store"
remaining_root_files << pn
end
end
dirs.each do |d|
files = []
d.find { |pn| files << pn unless pn.directory? }
print_remaining_files files, d
end
print_remaining_files remaining_root_files, root, other
end
def print_remaining_files(files, root, other = "")
if files.length == 1
puts files
elsif files.length > 1
puts "#{root}/ (#{files.length} #{other}files)"
end
end
end
end
def print_dir(root)
dirs = []
remaining_root_files = []
other = ""
root.children.sort.each do |pn|
if pn.directory?
dirs << pn
elsif block_given? && yield(pn)
puts pn
other = "other "
elsif pn.basename.to_s != ".DS_Store"
remaining_root_files << pn
end
end
dirs.each do |d|
files = []
d.find { |pn| files << pn unless pn.directory? }
print_remaining_files files, d
end
print_remaining_files remaining_root_files, root, other
end
def print_remaining_files(files, root, other = "")
if files.length == 1
puts files
elsif files.length > 1
puts "#{root}/ (#{files.length} #{other}files)"
end
end
end

View File

@ -1,65 +1,64 @@
# typed: true
# typed: strict
# frozen_string_literal: true
require "abstract_command"
require "cli/parser"
module Homebrew
module_function
module DevCmd
class Prof < AbstractCommand
cmd_args do
description <<~EOS
Run Homebrew with a Ruby profiler. For example, `brew prof readall`.
EOS
switch "--stackprof",
description: "Use `stackprof` instead of `ruby-prof` (the default)."
sig { returns(CLI::Parser) }
def prof_args
Homebrew::CLI::Parser.new do
description <<~EOS
Run Homebrew with a Ruby profiler. For example, `brew prof readall`.
EOS
switch "--stackprof",
description: "Use `stackprof` instead of `ruby-prof` (the default)."
named_args :command, min: 1
end
end
def prof
args = prof_args.parse
Homebrew.install_bundler_gems!(groups: ["prof"], setup_path: false)
brew_rb = (HOMEBREW_LIBRARY_PATH/"brew.rb").resolved_path
FileUtils.mkdir_p "prof"
cmd = args.named.first
case Commands.path(cmd)&.extname
when ".rb"
# expected file extension so we do nothing
when ".sh"
raise UsageError, <<~EOS
`#{cmd}` is a Bash command!
Try `hyperfine` for benchmarking instead.
EOS
else
raise UsageError, "`#{cmd}` is an unknown command!"
end
Homebrew.setup_gem_environment!
if args.stackprof?
with_env HOMEBREW_STACKPROF: "1" do
system(*HOMEBREW_RUBY_EXEC_ARGS, brew_rb, *args.named)
named_args :command, min: 1
end
sig { override.void }
def run
Homebrew.install_bundler_gems!(groups: ["prof"], setup_path: false)
brew_rb = (HOMEBREW_LIBRARY_PATH/"brew.rb").resolved_path
FileUtils.mkdir_p "prof"
cmd = args.named.first
case Commands.path(cmd)&.extname
when ".rb"
# expected file extension so we do nothing
when ".sh"
raise UsageError, <<~EOS
`#{cmd}` is a Bash command!
Try `hyperfine` for benchmarking instead.
EOS
else
raise UsageError, "`#{cmd}` is an unknown command!"
end
Homebrew.setup_gem_environment!
if args.stackprof?
with_env HOMEBREW_STACKPROF: "1" do
system(*HOMEBREW_RUBY_EXEC_ARGS, brew_rb, *args.named)
end
output_filename = "prof/d3-flamegraph.html"
safe_system "stackprof --d3-flamegraph prof/stackprof.dump > #{output_filename}"
else
output_filename = "prof/call_stack.html"
safe_system "ruby-prof", "--printer=call_stack", "--file=#{output_filename}", brew_rb, "--", *args.named
end
exec_browser output_filename
rescue OptionParser::InvalidOption => e
ofail e
# The invalid option could have been meant for the subcommand.
# Suggest `brew prof list -r` -> `brew prof -- list -r`
args = ARGV - ["--"]
puts "Try `brew prof -- #{args.join(" ")}` instead."
end
output_filename = "prof/d3-flamegraph.html"
safe_system "stackprof --d3-flamegraph prof/stackprof.dump > #{output_filename}"
else
output_filename = "prof/call_stack.html"
safe_system "ruby-prof", "--printer=call_stack", "--file=#{output_filename}", brew_rb, "--", *args.named
end
exec_browser output_filename
rescue OptionParser::InvalidOption => e
ofail e
# The invalid option could have been meant for the subcommand.
# Suggest `brew prof list -r` -> `brew prof -- list -r`
args = ARGV - ["--"]
puts "Try `brew prof -- #{args.join(" ")}` instead."
end
end

View File

@ -146,18 +146,12 @@ class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def custom_remote?; end
sig { returns(T::Boolean) }
def d?; end
sig { returns(T.nilable(String)) }
def days; end
sig { returns(T::Boolean) }
def debian?; end
sig { returns(T::Boolean) }
def debug?; end
sig { returns(T::Boolean) }
def debug_symbols?; end
@ -317,12 +311,6 @@ class Homebrew::CLI::Args
sig { returns(T.nilable(T::Array[String])) }
def groups; end
sig { returns(T::Boolean) }
def h?; end
sig { returns(T::Boolean) }
def help?; end
sig { returns(T.nilable(T::Array[String])) }
def hide; end
@ -401,9 +389,6 @@ class Homebrew::CLI::Args
sig { returns(T.nilable(String)) }
def keyboard_layoutdir; end
sig { returns(T::Boolean) }
def l?; end
sig { returns(T.nilable(T::Array[String])) }
def language; end
@ -461,9 +446,6 @@ class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def missing?; end
sig { returns(T::Boolean) }
def multiple?; end
sig { returns(T.nilable(String)) }
def n; end
@ -578,9 +560,6 @@ class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def perl?; end
sig { returns(T::Boolean) }
def pinned?; end
sig { returns(T::Boolean) }
def plain?; end
@ -632,18 +611,12 @@ class Homebrew::CLI::Args
sig { returns(T.nilable(String)) }
def python_package_name; end
sig { returns(T::Boolean) }
def q?; end
sig { returns(T.nilable(String)) }
def qlplugindir; end
sig { returns(T::Boolean) }
def quarantine?; end
sig { returns(T::Boolean) }
def quiet?; end
sig { returns(T.nilable(String)) }
def r; end
@ -752,9 +725,6 @@ class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def skip_style?; end
sig { returns(T::Boolean) }
def stackprof?; end
sig { returns(T.nilable(String)) }
def start_with; end
@ -773,9 +743,6 @@ class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def syntax?; end
sig { returns(T::Boolean) }
def t?; end
sig { returns(T.nilable(String)) }
def tag; end
@ -839,15 +806,9 @@ class Homebrew::CLI::Args
sig { returns(T.nilable(T::Array[String])) }
def user; end
sig { returns(T::Boolean) }
def v?; end
sig { returns(T::Boolean) }
def variations?; end
sig { returns(T::Boolean) }
def verbose?; end
sig { returns(T.nilable(String)) }
def version; end
@ -857,9 +818,6 @@ class Homebrew::CLI::Args
sig { returns(T.nilable(String)) }
def version_intel; end
sig { returns(T::Boolean) }
def versions?; end
sig { returns(T.nilable(String)) }
def vst3_plugindir; end

View File

@ -0,0 +1,40 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Homebrew::Cmd::List`.
# Please instead update this file by running `bin/tapioca dsl Homebrew::Cmd::List`.
class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def cask?; end
sig { returns(T::Boolean) }
def casks?; end
sig { returns(T::Boolean) }
def formula?; end
sig { returns(T::Boolean) }
def formulae?; end
sig { returns(T::Boolean) }
def full_name?; end
sig { returns(T::Boolean) }
def l?; end
sig { returns(T::Boolean) }
def multiple?; end
sig { returns(T::Boolean) }
def pinned?; end
sig { returns(T::Boolean) }
def r?; end
sig { returns(T::Boolean) }
def t?; end
sig { returns(T::Boolean) }
def versions?; end
end

View File

@ -0,0 +1,10 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Homebrew::DevCmd::Prof`.
# Please instead update this file by running `bin/tapioca dsl Homebrew::DevCmd::Prof`.
class Homebrew::CLI::Args
sig { returns(T::Boolean) }
def stackprof?; end
end

View File

@ -2,10 +2,17 @@
# frozen_string_literal: true
require_relative "../../../global"
require "cli/parser"
module Tapioca
module Compilers
class Args < Tapioca::Dsl::Compiler
GLOBAL_OPTIONS = T.let(
Homebrew::CLI::Parser.global_options.map do |short_option, long_option, _|
[short_option, long_option].map { "#{Homebrew::CLI::Parser.option_to_name(_1)}?" }
end.flatten.freeze, T::Array[String]
)
# This is ugly, but we're moving to a new interface that will use a consistent DSL
# These are cmd/dev-cmd methods that end in `_args` but are not parsers
NON_PARSER_ARGS_METHODS = T.let([
@ -16,34 +23,34 @@ module Tapioca
# FIXME: Enable cop again when https://github.com/sorbet/sorbet/issues/3532 is fixed.
# rubocop:disable Style/MutableConstant
ConstantType = type_member { { fixed: T.class_of(Homebrew::CLI::Args) } }
Parsable = T.type_alias { T.any(T.class_of(Homebrew::CLI::Args), T.class_of(Homebrew::AbstractCommand)) }
ConstantType = type_member { { fixed: Parsable } }
# rubocop:enable Style/MutableConstant
sig { override.returns(T::Enumerable[T.class_of(Homebrew::CLI::Args)]) }
sig { override.returns(T::Enumerable[Parsable]) }
def self.gather_constants
# require all the commands to ensure the _arg methods are defined
["cmd", "dev-cmd"].each do |dir|
Dir[File.join(__dir__, "../../../#{dir}", "*.rb")].each { require(_1) }
end
[Homebrew::CLI::Args]
[Homebrew::CLI::Args] + Homebrew::AbstractCommand.subclasses
end
sig { override.void }
def decorate
root.create_path(Homebrew::CLI::Args) do |klass|
Homebrew.methods(false).select { _1.end_with?("_args") }.each do |args_method_name|
next if NON_PARSER_ARGS_METHODS.include?(args_method_name)
if constant == Homebrew::CLI::Args
root.create_path(Homebrew::CLI::Args) do |klass|
Homebrew.methods(false).select { _1.end_with?("_args") }.each do |args_method_name|
next if NON_PARSER_ARGS_METHODS.include?(args_method_name)
parser = Homebrew.method(args_method_name).call
comma_array_methods = comma_arrays(parser)
args_table(parser).each do |method_name, value|
# some args are used in multiple commands (this is ok as long as they have the same type)
next if klass.nodes.any? { T.cast(_1, RBI::Method).name.to_sym == method_name }
return_type = get_return_type(method_name, value, comma_array_methods)
klass.create_method(method_name.to_s, return_type:)
parser = Homebrew.method(args_method_name).call
create_args_methods(klass, parser)
end
end
else
root.create_path(Homebrew::CLI::Args) do |klass|
create_args_methods(klass, T.must(T.cast(constant, T.class_of(Homebrew::AbstractCommand)).parser))
end
end
end
@ -69,6 +76,22 @@ module Tapioca
"T.nilable(String)"
end
end
private
sig { params(klass: RBI::Scope, parser: Homebrew::CLI::Parser).void }
def create_args_methods(klass, parser)
comma_array_methods = comma_arrays(parser)
args_table(parser).each do |method_name, value|
method_name_str = method_name.to_s
next if GLOBAL_OPTIONS.include?(method_name_str)
# some args are used in multiple commands (this is ok as long as they have the same type)
next if klass.nodes.any? { T.cast(_1, RBI::Method).name == method_name_str }
return_type = get_return_type(method_name, value, comma_array_methods)
klass.create_method(method_name_str, return_type:)
end
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
require "abstract_command"
RSpec.describe Homebrew::AbstractCommand do
describe "subclasses" do
before do
cat = Class.new(described_class) do
cmd_args do
switch "--foo"
flag "--bar="
end
def run; end
end
stub_const("Cat", cat)
end
describe "parsing args" do
it "parses valid args" do
expect { Cat.new(["--foo"]).run }.not_to raise_error
end
it "allows access to args" do
expect(Cat.new(["--bar", "baz"]).args[:bar]).to eq("baz")
end
it "raises on invalid args" do
expect { Cat.new(["--bat"]) }.to raise_error(OptionParser::InvalidOption)
end
end
describe "command names" do
it "has a default command name" do
expect(Cat.command_name).to eq("cat")
end
it "can lookup command" do
expect(described_class.command("cat")).to be(Cat)
end
describe "when command name is overridden" do
before do
tac = Class.new(described_class) do
def self.command_name = "t-a-c"
def run; end
end
stub_const("Tac", tac)
end
it "can be looked up by command name" do
expect(described_class.command("t-a-c")).to be(Tac)
end
end
end
end
end

View File

@ -0,0 +1,4 @@
# typed: strict
class Cat < Homebrew::AbstractCommand; end
class Tac < Homebrew::AbstractCommand; end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require "cmd/list"
require "cmd/shared_examples/args_parse"
RSpec.describe "brew list" do
RSpec.describe Homebrew::Cmd::List do
let(:formulae) { %w[bar foo qux] }
it_behaves_like "parseable arguments"

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples "parseable arguments" do
RSpec.shared_examples "parseable arguments" do |argv: []|
subject(:method_name) { "#{command_name.tr("-", "_")}_args" }
let(:command_name) do |example|
@ -8,10 +8,13 @@ RSpec.shared_examples "parseable arguments" do
end
it "can parse arguments" do
require "dev-cmd/#{command_name}" unless require? "cmd/#{command_name}"
parser = Homebrew.public_send(method_name)
expect(parser).to respond_to(:parse)
if described_class
cmd = described_class.new(argv)
expect(cmd.args).to be_a Homebrew::CLI::Args
else
require "dev-cmd/#{command_name}" unless require? "cmd/#{command_name}"
parser = Homebrew.public_send(method_name)
expect(parser).to respond_to(:parse)
end
end
end

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
require "cmd/shared_examples/args_parse"
require "dev-cmd/prof"
RSpec.describe "brew prof" do
it_behaves_like "parseable arguments"
RSpec.describe Homebrew::DevCmd::Prof do
it_behaves_like "parseable arguments", argv: ["--", "help"]
describe "integration tests", :integration_test, :needs_network do
after do

View File

@ -1,14 +1,13 @@
# frozen_string_literal: true
# require 'tapioca'
require "tapioca/dsl"
require_relative "../../../../sorbet/tapioca/compilers/args"
require "sorbet/tapioca/compilers/args"
RSpec.describe Tapioca::Compilers::Args do
let(:compiler) { described_class.new(Tapioca::Dsl::Pipeline.new(requested_constants: []), RBI::Tree.new, Homebrew) }
let(:list_parser) do
require "cmd/list"
Homebrew.list_args
Homebrew::Cmd::List.parser
end
# good testing candidate, bc it has multiple for each of switch, flag, and comma_array args:
let(:update_python_resources_parser) do