brew/Library/Homebrew/completions.rb

347 lines
12 KiB
Ruby
Raw Normal View History

2021-01-08 11:10:24 -05:00
# typed: true
# frozen_string_literal: true
require "utils/link"
require "settings"
require "erb"
2021-01-08 11:10:24 -05:00
module Homebrew
# Helper functions for generating shell completions.
#
# @api private
module Completions
extend T::Sig
2021-01-08 11:10:24 -05:00
module_function
2021-01-08 11:10:24 -05:00
COMPLETIONS_DIR = (HOMEBREW_REPOSITORY/"completions").freeze
TEMPLATE_DIR = (HOMEBREW_LIBRARY_PATH/"completions").freeze
SHELLS = %w[bash fish zsh].freeze
COMPLETIONS_EXCLUSION_LIST = %w[
instal
uninstal
update-report
].freeze
BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING = {
formula: "__brew_complete_formulae",
installed_formula: "__brew_complete_installed_formulae",
outdated_formula: "__brew_complete_outdated_formulae",
cask: "__brew_complete_casks",
installed_cask: "__brew_complete_installed_casks",
outdated_cask: "__brew_complete_outdated_casks",
tap: "__brew_complete_tapped",
installed_tap: "__brew_complete_tapped",
command: "__brew_complete_commands",
diagnostic_check: '__brewcomp "$(brew doctor --list-checks)"',
file: "__brew_complete_files",
}.freeze
2021-01-24 01:59:31 -05:00
ZSH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING = {
formula: "__brew_formulae",
installed_formula: "__brew_installed_formulae",
outdated_formula: "__brew_outdated_formulae",
cask: "__brew_casks",
installed_cask: "__brew_installed_casks",
outdated_cask: "__brew_outdated_casks",
tap: "__brew_any_tap",
installed_tap: "__brew_installed_taps",
command: "__brew_commands",
diagnostic_check: "__brew_diagnostic_checks",
file: "__brew_formulae_or_ruby_files",
}.freeze
2021-01-26 10:47:56 -05:00
FISH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING = {
formula: "__fish_brew_suggest_formulae_all",
installed_formula: "__fish_brew_suggest_formulae_installed",
outdated_formula: "__fish_brew_suggest_formulae_outdated",
cask: "__fish_brew_suggest_casks_all",
installed_cask: "__fish_brew_suggest_casks_installed",
outdated_cask: "__fish_brew_suggest_casks_outdated",
tap: "__fish_brew_suggest_taps_installed",
installed_tap: "__fish_brew_suggest_taps_installed",
command: "__fish_brew_suggest_commands",
diagnostic_check: "__fish_brew_suggest_diagnostic_checks",
}.freeze
sig { void }
def link!
Settings.write :linkcompletions, true
Tap.each do |tap|
Utils::Link.link_completions tap.path, "brew completions link"
end
end
2021-01-08 11:10:24 -05:00
sig { void }
def unlink!
Settings.write :linkcompletions, false
Tap.each do |tap|
next if tap.official?
Utils::Link.unlink_completions tap.path
end
end
2021-01-08 11:10:24 -05:00
sig { returns(T::Boolean) }
def link_completions?
Settings.read(:linkcompletions) == "true"
end
2021-01-08 11:10:24 -05:00
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
sig { void }
def update_shell_completions!
commands = Commands.commands(external: false, aliases: true).sort
2021-01-24 16:42:28 -05:00
puts "Writing completions to #{COMPLETIONS_DIR}"
(COMPLETIONS_DIR/"bash/brew").atomic_write generate_bash_completion_file(commands)
2021-01-24 01:59:31 -05:00
(COMPLETIONS_DIR/"zsh/_brew").atomic_write generate_zsh_completion_file(commands)
2021-01-26 10:47:56 -05:00
(COMPLETIONS_DIR/"fish/brew.fish").atomic_write generate_fish_completion_file(commands)
end
sig { params(command: String).returns(T::Boolean) }
def command_gets_completions?(command)
command_options(command).any?
end
2021-01-26 10:47:56 -05:00
sig { params(description: String, fish: T::Boolean).returns(String) }
def format_description(description, fish: false)
description = if fish
description.gsub("'", "\\\\'")
else
description.gsub("'", "'\\\\''")
end
description.gsub(/[<>]/, "").tr("\n", " ").chomp(".")
2021-01-24 01:59:31 -05:00
end
2021-01-24 03:12:52 -05:00
sig { params(command: String).returns(T::Hash[String, String]) }
def command_options(command)
2021-01-24 03:12:52 -05:00
options = {}
Commands.command_options(command)&.each do |option|
next if option.blank?
name = option.first
2021-01-24 16:42:28 -05:00
desc = option.second
if name.start_with? "--[no-]"
2021-01-24 03:12:52 -05:00
options[name.remove("[no-]")] = desc
options[name.sub("[no-]", "no-")] = desc
else
2021-01-24 03:12:52 -05:00
options[name] = desc
end
2021-01-24 03:12:52 -05:00
end
options
end
sig { params(command: String).returns(T.nilable(String)) }
def generate_bash_subcommand_completion(command)
return unless command_gets_completions? command
named_completion_string = ""
if (types = Commands.named_args_type(command))
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
named_args_types.each do |type|
next unless BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING.key? type
named_completion_string += "\n #{BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]}"
end
named_completion_string += "\n __brewcomp \"#{named_args_strings.join(" ")}\"" if named_args_strings.any?
end
<<~COMPLETION
_brew_#{Commands.method_name command}() {
local cur="${COMP_WORDS[COMP_CWORD]}"
case "$cur" in
-*)
__brewcomp "
2021-01-24 03:12:52 -05:00
#{command_options(command).keys.sort.join("\n ")}
"
return
;;
esac#{named_completion_string}
}
COMPLETION
end
2021-01-24 01:59:31 -05:00
sig { params(commands: T::Array[String]).returns(String) }
def generate_bash_completion_file(commands)
variables = OpenStruct.new
variables[:completion_functions] = commands.map do |command|
generate_bash_subcommand_completion command
end.compact
variables[:function_mappings] = commands.map do |command|
next unless command_gets_completions? command
"#{command}) _brew_#{Commands.method_name command} ;;"
end.compact
ERB.new((TEMPLATE_DIR/"bash.erb").read, trim_mode: ">").result(variables.instance_eval { binding })
end
2021-01-24 01:59:31 -05:00
sig { params(command: String).returns(T.nilable(String)) }
def generate_zsh_subcommand_completion(command)
return unless command_gets_completions? command
options = command_options(command)
2021-01-24 01:59:31 -05:00
args_options = []
if (types = Commands.named_args_type(command))
2021-01-24 01:59:31 -05:00
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
named_args_types.each do |type|
next unless ZSH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING.key? type
args_options << "- #{type}"
opt = "--#{type}"
if options.key?(opt)
desc = options[opt]
if desc.blank?
args_options << opt
else
conflicts = generate_zsh_option_exclusions(command, opt)
args_options << "#{conflicts}#{opt}[#{format_description desc}]"
end
options.delete(opt)
end
args_options << "*::#{type}:#{ZSH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]}"
2021-01-24 01:59:31 -05:00
end
if named_args_strings.any?
args_options << "- subcommand"
args_options << "*::subcommand:(#{named_args_strings.join(" ")})"
end
end
options = options.sort.map do |opt, desc|
next opt if desc.blank?
conflicts = generate_zsh_option_exclusions(command, opt)
"#{conflicts}#{opt}[#{format_description desc}]"
2021-01-24 01:59:31 -05:00
end
options += args_options
2021-01-24 01:59:31 -05:00
<<~COMPLETION
# brew #{command}
_brew_#{Commands.method_name command}() {
_arguments \\
#{options.map! { |opt| opt.start_with?("- ") ? opt : "'#{opt}'" }.join(" \\\n ")}
2021-01-24 01:59:31 -05:00
}
COMPLETION
end
def generate_zsh_option_exclusions(command, option)
conflicts = Commands.option_conflicts(command, option.gsub(/^--/, ""))
return "" unless conflicts.presence
"(#{conflicts.map { |conflict| "--#{conflict}" }.join(" ")})"
end
2021-01-24 01:59:31 -05:00
sig { params(commands: T::Array[String]).returns(String) }
def generate_zsh_completion_file(commands)
variables = OpenStruct.new
variables[:aliases] = Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.map do |alias_command, command|
alias_command = "'#{alias_command}'" if alias_command.start_with? "-"
command = "'#{command}'" if command.start_with? "-"
"#{alias_command} #{command}"
end.compact
variables[:builtin_command_descriptions] = commands.map do |command|
next if Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.key? command
2021-01-24 16:42:28 -05:00
description = Commands.command_description(command, short: true)
2021-01-24 01:59:31 -05:00
next if description.blank?
2021-01-24 16:42:28 -05:00
description = format_description description
2021-01-24 01:59:31 -05:00
"'#{command}:#{description}'"
end.compact
variables[:completion_functions] = commands.map do |command|
generate_zsh_subcommand_completion command
end.compact
ERB.new((TEMPLATE_DIR/"zsh.erb").read, trim_mode: ">").result(variables.instance_eval { binding })
end
2021-01-26 10:47:56 -05:00
sig { params(command: String).returns(T.nilable(String)) }
def generate_fish_subcommand_completion(command)
return unless command_gets_completions? command
command_description = format_description Commands.command_description(command, short: true), fish: true
lines = ["__fish_brew_complete_cmd '#{command}' '#{command_description}'"]
options = command_options(command).sort.map do |opt, desc|
arg_line = "__fish_brew_complete_arg '#{command}' -l #{opt.sub(/^-+/, "")}"
arg_line += " -d '#{format_description desc, fish: true}'" if desc.present?
arg_line
end.compact
subcommands = []
named_args = []
if (types = Commands.named_args_type(command))
2021-01-26 10:47:56 -05:00
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
named_args_types.each do |type|
next unless FISH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING.key? type
named_arg_function = FISH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]
named_args << "__fish_brew_complete_arg '#{command}' -a '(#{named_arg_function})'"
end
named_args_strings.each do |subcommand|
subcommands << "__fish_brew_complete_sub_cmd '#{command}' '#{subcommand}'"
end
end
lines += subcommands + options + named_args
<<~COMPLETION
#{lines.join("\n").chomp}
COMPLETION
end
sig { params(commands: T::Array[String]).returns(String) }
def generate_fish_completion_file(commands)
variables = OpenStruct.new
variables[:completion_functions] = commands.map do |command|
generate_fish_subcommand_completion command
end.compact
ERB.new((TEMPLATE_DIR/"fish.erb").read, trim_mode: ">").result(variables.instance_eval { binding })
end
end
2021-01-08 11:10:24 -05:00
end