From 9cd5078efbf273a3d3d22323cf6256c16c94d129 Mon Sep 17 00:00:00 2001 From: Ruoyu Zhong Date: Sun, 28 Apr 2024 11:25:06 +0800 Subject: [PATCH] cmd/tab: new command Add `brew tab`, a new command to edit tab information, as previously discussed in https://github.com/Homebrew/brew/pull/17125#issuecomment-2068473483. Currently, this supports marking or unmarking formulae as installed on request. Sample usage: $ brew tab --installed-on-request curl ==> curl is now marked as installed on request. $ brew autoremove --dry-run [no output] $ brew tab --no-installed-on-request curl ==> curl is now marked as not installed on request. $ brew autoremove --dry-run ==> Would autoremove 2 unneeded formulae: curl rtmpdump Co-authored-by: Mike McQuaid --- Library/Homebrew/cmd/tab.rb | 75 +++++++++++++++++++ .../sorbet/rbi/dsl/homebrew/cmd/tab_cmd.rbi | 19 +++++ Library/Homebrew/test/cmd/tab_spec.rb | 33 ++++++++ .../spec/shared_context/integration_test.rb | 21 +++++- completions/bash/brew | 20 +++++ completions/fish/brew.fish | 10 +++ completions/internal_commands_list.txt | 1 + completions/zsh/_brew | 14 ++++ docs/Manpage.md | 17 +++++ manpages/brew.1 | 10 +++ 10 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 Library/Homebrew/cmd/tab.rb create mode 100644 Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/tab_cmd.rbi create mode 100644 Library/Homebrew/test/cmd/tab_spec.rb diff --git a/Library/Homebrew/cmd/tab.rb b/Library/Homebrew/cmd/tab.rb new file mode 100644 index 0000000000..70b2aa261c --- /dev/null +++ b/Library/Homebrew/cmd/tab.rb @@ -0,0 +1,75 @@ +# typed: strict +# frozen_string_literal: true + +require "abstract_command" +require "formula" +require "tab" + +module Homebrew + module Cmd + class TabCmd < AbstractCommand + cmd_args do + description <<~EOS + Edit tab information for installed formulae. + + This can be useful when you want to control whether an installed + formula should be removed by `brew autoremove`. + To prevent removal, mark the formula as installed on request; + to allow removal, mark the formula as not installed on request. + EOS + + switch "--installed-on-request", + description: "Mark as installed on request." + switch "--no-installed-on-request", + description: "Mark as not installed on request." + + conflicts "--installed-on-request", "--no-installed-on-request" + + named_args :formula, min: 1 + end + + sig { override.void } + def run + installed_on_request = if args.installed_on_request? + true + elsif args.no_installed_on_request? + false + end + raise UsageError, "No marking option specified." if installed_on_request.nil? + + formulae = args.named.to_formulae + if (formulae_not_installed = formulae.reject(&:any_version_installed?)).any? + formula_names = formulae_not_installed.map(&:name) + is_or_are = (formula_names.length == 1) ? "is" : "are" + odie "#{formula_names.to_sentence} #{is_or_are} not installed." + end + + formulae.each do |formula| + update_tab formula, installed_on_request: + end + end + + private + + sig { params(formula: Formula, installed_on_request: T::Boolean).void } + def update_tab(formula, installed_on_request:) + tab = Tab.for_formula(formula) + unless tab.tabfile.exist? + raise ArgumentError, + "Tab file for #{formula.name} does not exist." + end + + installed_on_request_str = "#{"not " unless installed_on_request}installed on request" + if (tab.installed_on_request && installed_on_request) || + (!tab.installed_on_request && !installed_on_request) + ohai "#{formula.name} is already marked as #{installed_on_request_str}." + return + end + + tab.installed_on_request = installed_on_request + tab.write + ohai "#{formula.name} is now marked as #{installed_on_request_str}." + end + end + end +end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/tab_cmd.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/tab_cmd.rbi new file mode 100644 index 0000000000..e901f5eaeb --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/tab_cmd.rbi @@ -0,0 +1,19 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Homebrew::Cmd::TabCmd`. +# Please instead update this file by running `bin/tapioca dsl Homebrew::Cmd::TabCmd`. + + +class Homebrew::Cmd::TabCmd + sig { returns(Homebrew::Cmd::TabCmd::Args) } + def args; end +end + +class Homebrew::Cmd::TabCmd::Args < Homebrew::CLI::Args + sig { returns(T::Boolean) } + def installed_on_request?; end + + sig { returns(T::Boolean) } + def no_installed_on_request?; end +end diff --git a/Library/Homebrew/test/cmd/tab_spec.rb b/Library/Homebrew/test/cmd/tab_spec.rb new file mode 100644 index 0000000000..db67e2ed9b --- /dev/null +++ b/Library/Homebrew/test/cmd/tab_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "cmd/tab" +require "cmd/shared_examples/args_parse" +require "tab" + +RSpec.describe Homebrew::Cmd::TabCmd do + def installed_on_request?(formula) + # `brew` subprocesses can change the tab, invalidating the cached values. + Tab.clear_cache + Tab.for_formula(formula).installed_on_request + end + + it_behaves_like "parseable arguments" + + it "marks or unmarks a formula as installed on request", :integration_test do + setup_test_formula "foo", + tab_attributes: { "installed_on_request" => false } + foo = Formula["foo"] + + expect { brew "tab", "--installed-on-request", "foo" } + .to be_a_success + .and output(/foo is now marked as installed on request/).to_stdout + .and not_to_output.to_stderr + expect(installed_on_request?(foo)).to be true + + expect { brew "tab", "--no-installed-on-request", "foo" } + .to be_a_success + .and output(/foo is now marked as not installed on request/).to_stdout + .and not_to_output.to_stderr + expect(installed_on_request?(foo)).to be false + end +end diff --git a/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb b/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb index fbc361dd0e..c11c4e4d4d 100644 --- a/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb +++ b/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb @@ -129,7 +129,8 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin end end - def setup_test_formula(name, content = nil, tap: CoreTap.instance, bottle_block: nil) + def setup_test_formula(name, content = nil, tap: CoreTap.instance, + bottle_block: nil, tab_attributes: nil) case name when /^testball/ tarball = if OS.linux? @@ -174,8 +175,8 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin RUBY end - Formulary.find_formula_in_tap(name.downcase, tap).tap do |formula_path| - formula_path.write <<~RUBY + formula_path = Formulary.find_formula_in_tap(name.downcase, tap).tap do |path| + path.write <<~RUBY class #{Formulary.class_s(name)} < Formula #{content.gsub(/^(?!$)/, " ")} end @@ -183,6 +184,20 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin tap.clear_cache end + + return formula_path if tab_attributes.nil? + + keg = Formula[name].prefix + keg.mkpath + + tab = Tab.for_name(name) + tab.tabfile ||= keg/Tab::FILENAME + tab_attributes.each do |key, value| + tab.instance_variable_set(:"@#{key}", value) + end + tab.write + + formula_path end def install_test_formula(name, content = nil, build_bottle: false) diff --git a/completions/bash/brew b/completions/bash/brew index b3cd262559..d0dd3b1b88 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -2107,6 +2107,25 @@ _brew_style() { __brew_complete_casks } +_brew_tab() { + local cur="${COMP_WORDS[COMP_CWORD]}" + case "${cur}" in + -*) + __brewcomp " + --debug + --help + --installed-on-request + --no-installed-on-request + --quiet + --verbose + " + return + ;; + *) ;; + esac + __brew_complete_formulae +} + _brew_tap() { local cur="${COMP_WORDS[COMP_CWORD]}" case "${cur}" in @@ -2765,6 +2784,7 @@ _brew() { search) _brew_search ;; sh) _brew_sh ;; style) _brew_style ;; + tab) _brew_tab ;; tap) _brew_tap ;; tap-info) _brew_tap_info ;; tap-new) _brew_tap_new ;; diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index b00bd62b4e..7dc4fddc68 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -1413,6 +1413,16 @@ __fish_brew_complete_arg 'style; and not __fish_seen_argument -l cask -l casks' __fish_brew_complete_arg 'style; and not __fish_seen_argument -l formula -l formulae' -a '(__fish_brew_suggest_casks_all)' +__fish_brew_complete_cmd 'tab' 'Edit tab information for installed formulae' +__fish_brew_complete_arg 'tab' -l debug -d 'Display any debugging information' +__fish_brew_complete_arg 'tab' -l help -d 'Show this message' +__fish_brew_complete_arg 'tab' -l installed-on-request -d 'Mark formula as installed on request' +__fish_brew_complete_arg 'tab' -l no-installed-on-request -d 'Mark formula as not installed on request' +__fish_brew_complete_arg 'tab' -l quiet -d 'Make some output more quiet' +__fish_brew_complete_arg 'tab' -l verbose -d 'Make some output more verbose' +__fish_brew_complete_arg 'tab' -a '(__fish_brew_suggest_formulae_all)' + + __fish_brew_complete_cmd 'tap' 'Tap a formula repository' __fish_brew_complete_arg 'tap' -l custom-remote -d 'Install or change a tap with a custom remote. Useful for mirrors' __fish_brew_complete_arg 'tap' -l debug -d 'Display any debugging information' diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index 6c5b7423f3..58ca1ea8c5 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -90,6 +90,7 @@ setup-ruby sh shellenv style +tab tap tap-info tap-new diff --git a/completions/zsh/_brew b/completions/zsh/_brew index 0a35bc8922..2ff535c60b 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -210,6 +210,7 @@ __brew_internal_commands() { 'sh:Enter an interactive shell for Homebrew'\''s build environment' 'shellenv:Print export statements' 'style:Check formulae or files for conformance to Homebrew style guidelines' + 'tab:Edit tab information for installed formulae' 'tap:Tap a formula repository' 'tap-info:Show detailed information about one or more taps' 'tap-new:Generate the template files for a new tap' @@ -1747,6 +1748,19 @@ _brew_style() { '*::cask:__brew_casks' } +# brew tab +_brew_tab() { + _arguments \ + '--debug[Display any debugging information]' \ + '--help[Show this message]' \ + '(--no-installed-on-request)--installed-on-request[Mark formula as installed on request]' \ + '(--installed-on-request)--no-installed-on-request[Mark formula as not installed on request]' \ + '--quiet[Make some output more quiet]' \ + '--verbose[Make some output more verbose]' \ + - formula \ + '*::formula:__brew_formulae' +} + # brew tap _brew_tap() { _arguments \ diff --git a/docs/Manpage.md b/docs/Manpage.md index 1455fb3531..a26e4165a8 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -1162,6 +1162,23 @@ evaluation of this command's output to your dotfiles (e.g. `~/.bash_profile` or The shell can be specified explicitly with a supported shell name parameter. Unknown shells will output POSIX exports. +### `tab` \[`--installed-on-request`\] \[`--no-installed-on-request`\] *`formula`* \[...\] + +Edit tab information for installed formulae. + +This can be useful when you want to control whether an installed formula should +be removed by `brew autoremove`. To prevent removal, mark the formula as +installed on request; to allow removal, mark the formula as not installed on +request. + +`--installed-on-request` + +: Mark *`formula`* as installed on request. + +`--no-installed-on-request` + +: Mark *`formula`* as not installed on request. + ### `tap` \[*`options`*\] \[*`user`*`/`*`repo`*\] \[*`URL`*\] Tap a formula repository. If no arguments are provided, list all installed taps. diff --git a/manpages/brew.1 b/manpages/brew.1 index 54a0888d94..b0207e1d06 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -727,6 +727,16 @@ Print export statements\. When run in a shell, this installation of Homebrew wil The variables \fBHOMEBREW_PREFIX\fP, \fBHOMEBREW_CELLAR\fP and \fBHOMEBREW_REPOSITORY\fP are also exported to avoid querying them multiple times\. To help guarantee idempotence, this command produces no output when Homebrew\[u2019]s \fBbin\fP and \fBsbin\fP directories are first and second respectively in your \fBPATH\fP\&\. Consider adding evaluation of this command\[u2019]s output to your dotfiles (e\.g\. \fB~/\.bash_profile\fP or \fB~/\.zprofile\fP on macOS and \fB~/\.bashrc\fP or \fB~/\.zshrc\fP on Linux) with: \fBeval "$(brew shellenv)"\fP .P The shell can be specified explicitly with a supported shell name parameter\. Unknown shells will output POSIX exports\. +.SS "\fBtab\fP \fR[\fB\-\-installed\-on\-request\fP] \fR[\fB\-\-no\-installed\-on\-request\fP] \fIformula\fP \fR[\.\.\.]" +Edit tab information for installed formulae\. +.P +This can be useful when you want to control whether an installed formula should be removed by \fBbrew autoremove\fP\&\. To prevent removal, mark the formula as installed on request; to allow removal, mark the formula as not installed on request\. +.TP +\fB\-\-installed\-on\-request\fP +Mark \fIformula\fP as installed on request\. +.TP +\fB\-\-no\-installed\-on\-request\fP +Mark \fIformula\fP as not installed on request\. .SS "\fBtap\fP \fR[\fIoptions\fP] \fR[\fIuser\fP\fB/\fP\fIrepo\fP] \fR[\fIURL\fP]" Tap a formula repository\. If no arguments are provided, list all installed taps\. .P