Merge pull request #16815 from dduugg/abstract-command
Provide interface and individual namespaces for brew CLI commands
This commit is contained in:
commit
2cc3ce9bb4
51
Library/Homebrew/abstract_command.rb
Normal file
51
Library/Homebrew/abstract_command.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
19
Library/Homebrew/cli/args.rbi
Normal file
19
Library/Homebrew/cli/args.rbi
Normal 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
|
||||
@ -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}"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
40
Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/list.rbi
generated
Normal file
40
Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/list.rbi
generated
Normal 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
|
||||
10
Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/prof.rbi
generated
Normal file
10
Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/prof.rbi
generated
Normal 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
|
||||
@ -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
|
||||
|
||||
56
Library/Homebrew/test/abstract_command_spec.rb
Normal file
56
Library/Homebrew/test/abstract_command_spec.rb
Normal 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
|
||||
4
Library/Homebrew/test/abstract_command_spec.rbi
Normal file
4
Library/Homebrew/test/abstract_command_spec.rbi
Normal file
@ -0,0 +1,4 @@
|
||||
# typed: strict
|
||||
|
||||
class Cat < Homebrew::AbstractCommand; end
|
||||
class Tac < Homebrew::AbstractCommand; end
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user