├── .editorconfig ├── .gitattributes ├── LICENSE.md ├── README.md └── cmd ├── brew-whence.rb └── whence ├── descriptors.rb ├── exceptions.rb ├── format.rb ├── format ├── descriptor_table.rb ├── descriptor_table_plain.rb ├── descriptor_table_plain_row.rb └── descriptor_table_row.rb ├── source_locator.rb └── targets.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [.gitattributes] 13 | indent_size = 16 14 | 15 | [*.{diff,patch}] 16 | end_of_line = lf 17 | trim_trailing_whitespace = false 18 | 19 | [*.md] 20 | indent_size = 2 21 | trim_trailing_whitespace = false 22 | 23 | [*.rb] 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .editorconfig text eol=lf 2 | .gitattributes text eol=lf 3 | 4 | # LF required for interactive patching as of Git 2.17.1 5 | *.diff text eol=lf 6 | *.patch text eol=lf 7 | 8 | *.md text eol=lf 9 | *.rb text eol=lf 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 – 2020 Claudia 2 | 3 | Permission to use, copy, modify, and/or distribute this software for 4 | any purpose with or without fee is hereby granted, provided that the 5 | above copyright notice and this permission notice appear in all 6 | copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 9 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 10 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 11 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 12 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 13 | PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note: This external Homebrew command is no longer maintained. Feel free to create a fork or switch to one that is actively supported.** 2 | 3 | ___ 4 | 5 | # brew whence 6 | 7 | ## About brew whence 8 | 9 | `brew whence` is an external command to Homebrew. 10 | It lets you look up the Homebrew formula from where you got an executable. 11 | 12 | ## Example 13 | 14 | With `brew whence` installed, run: 15 | 16 | ``` 17 | $ brew whence 7z mvn zdb 18 | ``` 19 | 20 | And you get: 21 | 22 | ``` 23 | Executable Comes from 24 | ========== ========== 25 | /usr/local/bin/7z → p7zip 16.02 26 | /usr/local/bin/mvn → maven 3.6.3 27 | /usr/local/bin/zdb (not a link) 28 | ``` 29 | 30 | ## Command-line options 31 | 32 | The `-H` or `--no-headers` option will print the output in a machine-readable form (tab-separated, no headers): 33 | 34 | ``` 35 | $ brew whence -H 7z mvn zdb 36 | ``` 37 | 38 | ``` 39 | 7z A p7zip 16.02 /usr/local/bin/7z 40 | mvn A maven 3.6.3 /usr/local/bin/mvn 41 | zdb N /usr/local/bin/zdb 42 | ``` 43 | 44 | ## Installing 45 | 46 | To install the `brew whence` command, run: 47 | 48 | ``` 49 | $ brew tap claui/whence 50 | ``` 51 | 52 | ## License 53 | 54 | Copyright (c) 2019 – 2020 Claudia 55 | 56 | Permission to use, copy, modify, and/or distribute this software for 57 | any purpose with or without fee is hereby granted, provided that the 58 | above copyright notice and this permission notice appear in all 59 | copies. 60 | 61 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 62 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 63 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 64 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 65 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 66 | PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 67 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 68 | PERFORMANCE OF THIS SOFTWARE. 69 | -------------------------------------------------------------------------------- /cmd/brew-whence.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | #: * `whence` [`--no-headers`] 5 | #: 6 | #: Look up the formula to which an executable belongs. 7 | #: 8 | #: -H, --no-headers print output in machine-readable form 9 | #: (tab-separated, no headers). 10 | #: 11 | #: Example #1: 12 | #: 13 | #: $ brew whence 7z mvn zdb 14 | #: 15 | #: Executable Comes from 16 | #: ========== ========== 17 | #: /usr/local/bin/7z → p7zip 16.02 18 | #: /usr/local/bin/mvn → maven 3.6.3 19 | #: /usr/local/bin/zdb (not a link) 20 | #: 21 | #: Example #2: 22 | #: 23 | #: $ brew whence -H 7z mvn zdb 24 | #: 25 | #: 7z A p7zip 16.02 /usr/local/bin/7z 26 | #: mvn A maven 3.6.3 /usr/local/bin/mvn 27 | #: zdb N /usr/local/bin/zdb 28 | 29 | require "cli/parser" 30 | require "pathname" 31 | 32 | require_relative "./whence/exceptions" 33 | require_relative "./whence/format" 34 | require_relative "./whence/source_locator" 35 | 36 | module Homebrew 37 | module_function 38 | 39 | LINK_DIRECTORIES= %w[bin sbin] 40 | 41 | def whence_args 42 | Homebrew::CLI::Parser.new do 43 | usage_banner <<~EOS 44 | `whence` [] 45 | 46 | Look up the formula to which an executable belongs. 47 | EOS 48 | switch "-H", "--no-headers" 49 | switch :debug 50 | end 51 | end 52 | 53 | def whence 54 | args = whence_args.parse 55 | 56 | names = args.named 57 | odebug "Given executable names", *names 58 | raise FilenameUnspecifiedError if names.empty? 59 | 60 | odebug "Collecting files" 61 | subdirs = LINK_DIRECTORIES.map { |name| HOMEBREW_PREFIX/name } 62 | 63 | symlinks_by_name = names.reduce({}) do |h, name| 64 | symlinks = [] 65 | 66 | if File.dirname(name) == '.' 67 | subdirs.each do |subdir| 68 | pathname = subdir/name 69 | symlinks << pathname if pathname.exist? 70 | end 71 | else 72 | pathname = Pathname.new(name) 73 | symlinks << pathname.expand_path if pathname.exist? 74 | end 75 | 76 | if symlinks.empty? 77 | raise NoSuchFileError.new(name, subdirs) 78 | end 79 | h[name] = symlinks 80 | h 81 | end 82 | 83 | odebug "Files found", symlinks_by_name.inspect 84 | 85 | sources = symlinks_by_name.reduce([]) do |a, (name, symlinks)| 86 | symlinks.each do |symlink| 87 | odebug "Locating formula for #{name}" 88 | locator = SourceLocator.new(name, symlink) 89 | a << locator.descriptor 90 | end 91 | a 92 | end 93 | 94 | odebug "Sources found", *sources 95 | table = if args.no_headers? 96 | Format::DescriptorTablePlain.new(sources) 97 | else 98 | Format::DescriptorTable.new(sources) 99 | end 100 | puts table.to_s 101 | end 102 | 103 | whence 104 | odebug "Success" 105 | end 106 | -------------------------------------------------------------------------------- /cmd/whence/descriptors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Homebrew 4 | module Descriptors 5 | Formula = Struct.new(:executable, :target) do 6 | def note; end 7 | end 8 | 9 | NotALink = Struct.new(:executable) do 10 | def note 11 | 'not a link' 12 | end 13 | def target; end 14 | end 15 | 16 | Unknown = Struct.new(:executable, :target) do 17 | def note 18 | 'unknown' 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /cmd/whence/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "exceptions" 4 | 5 | module Homebrew 6 | class FilenameUnspecifiedError < UsageError 7 | def initialize 8 | super "This command requires one or more file names." 9 | end 10 | end 11 | 12 | class NoSuchFileError < RuntimeError 13 | attr_reader :name, :search_dirs 14 | 15 | def initialize(name, search_dirs) 16 | @name = name 17 | @search_dirs = search_dirs 18 | end 19 | 20 | def to_s 21 | "#{name}: no such file in any of #{search_dirs.join(", ")}" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /cmd/whence/format.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "./format/descriptor_table.rb" 4 | require_relative "./format/descriptor_table_plain.rb" 5 | -------------------------------------------------------------------------------- /cmd/whence/format/descriptor_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "./descriptor_table_row.rb" 4 | 5 | module Homebrew 6 | module Format 7 | class DescriptorTable 8 | COLUMN_TITLES = [ 9 | 'Executable', 10 | '', 11 | 'Comes from', 12 | ] 13 | 14 | attr_reader :descriptors 15 | 16 | def to_s 17 | out = StringIO.new 18 | out.printf(row_format_string, *COLUMN_TITLES) 19 | out.printf(row_format_string, 20 | *(COLUMN_TITLES.map {|title| '=' * title.to_s.length})) 21 | rows.each do |row| 22 | out.printf(row_format_string, row.executable, row.note, 23 | row.target) 24 | end 25 | out.string 26 | end 27 | 28 | def initialize(descriptors) 29 | @descriptors = descriptors.dup.freeze 30 | end 31 | 32 | private 33 | 34 | def column_widths 35 | @column_widths ||= begin 36 | field_widths 37 | .zip(COLUMN_TITLES.map(&:length)) 38 | .map { |pair| pair.compact.max } 39 | end 40 | end 41 | 42 | def field_widths 43 | %i[executable note target].map do |sym| 44 | rows.map { |row| String(row.send(sym)).length }.max 45 | end 46 | end 47 | 48 | def row_format_string 49 | @row_format_string ||= [ 50 | "%-#{column_widths[0]}s", 51 | "%#{column_widths[1]}s", 52 | "%-#{column_widths[2]}s", 53 | ].join(' ') << "\n" 54 | end 55 | 56 | def rows 57 | @rows ||= descriptors.map(&DescriptorTableRow.method(:new)) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /cmd/whence/format/descriptor_table_plain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'csv' 4 | 5 | require_relative "./descriptor_table_plain_row.rb" 6 | 7 | module Homebrew 8 | module Format 9 | class DescriptorTablePlain 10 | attr_reader :descriptors 11 | 12 | def to_s 13 | CSV.generate(col_sep: "\t") do |csv| 14 | rows.each { |row| csv << row.to_a } 15 | end 16 | end 17 | 18 | def initialize(descriptors) 19 | @descriptors = descriptors.dup.freeze 20 | end 21 | 22 | private 23 | 24 | def rows 25 | @rows ||= descriptors.map(&DescriptorTablePlainRow.method(:new)) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /cmd/whence/format/descriptor_table_plain_row.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Homebrew 5 | module Format 6 | DescriptorTablePlainRow = Struct.new(:descriptor) do 7 | def to_a 8 | [ 9 | executable.basename, 10 | target_type_letter, 11 | (target&.name if descriptor.is_a?(Descriptors::Formula)), 12 | (target&.version if descriptor.is_a?(Descriptors::Formula)), 13 | nil, # reserved 14 | nil, # reserved 15 | String(executable), 16 | ] 17 | end 18 | 19 | private 20 | 21 | def executable 22 | descriptor.executable 23 | end 24 | 25 | def target 26 | descriptor.target 27 | end 28 | 29 | def target_type_letter 30 | case descriptor 31 | when Descriptors::Formula then 'A' 32 | when Descriptors::NotALink then 'N' 33 | when Descriptors::Unknown then '?' 34 | else raise TypeError.new("Unexpected descriptor: #{descriptor}") 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /cmd/whence/format/descriptor_table_row.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Homebrew 5 | module Format 6 | DescriptorTableRow = Struct.new(:descriptor) do 7 | TARGET_FORMAT_STRING = '→ %s %s' 8 | 9 | def executable 10 | String(descriptor.executable) 11 | end 12 | 13 | def note 14 | descriptor.note ? "(#{descriptor.note})" : '' 15 | end 16 | 17 | def target 18 | if descriptor.target&.respond_to?(:to_str) 19 | descriptor.target.to_str 20 | elsif descriptor.target&.respond_to?(:name) && 21 | descriptor.target.respond_to?(:version) 22 | sprintf( 23 | TARGET_FORMAT_STRING, 24 | descriptor.target.name, 25 | descriptor.target.version 26 | ).rstrip 27 | else 28 | '' 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /cmd/whence/source_locator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "./descriptors" 4 | require_relative "./targets" 5 | 6 | module Homebrew 7 | class SourceLocator 8 | def descriptor 9 | @descriptor ||= find_descriptor 10 | end 11 | 12 | def initialize(target_name, target_path) 13 | @target_name = target_name 14 | @target_path = target_path 15 | end 16 | 17 | private 18 | 19 | attr_reader :target_name, :target_path 20 | 21 | def find_descriptor 22 | odebug "Finding descriptor for #{target_path}" 23 | 24 | unless target_path.symlink? 25 | odebug "#{target_path} not a symlink" 26 | return Descriptors::NotALink.new(target_path) 27 | end 28 | 29 | link = target_path.readlink 30 | if link.absolute? 31 | return Descriptors::Unknown.new(target_path, link) 32 | end 33 | source = link.expand_path(target_path.parent) 34 | path_starts_cellar = source.to_s.start_with?(HOMEBREW_CELLAR.to_s) 35 | 36 | if path_starts_cellar 37 | formula_target = Targets::FormulaTarget.new( 38 | *source 39 | .relative_path_from(HOMEBREW_CELLAR) 40 | .to_s 41 | .split('/')[0..1] 42 | ) 43 | return Descriptors::Formula.new(target_path, formula_target) 44 | end 45 | Descriptors::Unknown.new(target_path, source.relative_path_from(source.parent)) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /cmd/whence/targets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Homebrew 4 | module Targets 5 | FormulaTarget = Struct.new(:name, :version) 6 | end 7 | end 8 | --------------------------------------------------------------------------------