diff --git a/Library/Homebrew/cli/args.rb b/Library/Homebrew/cli/args.rb index a67d0e7d10..84c621fd8b 100644 --- a/Library/Homebrew/cli/args.rb +++ b/Library/Homebrew/cli/args.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "cli/named_args" require "ostruct" module Homebrew @@ -19,7 +20,7 @@ module Homebrew # Can set these because they will be overwritten by freeze_named_args! # (whereas other values below will only be overwritten if passed). - self[:named_args] = [] + self[:named_args] = NamedArgs.new self[:remaining] = [] end @@ -28,18 +29,12 @@ module Homebrew end def freeze_named_args!(named_args) - # Reset cache values reliant on named_args - @formulae = nil - @formulae_and_casks = nil - @resolved_formulae = nil - @resolved_formulae_casks = nil - @formulae_paths = nil - @casks = nil - @loaded_casks = nil - @kegs = nil - @kegs_casks = nil - - self[:named_args] = named_args.freeze + self[:named_args] = NamedArgs.new( + *named_args.freeze, + override_spec: spec(nil), + force_bottle: force_bottle?, + flags: flags_only, + ) end def freeze_processed_options!(processed_options) @@ -54,7 +49,7 @@ module Homebrew end def named - named_args || [] + named_args || NamedArgs.new end def no_named? @@ -62,102 +57,39 @@ module Homebrew end def formulae - require "formula" - - @formulae ||= (downcased_unique_named - casks).map do |name| - Formulary.factory(name, spec, force_bottle: force_bottle?, flags: flags_only) - end.uniq(&:name).freeze + named.to_formulae end def formulae_and_casks - @formulae_and_casks ||= begin - formulae_and_casks = [] - - downcased_unique_named.each do |name| - formulae_and_casks << Formulary.factory(name, spec) - rescue FormulaUnavailableError - begin - formulae_and_casks << Cask::CaskLoader.load(name) - rescue Cask::CaskUnavailableError - raise "No available formula or cask with the name \"#{name}\"" - end - end - - formulae_and_casks.freeze - end + named.to_formulae_and_casks end def resolved_formulae - require "formula" - - @resolved_formulae ||= (downcased_unique_named - casks).map do |name| - Formulary.resolve(name, spec: spec(nil), force_bottle: force_bottle?, flags: flags_only) - end.uniq(&:name).freeze + named.to_resolved_formulae end def resolved_formulae_casks - @resolved_formulae_casks ||= begin - resolved_formulae = [] - casks = [] - - downcased_unique_named.each do |name| - resolved_formulae << Formulary.resolve(name, spec: spec(nil), - force_bottle: force_bottle?, flags: flags_only) - rescue FormulaUnavailableError - begin - casks << Cask::CaskLoader.load(name) - rescue Cask::CaskUnavailableError - raise "No available formula or cask with the name \"#{name}\"" - end - end - - [resolved_formulae.freeze, casks.freeze].freeze - end + named.to_resolved_formulae_to_casks end def formulae_paths - @formulae_paths ||= (downcased_unique_named - casks).map do |name| - Formulary.path(name) - end.uniq.freeze + named.to_formulae_paths end def casks - @casks ||= downcased_unique_named.grep(HOMEBREW_CASK_TAP_CASK_REGEX) - .freeze + named.homebrew_tap_cask_names end def loaded_casks - @loaded_casks ||= downcased_unique_named.map(&Cask::CaskLoader.method(:load)).freeze + named.to_casks end def kegs - @kegs ||= downcased_unique_named.map do |name| - resolve_keg name - rescue NoSuchKegError => e - if (reason = Homebrew::MissingFormula.suggest_command(name, "uninstall")) - $stderr.puts reason - end - raise e - end.freeze + named.to_kegs end def kegs_casks - @kegs_casks ||= begin - kegs = [] - casks = [] - - downcased_unique_named.each do |name| - kegs << resolve_keg(name) - rescue NoSuchKegError - begin - casks << Cask::CaskLoader.load(name) - rescue Cask::CaskUnavailableError - raise "No installed keg or cask with the name \"#{name}\"" - end - end - - [kegs.freeze, casks.freeze].freeze - end + named.to_kegs_to_casks end def build_stable? @@ -218,17 +150,6 @@ module Homebrew @cli_args.freeze end - def downcased_unique_named - # Only lowercase names, not paths, bottle filenames or URLs - named.map do |arg| - if arg.include?("/") || arg.end_with?(".tar.gz") || File.exist?(arg) - arg - else - arg.downcase - end - end.uniq - end - def spec(default = :stable) if HEAD? :head @@ -238,50 +159,6 @@ module Homebrew default end end - - def resolve_keg(name) - require "keg" - require "formula" - require "missing_formula" - - raise UsageError if name.blank? - - rack = Formulary.to_rack(name.downcase) - - dirs = rack.directory? ? rack.subdirs : [] - raise NoSuchKegError, rack.basename if dirs.empty? - - linked_keg_ref = HOMEBREW_LINKED_KEGS/rack.basename - opt_prefix = HOMEBREW_PREFIX/"opt/#{rack.basename}" - - begin - if opt_prefix.symlink? && opt_prefix.directory? - Keg.new(opt_prefix.resolved_path) - elsif linked_keg_ref.symlink? && linked_keg_ref.directory? - Keg.new(linked_keg_ref.resolved_path) - elsif dirs.length == 1 - Keg.new(dirs.first) - else - f = if name.include?("/") || File.exist?(name) - Formulary.factory(name) - else - Formulary.from_rack(rack) - end - - unless (prefix = f.installed_prefix).directory? - raise MultipleVersionsInstalledError, "#{rack.basename} has multiple installed versions" - end - - Keg.new(prefix) - end - rescue FormulaUnavailableError - raise MultipleVersionsInstalledError, <<~EOS - Multiple kegs installed to #{rack} - However we don't know which one you refer to. - Please delete (with rm -rf!) all but one and then try again. - EOS - end - end end end end diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb new file mode 100644 index 0000000000..0690bfb2f8 --- /dev/null +++ b/Library/Homebrew/cli/named_args.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require "delegate" + +module Homebrew + module CLI + class NamedArgs < SimpleDelegator + def initialize(*args, override_spec: nil, force_bottle: false, flags: []) + @args = args + @override_spec = override_spec + @force_bottle = force_bottle + @flags = flags + + __setobj__(@args) + end + + def to_formulae + @to_formulae ||= (downcased_unique_named - homebrew_tap_cask_names).map do |name| + Formulary.factory(name, spec, force_bottle: @force_bottle, flags: @flags) + end.uniq(&:name).freeze + end + + def to_formulae_and_casks + @to_formulae_and_casks ||= begin + formulae_and_casks = [] + + downcased_unique_named.each do |name| + formulae_and_casks << Formulary.factory(name, spec) + rescue FormulaUnavailableError + begin + formulae_and_casks << Cask::CaskLoader.load(name) + rescue Cask::CaskUnavailableError + raise "No available formula or cask with the name \"#{name}\"" + end + end + + formulae_and_casks.freeze + end + end + + def to_resolved_formulae + @to_resolved_formulae ||= (downcased_unique_named - homebrew_tap_cask_names).map do |name| + Formulary.resolve(name, spec: spec(nil), force_bottle: @force_bottle, flags: @flags) + end.uniq(&:name).freeze + end + + def to_resolved_formulae_to_casks + @to_resolved_formulae_to_casks ||= begin + resolved_formulae = [] + casks = [] + + downcased_unique_named.each do |name| + resolved_formulae << Formulary.resolve(name, spec: spec(nil), force_bottle: @force_bottle, flags: @flags) + rescue FormulaUnavailableError + begin + casks << Cask::CaskLoader.load(name) + rescue Cask::CaskUnavailableError + raise "No available formula or cask with the name \"#{name}\"" + end + end + + [resolved_formulae.freeze, casks.freeze].freeze + end + end + + def to_formulae_paths + @to_formulae_paths ||= (downcased_unique_named - homebrew_tap_cask_names).map do |name| + Formulary.path(name) + end.uniq.freeze + end + + def to_casks + @to_casks ||= downcased_unique_named.map(&Cask::CaskLoader.method(:load)).freeze + end + + def to_kegs + @to_kegs ||= downcased_unique_named.map do |name| + resolve_keg name + rescue NoSuchKegError => e + if (reason = Homebrew::MissingFormula.suggest_command(name, "uninstall")) + $stderr.puts reason + end + raise e + end.freeze + end + + def to_kegs_to_casks + @to_kegs_to_casks ||= begin + kegs = [] + casks = [] + + downcased_unique_named.each do |name| + kegs << resolve_keg(name) + rescue NoSuchKegError, FormulaUnavailableError + begin + casks << Cask::CaskLoader.load(name) + rescue Cask::CaskUnavailableError + raise "No installed keg or cask with the name \"#{name}\"" + end + end + + [kegs.freeze, casks.freeze].freeze + end + end + + def homebrew_tap_cask_names + downcased_unique_named.grep(HOMEBREW_CASK_TAP_CASK_REGEX) + end + + private + + def downcased_unique_named + # Only lowercase names, not paths, bottle filenames or URLs + map do |arg| + if arg.include?("/") || arg.end_with?(".tar.gz") || File.exist?(arg) + arg + else + arg.downcase + end + end.uniq + end + + def spec(default = :stable) + @override_spec || default + end + + def resolve_keg(name) + raise UsageError if name.blank? + + rack = Formulary.to_rack(name.downcase) + + dirs = rack.directory? ? rack.subdirs : [] + raise NoSuchKegError, rack.basename if dirs.empty? + + linked_keg_ref = HOMEBREW_LINKED_KEGS/rack.basename + opt_prefix = HOMEBREW_PREFIX/"opt/#{rack.basename}" + + begin + if opt_prefix.symlink? && opt_prefix.directory? + Keg.new(opt_prefix.resolved_path) + elsif linked_keg_ref.symlink? && linked_keg_ref.directory? + Keg.new(linked_keg_ref.resolved_path) + elsif dirs.length == 1 + Keg.new(dirs.first) + else + f = if name.include?("/") || File.exist?(name) + Formulary.factory(name) + else + Formulary.from_rack(rack) + end + + unless (prefix = f.installed_prefix).directory? + raise MultipleVersionsInstalledError, "#{rack.basename} has multiple installed versions" + end + + Keg.new(prefix) + end + rescue FormulaUnavailableError + raise MultipleVersionsInstalledError, <<~EOS + Multiple kegs installed to #{rack} + However we don't know which one you refer to. + Please delete (with rm -rf!) all but one and then try again. + EOS + end + end + end + end +end diff --git a/Library/Homebrew/test/cli/named_args_spec.rb b/Library/Homebrew/test/cli/named_args_spec.rb new file mode 100644 index 0000000000..c3bc0f6e62 --- /dev/null +++ b/Library/Homebrew/test/cli/named_args_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "cli/named_args" + +describe Homebrew::CLI::NamedArgs do + let(:foo) do + formula "foo" do + url "https://brew.sh" + version "1.0" + end + end + + let(:foo_keg) do + path = (HOMEBREW_CELLAR/"foo/1.0").resolved_path + mkdir_p path + Keg.new(path) + end + + let(:bar) do + formula "bar" do + url "https://brew.sh" + version "1.0" + end + end + + let(:bar_keg) do + path = (HOMEBREW_CELLAR/"bar/1.0").resolved_path + mkdir_p path + Keg.new(path) + end + + let(:baz) do + Cask::CaskLoader.load(+<<~RUBY) + cask "baz" do + version "1.0" + end + RUBY + end + + describe "#to_formulae" do + it "returns formulae" do + allow(Formulary).to receive(:loader_for).and_call_original + stub_formula_loader foo + stub_formula_loader bar + + expect(described_class.new("foo", "bar").to_formulae).to eq [foo, bar] + end + end + + describe "#to_formulae_and_casks" do + it "returns formulae and casks" do + allow(Formulary).to receive(:loader_for).and_call_original + stub_formula_loader foo + stub_cask_loader baz + + expect(described_class.new("foo", "baz").to_formulae_and_casks).to eq [foo, baz] + end + end + + describe "#to_resolved_formulae" do + it "returns resolved formulae" do + allow(Formulary).to receive(:resolve).and_return(foo, bar) + + expect(described_class.new("foo", "bar").to_resolved_formulae).to eq [foo, bar] + end + end + + describe "#to_resolved_formulae_to_casks" do + it "returns resolved formulae, as well as casks" do + allow(Formulary).to receive(:resolve).and_call_original + allow(Formulary).to receive(:resolve).with("foo", any_args).and_return foo + stub_cask_loader baz + + resolved_formulae, casks = described_class.new("foo", "baz").to_resolved_formulae_to_casks + + expect(resolved_formulae).to eq [foo] + expect(casks).to eq [baz] + end + end + + describe "#to_casks" do + it "returns casks" do + stub_cask_loader baz + + expect(described_class.new("baz").to_casks).to eq [baz] + end + end + + describe "#to_kegs" do + it "returns kegs" do + named_args = described_class.new("foo", "bar") + allow(named_args).to receive(:resolve_keg).with("foo").and_return foo_keg + allow(named_args).to receive(:resolve_keg).with("bar").and_return bar_keg + + expect(named_args.to_kegs).to eq [foo_keg, bar_keg] + end + end + + describe "#to_kegs_to_casks" do + it "returns kegs, as well as casks" do + named_args = described_class.new("foo", "baz") + allow(named_args).to receive(:resolve_keg).and_call_original + allow(named_args).to receive(:resolve_keg).with("foo").and_return foo_keg + stub_cask_loader baz + + kegs, casks = named_args.to_kegs_to_casks + + expect(kegs).to eq [foo_keg] + expect(casks).to eq [baz] + end + end +end diff --git a/Library/Homebrew/test/spec_helper.rb b/Library/Homebrew/test/spec_helper.rb index 9e6ee3e826..1aad194f5e 100644 --- a/Library/Homebrew/test/spec_helper.rb +++ b/Library/Homebrew/test/spec_helper.rb @@ -36,6 +36,7 @@ $LOAD_PATH.push(File.expand_path("#{ENV["HOMEBREW_LIBRARY"]}/Homebrew/test/suppo require_relative "../global" require "test/support/no_seed_progress_formatter" +require "test/support/helper/cask" require "test/support/helper/fixtures" require "test/support/helper/formula" require "test/support/helper/mktmpdir" @@ -86,6 +87,7 @@ RSpec.configure do |config| config.include(RuboCop::RSpec::ExpectOffense) + config.include(Test::Helper::Cask) config.include(Test::Helper::Fixtures) config.include(Test::Helper::Formula) config.include(Test::Helper::MkTmpDir) diff --git a/Library/Homebrew/test/support/helper/cask.rb b/Library/Homebrew/test/support/helper/cask.rb new file mode 100644 index 0000000000..875d9ac8a9 --- /dev/null +++ b/Library/Homebrew/test/support/helper/cask.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "cask/cask_loader" + +module Test + module Helper + module Cask + def stub_cask_loader(cask, ref = cask.token) + loader = ::Cask::CaskLoader::FromInstanceLoader.new cask + allow(::Cask::CaskLoader).to receive(:for).with(ref).and_return(loader) + end + end + end +end