Add PowerShell (pwsh) completion support

Resolves #19403
This commit is contained in:
Heath Stewart 2025-03-02 18:17:38 -08:00
parent 64efed206d
commit 42caf20fa4
No known key found for this signature in database
8 changed files with 63 additions and 11 deletions

View File

@ -29,7 +29,7 @@ class Caveats
end
caveats << keg_only_text
valid_shells = [:bash, :zsh, :fish].freeze
valid_shells = [:bash, :zsh, :fish, :pwsh].freeze
current_shell = Utils::Shell.preferred || Utils::Shell.parent
shells = if current_shell.present? &&
(shell_sym = current_shell.to_sym) &&
@ -143,6 +143,11 @@ class Caveats
zsh #{installed.join(" and ")} have been installed to:
#{root_dir}/share/zsh/site-functions
EOS
when :pwsh
<<~EOS
PowerShell completion has been installed to:
#{root_dir}/share/pwsh/completions
EOS
end
end

View File

@ -1137,6 +1137,13 @@ class Formula
sig { returns(Pathname) }
def fish_completion = share/"fish/vendor_completions.d"
# The directory where formula's powershell completion files should be
# installed.
# This is symlinked into `HOMEBREW_PREFIX` after installation or with
# `brew link` for formulae that are not keg-only.
sig { returns(Pathname) }
def pwsh_completion = share/"pwsh/completions"
# The directory used for as the prefix for {#etc} and {#var} files on
# installation so, despite not being in `HOMEBREW_CELLAR`, they are installed
# there after pouring a bottle.
@ -1989,7 +1996,7 @@ class Formula
end
private :extract_macho_slice_from
# Generate shell completions for a formula for `bash`, `zsh` and `fish`, using the formula's executable.
# Generate shell completions for a formula for `bash`, `zsh`, `fish`, and `powershell`, using the formula's executable.
#
# ### Examples
#
@ -2003,6 +2010,8 @@ class Formula
# (zsh_completion/"_foo").write Utils.safe_popen_read({ "SHELL" => "zsh" }, bin/"foo", "completions", "zsh")
# (fish_completion/"foo.fish").write Utils.safe_popen_read({ "SHELL" => "fish" }, bin/"foo",
# "completions", "fish")
# (pwsh_completion/"foo").write Utils.safe_popen_read({ "SHELL" => "pwsh" }, bin/"foo",
# "completions", "powershell")
# ```
#
# Selecting shells and using a different `base_name`.
@ -2094,7 +2103,7 @@ class Formula
}
def generate_completions_from_executable(*commands,
base_name: nil,
shells: [:bash, :zsh, :fish],
shells: [:bash, :zsh, :fish, :pwsh],
shell_parameter_format: nil)
executable = commands.first.to_s
base_name ||= File.basename(executable) if executable.start_with?(bin.to_s, sbin.to_s)
@ -2104,28 +2113,31 @@ class Formula
bash: bash_completion/base_name,
zsh: zsh_completion/"_#{base_name}",
fish: fish_completion/"#{base_name}.fish",
pwsh: pwsh_completion/"#{base_name}.ps1",
}
shells.each do |shell|
popen_read_env = { "SHELL" => shell.to_s }
script_path = completion_script_path_map[shell]
# Go's cobra and Rust's clap accept "powershell".
shell_argument = shell == :pwsh ? "powershell" : shell.to_s
shell_parameter = if shell_parameter_format.nil?
shell.to_s
shell_argument.to_s
elsif shell_parameter_format == :flag
"--#{shell}"
"--#{shell_argument}"
elsif shell_parameter_format == :arg
"--shell=#{shell}"
"--shell=#{shell_argument}"
elsif shell_parameter_format == :none
nil
elsif shell_parameter_format == :click
prog_name = File.basename(executable).upcase.tr("-", "_")
popen_read_env["_#{prog_name}_COMPLETE"] = "#{shell}_source"
popen_read_env["_#{prog_name}_COMPLETE"] = "#{shell_argument}_source"
nil
elsif shell_parameter_format == :clap
popen_read_env["COMPLETE"] = shell.to_s
popen_read_env["COMPLETE"] = shell_argument.to_s
nil
else
"#{shell_parameter_format}#{shell}"
"#{shell_parameter_format}#{shell_argument}"
end
popen_read_args = %w[]

View File

