# typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true require "description_cache_store" module Homebrew # Helper module for searching formulae or casks. module Search def self.query_regexp(query) if (m = query.match(%r{^/(.*)/$})) Regexp.new(m[1]) else query end rescue RegexpError raise "#{query} is not a valid regex." end def self.search_descriptions(string_or_regex, args, search_type: :desc) both = !args.formula? && !args.cask? eval_all = args.eval_all? || Homebrew::EnvConfig.eval_all? if args.formula? || both ohai "Formulae" if eval_all CacheStoreDatabase.use(:descriptions) do |db| cache_store = DescriptionCacheStore.new(db) Descriptions.search(string_or_regex, search_type, cache_store, eval_all).print end else unofficial = Tap.all.sum { |tap| tap.official? ? 0 : tap.formula_files.size } if unofficial.positive? opoo "Use `--eval-all` to search #{unofficial} additional " \ "#{Utils.pluralize("formula", unofficial, plural: "e")} in third party taps." end descriptions = Homebrew::API::Formula.all_formulae.transform_values { |data| data["desc"] } Descriptions.search(string_or_regex, search_type, descriptions, eval_all, cache_store_hash: true).print end end return if !args.cask? && !both puts if both ohai "Casks" if eval_all CacheStoreDatabase.use(:cask_descriptions) do |db| cache_store = CaskDescriptionCacheStore.new(db) Descriptions.search(string_or_regex, search_type, cache_store, eval_all).print end else unofficial = Tap.all.sum { |tap| tap.official? ? 0 : tap.cask_files.size } if unofficial.positive? opoo "Use `--eval-all` to search #{unofficial} additional " \ "#{Utils.pluralize("cask", unofficial)} in third party taps." end descriptions = Homebrew::API::Cask.all_casks.transform_values { |c| [c["name"].join(", "), c["desc"]] } Descriptions.search(string_or_regex, search_type, descriptions, eval_all, cache_store_hash: true).print end end def self.search_formulae(string_or_regex) if string_or_regex.is_a?(String) && string_or_regex.match?(HOMEBREW_TAP_FORMULA_REGEX) return begin [Formulary.factory(string_or_regex).name] rescue FormulaUnavailableError [] end end aliases = Formula.alias_full_names results = search(Formula.full_names + aliases, string_or_regex).sort if string_or_regex.is_a?(String) results |= Formula.fuzzy_search(string_or_regex).map do |n| Formulary.factory(n).full_name end end results.filter_map do |name| formula, canonical_full_name = begin f = Formulary.factory(name) [f, f.full_name] rescue [nil, name] end # Ignore aliases from results when the full name was also found next if aliases.include?(name) && results.include?(canonical_full_name) if formula&.any_version_installed? pretty_installed(name) elsif formula.nil? || formula.valid_platform? name end end end def self.search_casks(string_or_regex) if string_or_regex.is_a?(String) && string_or_regex.match?(HOMEBREW_TAP_CASK_REGEX) return begin [Cask::CaskLoader.load(string_or_regex).token] rescue Cask::CaskUnavailableError [] end end cask_tokens = Tap.each_with_object([]) do |tap, array| # We can exclude the core cask tap because `CoreCaskTap#cask_tokens` returns short names by default. if tap.official? && !tap.core_cask_tap? tap.cask_tokens.each { |token| array << token.sub(%r{^homebrew/cask.*/}, "") } else tap.cask_tokens.each { |token| array << token } end end.uniq results = search(cask_tokens, string_or_regex) results += DidYouMean::SpellChecker.new(dictionary: cask_tokens) .correct(string_or_regex) results.sort.map do |name| cask = Cask::CaskLoader.load(name) if cask.installed? pretty_installed(cask.full_name) else cask.full_name end end.uniq end def self.search_names(string_or_regex, args) both = !args.formula? && !args.cask? all_formulae = if args.formula? || both search_formulae(string_or_regex) else [] end all_casks = if args.cask? || both search_casks(string_or_regex) else [] end [all_formulae, all_casks] end def self.search(selectable, string_or_regex, &block) case string_or_regex when Regexp search_regex(selectable, string_or_regex, &block) else search_string(selectable, string_or_regex.to_str, &block) end end def self.simplify_string(string) string.downcase.gsub(/[^a-z\d@+]/i, "") end def self.search_regex(selectable, regex) selectable.select do |*args| args = yield(*args) if block_given? args = Array(args).flatten.compact args.any? { |arg| arg.match?(regex) } end end def self.search_string(selectable, string) simplified_string = simplify_string(string) selectable.select do |*args| args = yield(*args) if block_given? args = Array(args).flatten.compact args.any? { |arg| simplify_string(arg).include?(simplified_string) } end end end end