diff --git a/Library/Homebrew/brew.rb b/Library/Homebrew/brew.rb index ca712613e4..a6299cdc44 100644 --- a/Library/Homebrew/brew.rb +++ b/Library/Homebrew/brew.rb @@ -85,16 +85,14 @@ begin ENV["PATH"] = path require "commands" + require "settings" if cmd internal_cmd = Commands.valid_internal_cmd?(cmd) internal_cmd ||= begin internal_dev_cmd = Commands.valid_internal_dev_cmd?(cmd) if internal_dev_cmd && !Homebrew::EnvConfig.developer? - if (HOMEBREW_REPOSITORY/".git/config").exist? - system "git", "config", "--file=#{HOMEBREW_REPOSITORY}/.git/config", - "--replace-all", "homebrew.devcmdrun", "true" - end + Homebrew::Settings.write "devcmdrun", true ENV["HOMEBREW_DEV_CMD_RUN"] = "1" end internal_dev_cmd diff --git a/Library/Homebrew/cmd/completions.rb b/Library/Homebrew/cmd/completions.rb new file mode 100644 index 0000000000..3aad3aa9c8 --- /dev/null +++ b/Library/Homebrew/cmd/completions.rb @@ -0,0 +1,52 @@ +# typed: true +# frozen_string_literal: true + +require "cli/parser" +require "completions" + +module Homebrew + extend T::Sig + + module_function + + sig { returns(CLI::Parser) } + def completions_args + Homebrew::CLI::Parser.new do + usage_banner <<~EOS + `completions` [] + + Control whether Homebrew automatically links external tap shell completion files. + Read more at . + + `brew completions` [`state`]: + Display the current state of Homebrew's completions. + + `brew completions` (`link`|`unlink`): + Link or unlink Homebrew's completions. + EOS + + max_named 1 + end + end + + def completions + args = completions_args.parse + + case args.named.first + when nil, "state" + if Completions.link_completions? + puts "Completions are linked." + else + puts "Completions are not linked." + end + when "link" + Completions.link! + puts "Completions are now linked." + when "unlink" + Completions.unlink! + puts "Completions are no longer linked." + else + raise UsageError, "unknown subcommand: #{args.named.first}" + end + end +end diff --git a/Library/Homebrew/cmd/update-report.rb b/Library/Homebrew/cmd/update-report.rb index f38b341394..3a91124812 100644 --- a/Library/Homebrew/cmd/update-report.rb +++ b/Library/Homebrew/cmd/update-report.rb @@ -8,6 +8,7 @@ require "descriptions" require "cleanup" require "description_cache_store" require "cli/parser" +require "settings" module Homebrew extend T::Sig @@ -63,16 +64,12 @@ module Homebrew Utils::Analytics.messages_displayed! if $stdout.tty? end - HOMEBREW_REPOSITORY.cd do - donation_message_displayed = - Utils.popen_read("git", "config", "--get", "homebrew.donationmessage").chomp == "true" - if !donation_message_displayed && !args.quiet? - ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:" - puts " #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n" + if Settings.read("donationmessage") != "true" && !args.quiet? + ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:" + puts " #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n" - # Consider the message possibly missed if not a TTY. - safe_system "git", "config", "--replace-all", "homebrew.donationmessage", "true" if $stdout.tty? - end + # Consider the message possibly missed if not a TTY. + Settings.write "donationmessage", true if $stdout.tty? end install_core_tap_if_necessary @@ -89,19 +86,14 @@ module Homebrew puts "Updated Homebrew from #{shorten_revision(initial_revision)} to #{shorten_revision(current_revision)}." updated = true - old_tag = if (HOMEBREW_REPOSITORY/".git/config").exist? - Utils.popen_read( - "git", "config", "--file=#{HOMEBREW_REPOSITORY}/.git/config", "--get", "homebrew.latesttag" - ).chomp.presence - end + old_tag = Settings.read "latesttag" new_tag = Utils.popen_read( "git", "-C", HOMEBREW_REPOSITORY, "tag", "--list", "--sort=-version:refname", "*.*" ).lines.first.chomp if new_tag != old_tag - system "git", "config", "--file=#{HOMEBREW_REPOSITORY}/.git/config", - "--replace-all", "homebrew.latesttag", new_tag + Settings.write "latesttag", new_tag new_repository_version = new_tag end end diff --git a/Library/Homebrew/completions.rb b/Library/Homebrew/completions.rb new file mode 100644 index 0000000000..880ac0d8e4 --- /dev/null +++ b/Library/Homebrew/completions.rb @@ -0,0 +1,69 @@ +# typed: true +# frozen_string_literal: true + +require "utils/link" +require "settings" + +module Homebrew + # Helper functions for generating shell completions. + # + # @api private + module Completions + extend T::Sig + + module_function + + SHELLS = %w[bash fish zsh].freeze + + sig { void } + def link! + Settings.write :linkcompletions, true + Tap.each do |tap| + Utils::Link.link_completions tap.path, "brew completions link" + end + end + + sig { void } + def unlink! + Settings.write :linkcompletions, false + Tap.each do |tap| + next if tap.official? + + Utils::Link.unlink_completions tap.path + end + end + + sig { returns(T::Boolean) } + def link_completions? + Settings.read(:linkcompletions) == "true" + end + + sig { returns(T::Boolean) } + def completions_to_link? + Tap.each do |tap| + next if tap.official? + + SHELLS.each do |shell| + return true if (tap.path/"completions/#{shell}").exist? + end + end + + false + end + + sig { void } + def show_completions_message_if_needed + return if Settings.read(:completionsmessageshown) == "true" + return unless completions_to_link? + + ohai "Homebrew completions for external commands are unlinked by default!" + puts <<~EOS + To opt-in to automatically linking external tap shell competion files, run: + brew completions link + Then, follow the directions at #{Formatter.url("https://docs.brew.sh/Shell-Completion")} + EOS + + Settings.write :completionsmessageshown, true + end + end +end diff --git a/Library/Homebrew/completions.rbi b/Library/Homebrew/completions.rbi new file mode 100644 index 0000000000..8c4746bcaf --- /dev/null +++ b/Library/Homebrew/completions.rbi @@ -0,0 +1,7 @@ +# typed: strict + +module Homebrew + module Completions + include Kernel + end +end diff --git a/Library/Homebrew/settings.rb b/Library/Homebrew/settings.rb new file mode 100644 index 0000000000..30d5567bd5 --- /dev/null +++ b/Library/Homebrew/settings.rb @@ -0,0 +1,41 @@ +# typed: true +# frozen_string_literal: true + +module Homebrew + # Helper functions for reading and writing settings. + # + # @api private + module Settings + extend T::Sig + + module_function + + sig { params(setting: T.any(String, Symbol), repo: Pathname).returns(T.nilable(String)) } + def read(setting, repo: HOMEBREW_REPOSITORY) + return unless (repo/".git/config").exist? + + repo.cd do + Utils.popen_read("git", "config", "--get", "homebrew.#{setting}").chomp.presence + end + end + + sig { params(setting: T.any(String, Symbol), value: T.any(String, T::Boolean), repo: Pathname).void } + def write(setting, value, repo: HOMEBREW_REPOSITORY) + return unless (repo/".git/config").exist? + + repo.cd do + safe_system "git", "config", "--replace-all", "homebrew.#{setting}", value.to_s + end + end + + sig { params(setting: T.any(String, Symbol), repo: Pathname).void } + def delete(setting, repo: HOMEBREW_REPOSITORY) + return unless (repo/".git/config").exist? + return if read(setting, repo: repo).blank? + + repo.cd do + safe_system "git", "config", "--unset-all", "homebrew.#{setting}" + end + end + end +end diff --git a/Library/Homebrew/settings.rbi b/Library/Homebrew/settings.rbi new file mode 100644 index 0000000000..289622fd41 --- /dev/null +++ b/Library/Homebrew/settings.rbi @@ -0,0 +1,7 @@ +# typed: strict + +module Homebrew + module Settings + include Kernel + end +end diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index a5e580249c..2e75f812fa 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -2,8 +2,10 @@ # frozen_string_literal: true require "commands" +require "completions" require "extend/cachable" require "description_cache_store" +require "settings" # A {Tap} is used to extend the formulae provided by Homebrew core. # Usually, it's synced with a remote Git repository. And it's likely @@ -340,7 +342,13 @@ class Tap def link_completions_and_manpages command = "brew tap --repair" Utils::Link.link_manpages(path, command) - Utils::Link.link_completions(path, command) + + Homebrew::Completions.show_completions_message_if_needed + if official? || Homebrew::Completions.link_completions? + Utils::Link.link_completions(path, command) + else + Utils::Link.unlink_completions(path) + end end # Uninstall this {Tap}. @@ -814,18 +822,14 @@ class TapConfig return unless tap.git? return unless Utils::Git.available? - tap.path.cd do - Utils.popen_read("git", "config", "--get", "homebrew.#{key}").chomp.presence - end + Homebrew::Settings.read key, repo: tap.path end def []=(key, value) return unless tap.git? return unless Utils::Git.available? - tap.path.cd do - safe_system "git", "config", "--replace-all", "homebrew.#{key}", value.to_s - end + Homebrew::Settings.write key, value.to_s, repo: tap.path end end diff --git a/Library/Homebrew/test/cmd/completions_spec.rb b/Library/Homebrew/test/cmd/completions_spec.rb new file mode 100644 index 0000000000..721fa184ae --- /dev/null +++ b/Library/Homebrew/test/cmd/completions_spec.rb @@ -0,0 +1,22 @@ +# typed: false +# frozen_string_literal: true + +require "cmd/shared_examples/args_parse" + +describe "brew completions" do + describe "Homebrew.completions_args" do + it_behaves_like "parseable arguments" + end + + it "runs the status subcommand correctly", :integration_test do + HOMEBREW_REPOSITORY.cd do + system "git", "init" + end + + brew "completions", "link" + expect { brew "completions" } + .to output(/Completions are linked/).to_stdout + .and not_to_output.to_stderr + .and be_a_success + end +end diff --git a/Library/Homebrew/test/completions_spec.rb b/Library/Homebrew/test/completions_spec.rb new file mode 100644 index 0000000000..efdcee115e --- /dev/null +++ b/Library/Homebrew/test/completions_spec.rb @@ -0,0 +1,140 @@ +# typed: false +# frozen_string_literal: true + +require "completions" + +describe Homebrew::Completions do + let(:internal_path) { HOMEBREW_REPOSITORY/"Library/Taps/homebrew/homebrew-bar" } + let(:external_path) { HOMEBREW_REPOSITORY/"Library/Taps/foo/homebrew-bar" } + + before do + HOMEBREW_REPOSITORY.cd do + system "git", "init" + end + internal_path.mkpath + external_path.mkpath + end + + def setup_completions(external:) + (internal_path/"completions/bash/foo_internal").write "#foo completions" + if external + (external_path/"completions/bash/foo_external").write "#foo completions" + elsif (external_path/"completions/bash/foo_external").exist? + (external_path/"completions/bash/foo_external").delete + end + end + + def setup_completions_setting(state, setting: "linkcompletions") + HOMEBREW_REPOSITORY.cd do + system "git", "config", "--replace-all", "homebrew.#{setting}", state.to_s + end + end + + def read_completions_setting(setting: "linkcompletions") + HOMEBREW_REPOSITORY.cd do + Utils.popen_read("git", "config", "--get", "homebrew.#{setting}").chomp.presence + end + end + + def delete_completions_setting(setting: "linkcompletions") + HOMEBREW_REPOSITORY.cd do + system "git", "config", "--unset-all", "homebrew.#{setting}" + end + end + + after do + FileUtils.rm_rf internal_path + FileUtils.rm_rf external_path.dirname + end + + describe ".link!" do + it "sets homebrew.linkcompletions to true" do + setup_completions_setting false + expect { described_class.link! }.not_to raise_error + expect(read_completions_setting).to eq "true" + end + + it "sets homebrew.linkcompletions to true if unset" do + delete_completions_setting + expect { described_class.link! }.not_to raise_error + expect(read_completions_setting).to eq "true" + end + + it "keeps homebrew.linkcompletions set to true" do + setup_completions_setting true + expect { described_class.link! }.not_to raise_error + expect(read_completions_setting).to eq "true" + end + end + + describe ".unlink!" do + it "sets homebrew.linkcompletions to false" do + setup_completions_setting true + expect { described_class.unlink! }.not_to raise_error + expect(read_completions_setting).to eq "false" + end + + it "sets homebrew.linkcompletions to false if unset" do + delete_completions_setting + expect { described_class.unlink! }.not_to raise_error + expect(read_completions_setting).to eq "false" + end + + it "keeps homebrew.linkcompletions set to false" do + setup_completions_setting false + expect { described_class.unlink! }.not_to raise_error + expect(read_completions_setting).to eq "false" + end + end + + describe ".link_completions?" do + it "returns true if homebrew.linkcompletions is true" do + setup_completions_setting true + expect(described_class.link_completions?).to be true + end + + it "returns false if homebrew.linkcompletions is false" do + setup_completions_setting false + expect(described_class.link_completions?).to be false + end + + it "returns false if homebrew.linkcompletions is not set" do + expect(described_class.link_completions?).to be false + end + end + + describe ".completions_to_link?" do + it "returns false if only internal taps have completions" do + setup_completions external: false + expect(described_class.completions_to_link?).to be false + end + + it "returns true if external taps have completions" do + setup_completions external: true + expect(described_class.completions_to_link?).to be true + end + end + + describe ".show_completions_message_if_needed" do + it "doesn't show the message if there are no completions to link" do + setup_completions external: false + delete_completions_setting setting: :completionsmessageshown + expect { described_class.show_completions_message_if_needed }.not_to output.to_stdout + end + + it "doesn't show the message if there are completions to link but the message has already been shown" do + setup_completions external: true + setup_completions_setting true, setting: :completionsmessageshown + expect { described_class.show_completions_message_if_needed }.not_to output.to_stdout + end + + it "shows the message if there are completions to link and the message hasn't already been shown" do + setup_completions external: true + delete_completions_setting setting: :completionsmessageshown + + message = /Homebrew completions for external commands are unlinked by default!/ + expect { described_class.show_completions_message_if_needed } + .to output(message).to_stdout + end + end +end diff --git a/Library/Homebrew/test/settings_spec.rb b/Library/Homebrew/test/settings_spec.rb new file mode 100644 index 0000000000..8df4063fc2 --- /dev/null +++ b/Library/Homebrew/test/settings_spec.rb @@ -0,0 +1,74 @@ +# typed: false +# frozen_string_literal: true + +require "settings" + +describe Homebrew::Settings do + before do + HOMEBREW_REPOSITORY.cd do + system "git", "init" + end + end + + def setup_setting + HOMEBREW_REPOSITORY.cd do + system "git", "config", "--replace-all", "homebrew.foo", "true" + end + end + + describe ".read" do + it "returns the correct value for a setting" do + setup_setting + expect(described_class.read("foo")).to eq "true" + end + + it "returns the correct value for a setting as a symbol" do + setup_setting + expect(described_class.read(:foo)).to eq "true" + end + + it "returns nil when setting is not set" do + setup_setting + expect(described_class.read("bar")).to be_nil + end + + it "runs on a repo without a configuration file" do + expect { described_class.read("foo", repo: HOMEBREW_REPOSITORY/"bar") }.not_to raise_error + end + end + + describe ".write" do + it "writes over an existing value" do + setup_setting + described_class.write :foo, false + expect(described_class.read("foo")).to eq "false" + end + + it "writes a new value" do + setup_setting + described_class.write :bar, "abcde" + expect(described_class.read("bar")).to eq "abcde" + end + + it "returns if the repo doesn't have a configuration file" do + expect { described_class.write("foo", repo: HOMEBREW_REPOSITORY/"bar") }.not_to raise_error + end + end + + describe ".delete" do + it "deletes an existing setting" do + setup_setting + described_class.delete(:foo) + expect(described_class.read("foo")).to be_nil + end + + it "deletes a non-existing setting" do + setup_setting + expect { described_class.delete(:bar) }.not_to raise_error + end + + it "returns if the repo doesn't have a configuration file" do + expect { described_class.delete("foo", repo: HOMEBREW_REPOSITORY/"bar") }.not_to raise_error + end + end +end diff --git a/Library/Homebrew/test/tap_spec.rb b/Library/Homebrew/test/tap_spec.rb index 3a5ddf8197..691e5fa61c 100644 --- a/Library/Homebrew/test/tap_spec.rb +++ b/Library/Homebrew/test/tap_spec.rb @@ -84,6 +84,14 @@ describe Tap do end end + def setup_completion(link:) + HOMEBREW_REPOSITORY.cd do + system "git", "init" + system "git", "config", "--replace-all", "homebrew.linkcompletions", link.to_s + system "git", "config", "--replace-all", "homebrew.completionsmessageshown", "true" + end + end + specify "::fetch" do expect(described_class.fetch("Homebrew", "core")).to be_kind_of(CoreTap) expect(described_class.fetch("Homebrew", "homebrew")).to be_kind_of(CoreTap) @@ -285,6 +293,7 @@ describe Tap do specify "#install and #uninstall" do setup_tap_files setup_git_repo + setup_completion link: true tap = described_class.new("Homebrew", "bar") @@ -308,9 +317,49 @@ describe Tap do (HOMEBREW_PREFIX/"share").rmtree if (HOMEBREW_PREFIX/"share").exist? end - specify "#link_completions_and_manpages" do + specify "#link_completions_and_manpages when completions are enabled for non-official tap" do setup_tap_files setup_git_repo + setup_completion link: true + tap = described_class.new("NotHomebrew", "baz") + tap.install clone_target: subject.path/".git" + (HOMEBREW_PREFIX/"share/man/man1/brew-tap-cmd.1").delete + (HOMEBREW_PREFIX/"etc/bash_completion.d/brew-tap-cmd").delete + (HOMEBREW_PREFIX/"share/zsh/site-functions/_brew-tap-cmd").delete + (HOMEBREW_PREFIX/"share/fish/vendor_completions.d/brew-tap-cmd.fish").delete + tap.link_completions_and_manpages + expect(HOMEBREW_PREFIX/"share/man/man1/brew-tap-cmd.1").to be_a_file + expect(HOMEBREW_PREFIX/"etc/bash_completion.d/brew-tap-cmd").to be_a_file + expect(HOMEBREW_PREFIX/"share/zsh/site-functions/_brew-tap-cmd").to be_a_file + expect(HOMEBREW_PREFIX/"share/fish/vendor_completions.d/brew-tap-cmd.fish").to be_a_file + tap.uninstall + ensure + (HOMEBREW_PREFIX/"etc").rmtree if (HOMEBREW_PREFIX/"etc").exist? + (HOMEBREW_PREFIX/"share").rmtree if (HOMEBREW_PREFIX/"share").exist? + end + + specify "#link_completions_and_manpages when completions are disabled for non-official tap" do + setup_tap_files + setup_git_repo + setup_completion link: false + tap = described_class.new("NotHomebrew", "baz") + tap.install clone_target: subject.path/".git" + (HOMEBREW_PREFIX/"share/man/man1/brew-tap-cmd.1").delete + tap.link_completions_and_manpages + expect(HOMEBREW_PREFIX/"share/man/man1/brew-tap-cmd.1").to be_a_file + expect(HOMEBREW_PREFIX/"etc/bash_completion.d/brew-tap-cmd").not_to be_a_file + expect(HOMEBREW_PREFIX/"share/zsh/site-functions/_brew-tap-cmd").not_to be_a_file + expect(HOMEBREW_PREFIX/"share/fish/vendor_completions.d/brew-tap-cmd.fish").not_to be_a_file + tap.uninstall + ensure + (HOMEBREW_PREFIX/"etc").rmtree if (HOMEBREW_PREFIX/"etc").exist? + (HOMEBREW_PREFIX/"share").rmtree if (HOMEBREW_PREFIX/"share").exist? + end + + specify "#link_completions_and_manpages when completions are enabled for official tap" do + setup_tap_files + setup_git_repo + setup_completion link: false tap = described_class.new("Homebrew", "baz") tap.install clone_target: subject.path/".git" (HOMEBREW_PREFIX/"share/man/man1/brew-tap-cmd.1").delete diff --git a/Library/Homebrew/utils/analytics.rb b/Library/Homebrew/utils/analytics.rb index 6dca9a2001..3d20136514 100644 --- a/Library/Homebrew/utils/analytics.rb +++ b/Library/Homebrew/utils/analytics.rb @@ -3,6 +3,7 @@ require "context" require "erb" +require "settings" module Utils # Helper module for fetching and reporting analytics data. @@ -102,27 +103,27 @@ module Utils end def uuid - config_get(:analyticsuuid) + Homebrew::Settings.read :analyticsuuid end def messages_displayed! - config_set(:analyticsmessage, true) - config_set(:caskanalyticsmessage, true) + Homebrew::Settings.write :analyticsmessage, true + Homebrew::Settings.write :caskanalyticsmessage, true end def enable! - config_set(:analyticsdisabled, false) + Homebrew::Settings.write :analyticsdisabled, false messages_displayed! end def disable! - config_set(:analyticsdisabled, true) + Homebrew::Settings.write :analyticsdisabled, true regenerate_uuid! end def regenerate_uuid! # it will be regenerated in next run unless disabled. - config_delete(:analyticsuuid) + Homebrew::Settings.delete :analyticsuuid end def output(args:, filter: nil) @@ -313,25 +314,7 @@ module Utils end def config_true?(key) - config_get(key) == "true" - end - - def config_get(key) - HOMEBREW_REPOSITORY.cd do - Utils.popen_read("git", "config", "--get", "homebrew.#{key}").chomp - end - end - - def config_set(key, value) - HOMEBREW_REPOSITORY.cd do - safe_system "git", "config", "--replace-all", "homebrew.#{key}", value.to_s - end - end - - def config_delete(key) - HOMEBREW_REPOSITORY.cd do - system "git", "config", "--unset-all", "homebrew.#{key}" - end + Homebrew::Settings.read(key) == "true" end def formulae_brew_sh_json(endpoint) diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index f1a952a9e1..13e278cc73 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -25,6 +25,7 @@ cat cleanup command commands +completions config configure create diff --git a/docs/Manpage.md b/docs/Manpage.md index bda3f0502c..7061af5693 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -94,6 +94,17 @@ Show lists of built-in and external commands. * `--include-aliases`: Include aliases of internal commands. +### `completions` [*`subcommand`*] + +Control whether Homebrew automatically links external tap shell completion files. +Read more at . + +`brew completions` [`state`] +
Display the current state of Homebrew's completions. + +`brew completions` (`link`|`unlink`) +
Link or unlink Homebrew's completions. + ### `config` Show Homebrew and system configuration info useful for debugging. If you file diff --git a/docs/Shell-Completion.md b/docs/Shell-Completion.md index bb5aa592f5..b68d23744b 100644 --- a/docs/Shell-Completion.md +++ b/docs/Shell-Completion.md @@ -4,7 +4,9 @@ Homebrew comes with completion definitions for the `brew` command. Some packages `zsh`, `bash` and `fish` are currently supported. -You must configure your shell to enable its completion support. This is because the Homebrew-managed completions are stored under `HOMEBREW_PREFIX` which your system shell may not be aware of, and since it is difficult to automatically configure `bash` and `zsh` completions in a robust manner, the Homebrew installer does not do it for you. +You must then configure your shell to enable its completion support. This is because the Homebrew-managed completions are stored under `HOMEBREW_PREFIX` which your system shell may not be aware of, and since it is difficult to automatically configure `bash` and `zsh` completions in a robust manner, the Homebrew installer does not do it for you. + +Shell completions for external Homebrew commands are not automatically installed. To opt-in to using completions for external commands (if provided), they need to be linked to `HOMEBREW_PREFIX` by running `brew completions link`. ## Configuring Completions in `bash` diff --git a/manpages/brew.1 b/manpages/brew.1 index 09132afcc7..3734944d13 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -93,6 +93,17 @@ List only the names of commands without category headers\. \fB\-\-include\-aliases\fR Include aliases of internal commands\. . +.SS "\fBcompletions\fR [\fIsubcommand\fR]" +Control whether Homebrew automatically links external tap shell completion files\. Read more at \fIhttps://docs\.brew\.sh/Shell\-Completion\fR\. +. +.P +\fBbrew completions\fR [\fBstate\fR] + Display the current state of Homebrew\'s completions\. +. +.P +\fBbrew completions\fR (\fBlink\fR|\fBunlink\fR) + Link or unlink Homebrew\'s completions\. +. .SS "\fBconfig\fR" Show Homebrew and system configuration info useful for debugging\. If you file a bug report, you will be required to provide this information\. .