@ -147,6 +147,7 @@ class Keg
share/man/man1 share/man/man2 share/man/man3 share/man/man4
share/man/man5 share/man/man6 share/man/man7 share/man/man8
share/zsh share/zsh/site-functions
share/pwsh share/pwsh/completions
var/log
].map { |dir| HOMEBREW_PREFIX/dir } + must_exist_subdirectories + [
HOMEBREW_CACHE,
@ -354,6 +355,7 @@ class Keg
when :zsh
dir = path/"share/zsh/site-functions"
dir if dir.directory? && dir.children.any? { |f| f.basename.to_s.start_with?("_") }
when :pwsh then path/"share/pwsh/completions"
end
dir&.directory? && !dir.children.empty?
end

View File

@ -533,7 +533,7 @@ module RuboCop
correctable_shell_completion_node(install) do |node, shell, base_name, executable, subcmd, shell_parameter|
# generate_completions_from_executable only applicable if shell is passed
next unless shell_parameter.match?(/(bash|zsh|fish)/)
next unless shell_parameter.match?(/(bash|zsh|fish|pwsh)/)
base_name = base_name.delete_prefix("_").delete_suffix(".fish")
shell = shell.to_s.delete_suffix("_completion").to_sym
@ -541,6 +541,7 @@ module RuboCop
.delete_suffix("bash")
.delete_suffix("zsh")
.delete_suffix("fish")
.delete_suffix("pwsh")
shell_parameter_format = if shell_parameter_stripped.empty?
nil
elsif shell_parameter_stripped == "--"

View File

@ -248,6 +248,7 @@ RSpec.describe Caveats do
let(:bash_completion_dir) { path/"etc/bash_completion.d" }
let(:fish_vendor_completions) { path/"share/fish/vendor_completions.d" }
let(:zsh_site_functions) { path/"share/zsh/site-functions" }
let(:pwsh_completion_dir) { path/"share/pwsh/completions" }
before do
# don't try to load/fetch gcc/glibc
@ -274,6 +275,12 @@ RSpec.describe Caveats do
FileUtils.touch zsh_site_functions/f.name
expect(caveats).to include(HOMEBREW_PREFIX/"share/zsh/site-functions")
end
it "includes where pwsh completions have been installed to" do
pwsh_completion_dir.mkpath
FileUtils.touch pwsh_completion_dir/f.name
expect(caveats).to include(HOMEBREW_PREFIX/"share/pwsh/completions")
end
end
end
end

View File

@ -35,6 +35,11 @@ RSpec.describe Utils::Shell do
ENV["SHELL"] = "/bin/ksh"
expect(described_class.profile).to eq("~/.kshrc")
end
it "returns ~/.config/powershell/Microsoft.PowerShell_profile.ps1 for PowerShell" do
ENV["SHELL"] = "/usr/bin/pwsh"
expect(described_class.profile).to eq("~/.config/powershell/Microsoft.PowerShell_profile.ps1")
end
end
describe "::from_path" do

View File

@ -17,7 +17,7 @@ module Utils
shell_name = File.basename(path)
# handle possible version suffix like `zsh-5.2`
shell_name.sub!(/-.*\z/m, "")
shell_name.to_sym if %w[bash csh fish ksh mksh rc sh tcsh zsh].include?(shell_name)
shell_name.to_sym if %w[bash csh fish ksh mksh pwsh rc sh tcsh zsh].include?(shell_name)
end
sig { params(default: String).returns(String) }
@ -60,6 +60,9 @@ module Utils
when :bash
bash_profile = "#{Dir.home}/.bash_profile"
return bash_profile if File.exist? bash_profile
when :pwsh
pwsh_profile = "#{Dir.home}/.config/powershell/Microsoft.PowerShell_profile.ps1"
return pwsh_profile if File.exist? pwsh_profile
when :rc
rc_profile = "#{Dir.home}/.rcrc"
return rc_profile if File.exist? rc_profile
@ -78,6 +81,8 @@ module Utils
case preferred
when :bash, :ksh, :sh, :zsh, nil
"echo 'export #{variable}=#{sh_quote(value)}' >> #{profile}"
when :pwsh
"$env:#{variable}='#{value}' >> #{profile}"
when :rc
"echo '#{variable}=(#{sh_quote(value)})' >> #{profile}"
when :csh, :tcsh
@ -92,6 +97,8 @@ module Utils
case preferred
when :bash, :ksh, :mksh, :sh, :zsh, nil
"echo 'export PATH=\"#{sh_quote(path)}:$PATH\"' >> #{profile}"
when :pwsh
"$env:PATH = '#{path}' + \":${env:PATH}\" >> #{profile}"
when :rc
"echo 'path=(#{sh_quote(path)} $path)' >> #{profile}"
when :csh, :tcsh
@ -108,6 +115,7 @@ module Utils
fish: "~/.config/fish/config.fish",
ksh: "~/.kshrc",
mksh: "~/.kshrc",
pwsh: "~/.config/powershell/Microsoft.PowerShell_profile.ps1",
rc: "~/.rcrc",
sh: "~/.profile",
tcsh: "~/.tcshrc",

View File

@ -74,3 +74,15 @@ if test -d (brew --prefix)"/share/fish/vendor_completions.d"
set -p fish_complete_path (brew --prefix)/share/fish/vendor_completions.d
end
```
## Configuring Completions in `pwsh`
To make Homebrew's completions available in `pwsh` (PowerShell), you must source the definitions as part of your shell's startup. Add the following to your `$PROFILE`, for example: `~/.config/powershell/Microsoft.PowerShell_profile.ps1`:
```pwsh
if ((Get-Command brew) -and (Test-Path ($completions = "$(brew --prefix)/share/pwsh/completions"))) {
foreach ($f in Get-ChildItem -Path $completions -File) {
. $f
}
}
```