From f4b4fdac989448d68b5b8397b8c2a864059874c4 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 19 Nov 2020 12:57:10 +0100 Subject: [PATCH] Add `brew zap` command. --- Library/Homebrew/cask/cmd/zap.rb | 13 +++- Library/Homebrew/cli/named_args.rb | 33 +++++++--- Library/Homebrew/cmd/uninstall.rb | 57 +++++------------ Library/Homebrew/cmd/zap.rb | 64 ++++++++++++++++++++ Library/Homebrew/test/cask/cmd/style_spec.rb | 2 +- Library/Homebrew/uninstall.rb | 10 +++ completions/internal_commands_list.txt | 1 + docs/Manpage.md | 22 ++++++- manpages/brew.1 | 28 ++++++++- 9 files changed, 169 insertions(+), 61 deletions(-) create mode 100644 Library/Homebrew/cmd/zap.rb 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 a7423991d5..0a732b1241 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 @@ -58,6 +58,10 @@ module Homebrew end.uniq.freeze end + sig do + params(name: String, only: T.nilable(Symbol), method: T.nilable(Symbol)) + .returns(T.any(Formula, Cask::Cask, Keg, T::Array[Keg])) + end def load_formula_or_cask(name, only: nil, method: nil) if only != :cask begin @@ -68,6 +72,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 +115,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 +168,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..bd893c30ca 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,13 @@ 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 "--ignore-dependencies", description: "Don't fail uninstall, even if is a dependency of any installed "\ "formulae." @@ -35,7 +36,7 @@ module Homebrew description: "Treat all named arguments as casks." conflicts "--formula", "--cask" - min_named :formula + min_named :formula_or_cask end end @@ -45,35 +46,15 @@ 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? + Uninstall.uninstall_kegs( + kegs_by_rack, + force: args.force?, + ignore_dependencies: args.ignore_dependencies?, + named_args: args.named, + ) Cask::Cmd::Uninstall.uninstall_casks( *casks, @@ -81,15 +62,5 @@ module Homebrew verbose: args.verbose?, force: args.force?, ) - 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 end diff --git a/Library/Homebrew/cmd/zap.rb b/Library/Homebrew/cmd/zap.rb new file mode 100644 index 0000000000..70a80991d9 --- /dev/null +++ b/Library/Homebrew/cmd/zap.rb @@ -0,0 +1,64 @@ +# typed: true +# frozen_string_literal: true + +require "cask/cmd" +require "cask/cask_loader" +require "uninstall" + +module Homebrew + extend T::Sig + + module_function + + sig { returns(CLI::Parser) } + def zap_args + Homebrew::CLI::Parser.new do + usage_banner <<~EOS + `zap` [] | + + Remove all files associated with the given or . + Implicitly also performs all actions associated with `uninstall`. + + *May remove files which are shared between applications.* + EOS + switch "-f", "--force", + description: "Delete all installed versions of . Uninstall even if is not " \ + "installed, overwrite existing files and ignore errors when removing files." + switch "--ignore-dependencies", + description: "Don't fail uninstall, even if is a dependency of any installed "\ + "formulae." + + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." + conflicts "--formula", "--cask" + + min_named :formula_or_cask + end + end + + def zap + args = zap_args.parse + + only = :formula if args.formula? && !args.cask? + only = :cask if args.cask? && !args.formula? + + 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) + + Uninstall.uninstall_kegs( + kegs_by_rack, + force: args.force?, + ignore_dependencies: args.ignore_dependencies?, + named_args: args.named, + ) + + Cask::Cmd::Zap.zap_casks( + *casks, + binaries: EnvConfig.cask_opts_binaries?, + verbose: args.verbose?, + force: args.force?, + ) + 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/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index 0d277a1200..34d23167e0 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -104,3 +104,4 @@ upgrade uses vendor-gems vendor-install +zap diff --git a/docs/Manpage.md b/docs/Manpage.md index 8bc56518e8..dbcdc13bce 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -563,12 +563,12 @@ 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. * `--ignore-dependencies`: Don't fail uninstall, even if *`formula`* is a dependency of any installed formulae. * `--formula`: @@ -677,6 +677,22 @@ specify *`formula`* as a required or recommended dependency for their stable bui * `--skip-recommended`: Skip all formulae that specify *`formula`* as `:recommended` type dependency. +### `zap` [*`options`*] *`formula`*|*`cask`* + +Remove all files associated with the given *`formula`* or *`cask`*. +Implicitly also performs all actions associated with `uninstall`. + +*May remove files which are shared between applications.* + +* `-f`, `--force`: + Delete all installed versions of *`formula`*. Uninstall even if *`cask`* is not installed, overwrite existing files and ignore errors when removing files. +* `--ignore-dependencies`: + Don't fail uninstall, even if *`formula`* is a dependency of any installed formulae. +* `--formula`: + Treat all named arguments as formulae. +* `--cask`: + Treat all named arguments as casks. + ### `--cache` [*`options`*] [*`formula`*|*`cask`*] Display Homebrew's download cache. See also `HOMEBREW_CACHE`. diff --git a/manpages/brew.1 b/manpages/brew.1 index ec68ae005e..26cacaa7ec 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -782,12 +782,12 @@ 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\-\-ignore\-dependencies\fR @@ -940,6 +940,28 @@ Include all formulae that specify \fIformula\fR as \fB:optional\fR type dependen \fB\-\-skip\-recommended\fR Skip all formulae that specify \fIformula\fR as \fB:recommended\fR type dependency\. . +.SS "\fBzap\fR [\fIoptions\fR] \fIformula\fR|\fIcask\fR" +Remove all files associated with the given \fIformula\fR or \fIcask\fR\. Implicitly also performs all actions associated with \fBuninstall\fR\. +. +.P +\fIMay remove files which are shared between applications\.\fR +. +.TP +\fB\-f\fR, \fB\-\-force\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\-\-ignore\-dependencies\fR +Don\'t fail uninstall, even if \fIformula\fR is a dependency of any installed formulae\. +. +.TP +\fB\-\-formula\fR +Treat all named arguments as formulae\. +. +.TP +\fB\-\-cask\fR +Treat all named arguments as casks\. +. .SS "\fB\-\-cache\fR [\fIoptions\fR] [\fIformula\fR|\fIcask\fR]" Display Homebrew\'s download cache\. See also \fBHOMEBREW_CACHE\fR\. .