From 837437416840d4c4f132b7e4ba2ea0fa2f861668 Mon Sep 17 00:00:00 2001 From: Dan Martinez Date: Tue, 5 May 2015 15:29:01 -0700 Subject: [PATCH] Improve description searching and add a cache. Closes Homebrew/homebrew#42281. Signed-off-by: Mike McQuaid --- Library/Contributions/brew_bash_completion.sh | 13 ++ Library/Contributions/brew_zsh_completion.zsh | 3 +- Library/Homebrew/cmd/desc.rb | 40 +++++ Library/Homebrew/cmd/search.rb | 7 +- Library/Homebrew/cmd/tap.rb | 2 + Library/Homebrew/cmd/untap.rb | 2 + Library/Homebrew/cmd/update.rb | 2 + Library/Homebrew/descriptions.rb | 151 ++++++++++++++++++ Library/Homebrew/manpages/brew.1.md | 10 ++ Library/Homebrew/utils.rb | 4 +- share/man/man1/brew.1 | 6 + 11 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 Library/Homebrew/cmd/desc.rb create mode 100644 Library/Homebrew/descriptions.rb diff --git a/Library/Contributions/brew_bash_completion.sh b/Library/Contributions/brew_bash_completion.sh index 5c29921263..83087ea54d 100644 --- a/Library/Contributions/brew_bash_completion.sh +++ b/Library/Contributions/brew_bash_completion.sh @@ -195,6 +195,18 @@ _brew_deps () __brew_complete_formulae } +_brew_desc () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __brewcomp "--search --name --description" + return + ;; + esac + __brew_complete_formulae +} + _brew_doctor () { local cur="${COMP_WORDS[COMP_CWORD]}" __brewcomp "$(brew doctor --list-checks)" @@ -599,6 +611,7 @@ _brew () cleanup) _brew_cleanup ;; create) _brew_create ;; deps) _brew_deps ;; + desc) _brew_desc ;; doctor|dr) _brew_doctor ;; diy|configure) _brew_diy ;; fetch) _brew_fetch ;; diff --git a/Library/Contributions/brew_zsh_completion.zsh b/Library/Contributions/brew_zsh_completion.zsh index dec63b84de..27faa339a2 100644 --- a/Library/Contributions/brew_zsh_completion.zsh +++ b/Library/Contributions/brew_zsh_completion.zsh @@ -40,6 +40,7 @@ _1st_arguments=( 'config:show homebrew and system configuration' 'create:create a new formula' 'deps:list dependencies and dependants of a formula' + 'desc:display a description of a formula' 'doctor:audits your installation for common issues' 'edit:edit a formula' 'fetch:download formula resources to the cache' @@ -95,7 +96,7 @@ if (( CURRENT == 1 )); then fi case "$words[1]" in - install|reinstall|audit|home|homepage|log|info|abv|uses|cat|deps|edit|options|switch) + install|reinstall|audit|home|homepage|log|info|abv|uses|cat|deps|desc|edit|options|switch) _brew_all_formulae _wanted formulae expl 'all formulae' compadd -a formulae ;; list|ls) diff --git a/Library/Homebrew/cmd/desc.rb b/Library/Homebrew/cmd/desc.rb new file mode 100644 index 0000000000..77078e8f5d --- /dev/null +++ b/Library/Homebrew/cmd/desc.rb @@ -0,0 +1,40 @@ +require "descriptions" +require "cmd/search" + +module Homebrew + def desc + if ARGV.options_only.empty? + if ARGV.named.empty? + raise FormulaUnspecifiedError + exit + end + results = Descriptions.named(ARGV.formulae.map(&:full_name)) + else + if ARGV.options_only.count != 1 + odie "Pick one, and only one, of -s/--search, -n/--name, or -d/--description." + end + + search_arg = ARGV.options_only.first + + search_type = case search_arg + when '-s', '--search' + :either + when '-n', '--name' + :name + when '-d', '--description' + :desc + else + odie "Unrecognized option '#{search_arg}'." + end + + if arg = ARGV.named.first + regex = Homebrew::query_regexp(arg) + results = Descriptions.search(regex, search_type) + else + odie "You must provide a search term." + end + end + + results.print unless results.nil? + end +end diff --git a/Library/Homebrew/cmd/search.rb b/Library/Homebrew/cmd/search.rb index 10e0a8269b..9e5f288156 100644 --- a/Library/Homebrew/cmd/search.rb +++ b/Library/Homebrew/cmd/search.rb @@ -3,6 +3,7 @@ require "blacklist" require "utils" require "thread" require "official_taps" +require 'descriptions' module Homebrew SEARCH_ERROR_QUEUE = Queue.new @@ -23,11 +24,7 @@ module Homebrew elsif ARGV.include? "--desc" query = ARGV.next rx = query_regexp(query) - Formula.each do |formula| - if formula.desc =~ rx - puts "#{Tty.white}#{formula.full_name}:#{Tty.reset} #{formula.desc}" - end - end + Descriptions.search(rx, :desc).print elsif ARGV.empty? puts_columns Formula.full_names elsif ARGV.first =~ HOMEBREW_TAP_FORMULA_REGEX diff --git a/Library/Homebrew/cmd/tap.rb b/Library/Homebrew/cmd/tap.rb index 0c8960cbd6..291407aff1 100644 --- a/Library/Homebrew/cmd/tap.rb +++ b/Library/Homebrew/cmd/tap.rb @@ -1,4 +1,5 @@ require "tap" +require "descriptions" module Homebrew def tap @@ -41,6 +42,7 @@ module Homebrew formula_count = tap.formula_files.size puts "Tapped #{formula_count} formula#{plural(formula_count, "e")} (#{tap.path.abv})" + Descriptions.cache_formulae(tap.formula_names) if !clone_target && tap.private? puts <<-EOS.undent diff --git a/Library/Homebrew/cmd/untap.rb b/Library/Homebrew/cmd/untap.rb index 1e8bfdcabe..22dab73832 100644 --- a/Library/Homebrew/cmd/untap.rb +++ b/Library/Homebrew/cmd/untap.rb @@ -1,4 +1,5 @@ require "cmd/tap" # for tap_args +require "descriptions" module Homebrew def untap @@ -13,6 +14,7 @@ module Homebrew tap.unpin if tap.pinned? formula_count = tap.formula_files.size + Descriptions.uncache_formulae(tap.formula_names) tap.path.rmtree tap.path.dirname.rmdir_if_possible puts "Untapped #{formula_count} formula#{plural(formula_count, "e")}" diff --git a/Library/Homebrew/cmd/update.rb b/Library/Homebrew/cmd/update.rb index 1d4eb5928d..4be763b6aa 100644 --- a/Library/Homebrew/cmd/update.rb +++ b/Library/Homebrew/cmd/update.rb @@ -2,6 +2,7 @@ require "cmd/tap" require "formula_versions" require "migrator" require "formulary" +require "descriptions" module Homebrew def update @@ -100,6 +101,7 @@ module Homebrew puts "Updated Homebrew from #{master_updater.initial_revision[0, 8]} to #{master_updater.current_revision[0, 8]}." report.dump end + Descriptions.update_cache(report) end private diff --git a/Library/Homebrew/descriptions.rb b/Library/Homebrew/descriptions.rb new file mode 100644 index 0000000000..428f42a5e1 --- /dev/null +++ b/Library/Homebrew/descriptions.rb @@ -0,0 +1,151 @@ +require "formula" +require "csv" + +class Descriptions + CACHE_FILE = HOMEBREW_CACHE + "desc_cache" + + def self.cache + @cache || self.load_cache + end + + # If the cache file exists, load it into, and return, a hash; otherwise, + # return nil. + def self.load_cache + if CACHE_FILE.exist? + @cache = {} + CSV.foreach(CACHE_FILE) { |name, desc| @cache[name] = desc } + @cache + end + end + + # Write the cache to disk after ensuring the existence of the containing + # directory. + def self.save_cache + HOMEBREW_CACHE.mkpath + CSV.open(CACHE_FILE, 'w') do |csv| + @cache.each do |name, desc| + csv << [name, desc] + end + end + end + + # Create a hash mapping all formulae to their descriptions; + # save it for future use. + def self.generate_cache + @cache = {} + Formula.map do |f| + @cache[f.full_name] = f.desc + end + self.save_cache + end + + # Return true if the cache exists, and neither Homebrew nor any of the Taps + # repos were updated more recently than it was. + def self.cache_fresh? + if CACHE_FILE.exist? + cache_date = File.mtime(CACHE_FILE) + + ref_master = ".git/refs/heads/master" + master = HOMEBREW_REPOSITORY/ref_master + + last_update = (master.exist? ? File.mtime(master) : Time.at(0)) + + Dir.glob(HOMEBREW_LIBRARY/"Taps/**"/ref_master).each do |repo| + repo_mtime = File.mtime(repo) + last_update = repo_mtime if repo_mtime > last_update + end + last_update <= cache_date + end + end + + # Create the cache if it doesn't already exist. + def self.ensure_cache + self.generate_cache unless self.cache_fresh? && self.cache + end + + # Take a {Report}, as generated by cmd/update.rb. + # Unless the cache file exists, do nothing. + # If it does exist, but the Report is empty, just touch the cache file. + # Otherwise, use the report to update the cache. + def self.update_cache(report) + if CACHE_FILE.exist? + if report.empty? + FileUtils.touch CACHE_FILE + else + renamings = report.select_formula(:R) + alterations = report.select_formula(:A) + report.select_formula(:M) + + renamings.map(&:last) + self.cache_formulae(alterations, :save => false) + self.uncache_formulae(report.select_formula(:D) + + renamings.map(&:first)) + end + end + end + + # Given an array of formula names, add them and their descriptions to the + # cache. Save the updated cache to disk, unless explicitly told not to. + def self.cache_formulae(formula_names, options = { :save => true }) + if self.cache + formula_names.each { |name| @cache[name] = Formula[name].desc } + self.save_cache if options[:save] + end + end + + # Given an array of formula names, remove them and their descriptions from + # the cache. Save the updated cache to disk, unless explicitly told not to. + def self.uncache_formulae(formula_names, options = { :save => true }) + if self.cache + formula_names.each { |name| @cache.delete(name) } + self.save_cache if options[:save] + end + end + + # Given an array of formula names, return a {Descriptions} object mapping + # those names to their descriptions. + def self.named(names) + self.ensure_cache + + results = {} + unless names.empty? + results = names.inject({}) do |accum, name| + accum[name] = @cache[name] + accum + end + end + + new(results) + end + + # Given a regex, find all formulae whose specified fields contain a match. + def self.search(regex, field = :either) + self.ensure_cache + + results = case field + when :name + @cache.select { |name, _| name =~ regex } + when :desc + @cache.select { |_, desc| desc =~ regex } + when :either + @cache.select { |name, desc| (name =~ regex) || (desc =~ regex) } + end + + results = Hash[results] if RUBY_VERSION <= "1.8.7" + + new(results) + end + + # Create an actual instance. + def initialize(descriptions) + @descriptions = descriptions + end + + # Take search results -- a hash mapping formula names to descriptions -- and + # print them. + def print + blank = "#{Tty.yellow}[no description]#{Tty.reset}" + @descriptions.keys.sort.each do |name| + description = @descriptions[name] || blank + puts "#{Tty.white}#{name}:#{Tty.reset} #{description}" + end + end +end diff --git a/Library/Homebrew/manpages/brew.1.md b/Library/Homebrew/manpages/brew.1.md index 5adcb21808..06df14196c 100644 --- a/Library/Homebrew/manpages/brew.1.md +++ b/Library/Homebrew/manpages/brew.1.md @@ -126,6 +126,16 @@ Note that these flags should only appear after a command. type dependencies, pass `--skip-build`. Similarly, pass `--skip-optional` to skip `:optional` dependencies. + * `desc` : + Display 's name and one-line description. + + * `desc [-s|-n|-d] `: + Search both name and description (`-s`), just the names (`-n`), or just the + descriptions (`-d`) for ``. `` is by default interpreted + as a literal string; if flanked by slashes, it is instead interpreted as a + regular expression. Formula descriptions are cached; the cache is created on + the first search, making that search slower than subsequent ones. + * `diy [--name=] [--version=]`: Automatically determine the installation prefix for non-Homebrew software. diff --git a/Library/Homebrew/utils.rb b/Library/Homebrew/utils.rb index 2b496565cb..bec8861dad 100644 --- a/Library/Homebrew/utils.rb +++ b/Library/Homebrew/utils.rb @@ -255,8 +255,8 @@ def puts_columns(items, star_items = []) # determine the best width to display for different console sizes console_width = `/bin/stty size`.chomp.split(" ").last.to_i console_width = 80 if console_width <= 0 - longest = items.sort_by(&:length).last - optimal_col_width = (console_width.to_f / (longest.length + 2).to_f).floor + max_len = items.reduce(0) { |max, item| l = item.length ; l > max ? l : max } + optimal_col_width = (console_width.to_f / (max_len + 2).to_f).floor cols = optimal_col_width > 1 ? optimal_col_width : 1 IO.popen("/usr/bin/pr -#{cols} -t -w#{console_width}", "w") { |io| io.puts(items) } diff --git a/share/man/man1/brew.1 b/share/man/man1/brew.1 index e059285830..0179f0ef4f 100644 --- a/share/man/man1/brew.1 +++ b/share/man/man1/brew.1 @@ -128,6 +128,12 @@ If \fB\-\-installed\fR is passed, show dependencies for all installed formulae\. By default, \fBdeps\fR shows dependencies for \fIformulae\fR\. To skip the \fB:build\fR type dependencies, pass \fB\-\-skip\-build\fR\. Similarly, pass \fB\-\-skip\-optional\fR to skip \fB:optional\fR dependencies\. . .IP "\(bu" 4 +\fBdesc\fR \fIformula\fR: Display \fIformula\fR\'s name and one\-line description\. +. +.IP "\(bu" 4 +\fBdesc [\-s|\-n|\-d] \fR: Search both name and description (\fB\-s\fR), just the names (\fB\-n\fR), or just the descriptions (\fB\-d\fR) for \fB\fR\. \fB\fR is by default interpreted as a literal string; if flanked by slashes, it is instead interpreted as a regular expression\. Formula descriptions are cached; the cache is created on the first search, making that search slower than subsequent ones\. +. +.IP "\(bu" 4 \fBdiy [\-\-name=] [\-\-version=]\fR: Automatically determine the installation prefix for non\-Homebrew software\. . .IP