diff --git a/Library/Homebrew/cask/cmd/zap.rb b/Library/Homebrew/cask/cmd/zap.rb index d5b5dc3c80..028c103afd 100644 --- a/Library/Homebrew/cask/cmd/zap.rb +++ b/Library/Homebrew/cask/cmd/zap.rb @@ -32,6 +32,15 @@ module Cask sig { void } def run + self.class.zap_casks(*casks, verbose: args.verbose?, force: args.force?) + end + + sig { params(casks: Cask, force: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean)).void } + def self.zap_casks( + *casks, + force: nil, + verbose: nil + ) require "cask/installer" casks.each do |cask| @@ -43,10 +52,10 @@ module Cask cask = CaskLoader.load(installed_caskfile) end else - raise CaskNotInstalledError, cask unless args.force? + raise CaskNotInstalledError, cask unless force end - Installer.new(cask, verbose: args.verbose?, force: args.force?).zap + Installer.new(cask, verbose: verbose, force: force).zap end end end diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index bed08e85e6..bc7e3f49d1 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -35,10 +35,10 @@ module Homebrew @to_formulae ||= to_formulae_and_casks(only: :formula).freeze end - def to_formulae_and_casks(only: nil, method: nil) + def to_formulae_and_casks(only: nil, ignore_unavailable: nil, method: nil) @to_formulae_and_casks ||= {} @to_formulae_and_casks[only] ||= begin - to_objects(only: only, method: method).reject { |o| o.is_a?(Tap) }.freeze + to_objects(only: only, ignore_unavailable: ignore_unavailable, method: method).freeze end end @@ -68,6 +68,9 @@ module Homebrew resolve_formula(name) when :keg resolve_keg(name) + when :kegs + rack = Formulary.to_rack(name) + rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : [] else raise end @@ -108,10 +111,12 @@ module Homebrew # Convert named arguments to {Formula} or {Cask} objects. # If both a formula and cask exist with the same name, returns the # formula and prints a warning unless `only` is specified. - def to_objects(only: nil, method: nil) + def to_objects(only: nil, ignore_unavailable: nil, method: nil) @to_objects ||= {} - @to_objects[only] ||= downcased_unique_named.map do |name| + @to_objects[only] ||= downcased_unique_named.flat_map do |name| load_formula_or_cask(name, only: only, method: method) + rescue NoSuchKegError, FormulaUnavailableError, Cask::CaskUnavailableError + ignore_unavailable ? [] : raise end.uniq.freeze end private :to_objects @@ -159,11 +164,17 @@ module Homebrew end end - sig { params(only: T.nilable(Symbol)).returns([T::Array[Keg], T::Array[Cask::Cask]]) } - def to_kegs_to_casks(only: nil) - @to_kegs_to_casks ||= to_formulae_and_casks(only: only, method: :keg) - .partition { |o| o.is_a?(Keg) } - .map(&:freeze).freeze + sig do + params(only: T.nilable(Symbol), ignore_unavailable: T.nilable(T::Boolean), all_kegs: T.nilable(T::Boolean)) + .returns([T::Array[Keg], T::Array[Cask::Cask]]) + end + def to_kegs_to_casks(only: nil, ignore_unavailable: nil, all_kegs: nil) + method = all_kegs ? :kegs : :keg + @to_kegs_to_casks ||= {} + @to_kegs_to_casks[method] ||= + to_formulae_and_casks(only: only, ignore_unavailable: ignore_unavailable, method: method) + .partition { |o| o.is_a?(Keg) } + .map(&:freeze).freeze end sig { returns(T::Array[String]) } diff --git a/Library/Homebrew/cmd/uninstall.rb b/Library/Homebrew/cmd/uninstall.rb index 7b787d923e..bb7968a9bf 100644 --- a/Library/Homebrew/cmd/uninstall.rb +++ b/Library/Homebrew/cmd/uninstall.rb @@ -1,4 +1,4 @@ -# typed: false +# typed: true # frozen_string_literal: true require "keg" @@ -19,12 +19,17 @@ module Homebrew def uninstall_args Homebrew::CLI::Parser.new do usage_banner <<~EOS - `uninstall`, `rm`, `remove` [] + `uninstall`, `rm`, `remove` [] | - Uninstall . + Uninstall a or . EOS switch "-f", "--force", - description: "Delete all installed versions of ." + description: "Delete all installed versions of . Uninstall even if is not " \ + "installed, overwrite existing files and ignore errors when removing files." + switch "--zap", + description: "Remove all files associated with a . " \ + "*May remove files which are shared between applications.*" + conflicts "--formula", "--zap" switch "--ignore-dependencies", description: "Don't fail uninstall, even if is a dependency of any installed "\ "formulae." @@ -35,7 +40,7 @@ module Homebrew description: "Treat all named arguments as casks." conflicts "--formula", "--cask" - min_named :formula + min_named :formula_or_cask end end @@ -45,51 +50,29 @@ module Homebrew only = :formula if args.formula? && !args.cask? only = :cask if args.cask? && !args.formula? - if args.force? - casks = [] - kegs_by_rack = {} + all_kegs, casks = args.named.to_kegs_to_casks(only: only, ignore_unavailable: args.force?, all_kegs: args.force?) + kegs_by_rack = all_kegs.group_by(&:rack) - args.named.each do |name| - if only != :cask - rack = Formulary.to_rack(name) - kegs_by_rack[rack] = rack.subdirs.map { |d| Keg.new(d) } if rack.directory? - end - - next if only == :formula - - begin - casks << Cask::CaskLoader.load(name) - rescue Cask::CaskUnavailableError - # Since the uninstall was forced, ignore any unavailable casks. - end - end - else - all_kegs, casks = args.named.to_kegs_to_casks(only: only) - kegs_by_rack = all_kegs.group_by(&:rack) - end - - Uninstall.uninstall_kegs(kegs_by_rack, - force: args.force?, - ignore_dependencies: args.ignore_dependencies?, - named_args: args.named) - - return if casks.blank? - - Cask::Cmd::Uninstall.uninstall_casks( - *casks, - binaries: EnvConfig.cask_opts_binaries?, - verbose: args.verbose?, - force: args.force?, + Uninstall.uninstall_kegs( + kegs_by_rack, + force: args.force?, + ignore_dependencies: args.ignore_dependencies?, + named_args: args.named, ) - rescue MultipleVersionsInstalledError => e - ofail e - ensure - # If we delete Cellar/newname, then Cellar/oldname symlink - # can become broken and we have to remove it. - if HOMEBREW_CELLAR.directory? - HOMEBREW_CELLAR.children.each do |rack| - rack.unlink if rack.symlink? && !rack.resolved_path_exists? - end + + if args.zap? + Cask::Cmd::Zap.zap_casks( + *casks, + verbose: args.verbose?, + force: args.force?, + ) + else + Cask::Cmd::Uninstall.uninstall_casks( + *casks, + binaries: EnvConfig.cask_opts_binaries?, + verbose: args.verbose?, + force: args.force?, + ) end end end diff --git a/Library/Homebrew/test/cask/cmd/style_spec.rb b/Library/Homebrew/test/cask/cmd/style_spec.rb index 7862cfd519..c494be65d9 100644 --- a/Library/Homebrew/test/cask/cmd/style_spec.rb +++ b/Library/Homebrew/test/cask/cmd/style_spec.rb @@ -75,7 +75,7 @@ describe Cask::Cmd::Style, :cask do end it "tries to find paths for all tokens" do - expect(Cask::CaskLoader).to receive(:load).twice.and_return(double("cask", sourcefile_path: nil)) + expect(Cask::CaskLoader).to receive(:load).twice.and_return(instance_double(Cask::Cask, sourcefile_path: nil)) subject end end diff --git a/Library/Homebrew/uninstall.rb b/Library/Homebrew/uninstall.rb index 214f33a662..cd3140e2bb 100644 --- a/Library/Homebrew/uninstall.rb +++ b/Library/Homebrew/uninstall.rb @@ -82,6 +82,16 @@ module Homebrew end end end + rescue MultipleVersionsInstalledError => e + ofail e + ensure + # If we delete Cellar/newname, then Cellar/oldname symlink + # can become broken and we have to remove it. + if HOMEBREW_CELLAR.directory? + HOMEBREW_CELLAR.children.each do |rack| + rack.unlink if rack.symlink? && !rack.resolved_path_exists? + end + end end def handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: false, named_args: []) diff --git a/docs/Manpage.md b/docs/Manpage.md index 1a5f2780e8..8a182c2f9e 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -569,12 +569,14 @@ If no *`tap`* names are provided, display brief statistics for all installed tap * `--json`: Print a JSON representation of *`tap`*. Currently the default and only accepted value for *`version`* is `v1`. See the docs for examples of using the JSON output: -### `uninstall`, `rm`, `remove` [*`options`*] *`formula`* +### `uninstall`, `rm`, `remove` [*`options`*] *`formula`*|*`cask`* -Uninstall *`formula`*. +Uninstall a *`formula`* or *`cask`*. * `-f`, `--force`: - Delete all installed versions of *`formula`*. + Delete all installed versions of *`formula`*. Uninstall even if *`cask`* is not installed, overwrite existing files and ignore errors when removing files. +* `--zap`: + Remove all files associated with a *`cask`*. *May remove files which are shared between applications.* * `--ignore-dependencies`: Don't fail uninstall, even if *`formula`* is a dependency of any installed formulae. * `--formula`: diff --git a/manpages/brew.1 b/manpages/brew.1 index f9dbaa6629..1f85602b64 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -794,12 +794,16 @@ Show information on each installed tap\. \fB\-\-json\fR Print a JSON representation of \fItap\fR\. Currently the default and only accepted value for \fIversion\fR is \fBv1\fR\. See the docs for examples of using the JSON output: \fIhttps://docs\.brew\.sh/Querying\-Brew\fR . -.SS "\fBuninstall\fR, \fBrm\fR, \fBremove\fR [\fIoptions\fR] \fIformula\fR" -Uninstall \fIformula\fR\. +.SS "\fBuninstall\fR, \fBrm\fR, \fBremove\fR [\fIoptions\fR] \fIformula\fR|\fIcask\fR" +Uninstall a \fIformula\fR or \fIcask\fR\. . .TP \fB\-f\fR, \fB\-\-force\fR -Delete all installed versions of \fIformula\fR\. +Delete all installed versions of \fIformula\fR\. Uninstall even if \fIcask\fR is not installed, overwrite existing files and ignore errors when removing files\. +. +.TP +\fB\-\-zap\fR +Remove all files associated with a \fIcask\fR\. \fIMay remove files which are shared between applications\.\fR . .TP \fB\-\-ignore\-dependencies\fR