From 8adc188992bd39dd80e2fb6644480f497b79575b Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Fri, 7 Feb 2025 14:31:50 +0000 Subject: [PATCH] Import `brew alias` and `brew unalias` commands Import these from the homebrew/aliases tap and deprecate that tap. This required a little messing around with class/module/constant names to get `brew tests` and `brew typecheck` to play nicely. I added also added Sorbet type signatures and integration tests. --- .github/workflows/tests.yml | 4 +- Library/Homebrew/aliases/alias.rb | 113 ++++++++++++++++++ Library/Homebrew/aliases/aliases.rb | 77 ++++++++++++ Library/Homebrew/cmd/alias.rb | 47 ++++++++ Library/Homebrew/cmd/unalias.rb | 24 ++++ Library/Homebrew/official_taps.rb | 2 +- .../sorbet/rbi/dsl/homebrew/cmd/alias.rbi | 16 +++ .../sorbet/rbi/dsl/homebrew/cmd/unalias.rbi | 13 ++ Library/Homebrew/startup/config.rb | 12 ++ Library/Homebrew/test/.brew-aliases/foo | 6 + Library/Homebrew/test/cmd/alias_spec.rb | 19 +++ Library/Homebrew/test/cmd/unalias_spec.rb | 27 +++++ Library/Homebrew/test/spec_helper.rb | 1 + .../test/support/lib/startup/config.rb | 1 + docs/How-to-Create-and-Maintain-a-Tap.md | 2 +- 15 files changed, 359 insertions(+), 5 deletions(-) create mode 100644 Library/Homebrew/aliases/alias.rb create mode 100644 Library/Homebrew/aliases/aliases.rb create mode 100755 Library/Homebrew/cmd/alias.rb create mode 100755 Library/Homebrew/cmd/unalias.rb create mode 100644 Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/alias.rbi create mode 100644 Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/unalias.rbi create mode 100755 Library/Homebrew/test/.brew-aliases/foo create mode 100644 Library/Homebrew/test/cmd/alias_spec.rb create mode 100644 Library/Homebrew/test/cmd/unalias_spec.rb diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f5050f4172b9..bf9a7e49b309a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -113,7 +113,6 @@ jobs: - name: Set up all Homebrew taps run: | - brew tap homebrew/aliases brew tap homebrew/bundle brew tap homebrew/command-not-found brew tap homebrew/formula-analytics @@ -129,8 +128,7 @@ jobs: homebrew/services \ homebrew/test-bot - brew style homebrew/aliases \ - homebrew/command-not-found \ + brew style homebrew/command-not-found \ homebrew/formula-analytics \ homebrew/portable-ruby diff --git a/Library/Homebrew/aliases/alias.rb b/Library/Homebrew/aliases/alias.rb new file mode 100644 index 0000000000000..4c93f88b7d171 --- /dev/null +++ b/Library/Homebrew/aliases/alias.rb @@ -0,0 +1,113 @@ +# typed: strict +# frozen_string_literal: true + +module Homebrew + module Aliases + class Alias + sig { returns(String) } + attr_accessor :name + + sig { returns(T.nilable(String)) } + attr_accessor :command + + sig { params(name: String, command: T.nilable(String)).void } + def initialize(name, command = nil) + @name = T.let(name.strip, String) + @command = T.let(nil, T.nilable(String)) + @script = T.let(nil, T.nilable(Pathname)) + @symlink = T.let(nil, T.nilable(Pathname)) + + @command = if command&.start_with?("!", "%") + command[1..] + elsif command + "brew #{command}" + end + end + + sig { returns(T::Boolean) } + def reserved? + RESERVED.include? name + end + + sig { returns(T::Boolean) } + def cmd_exists? + path = which("brew-#{name}.rb") || which("brew-#{name}") + !path.nil? && path.realpath.parent != HOMEBREW_ALIASES + end + + sig { returns(Pathname) } + def script + @script ||= Pathname.new("#{HOMEBREW_ALIASES}/#{name.gsub(/\W/, "_")}") + end + + sig { returns(Pathname) } + def symlink + @symlink ||= Pathname.new("#{HOMEBREW_PREFIX}/bin/brew-#{name}") + end + + sig { returns(T::Boolean) } + def valid_symlink? + symlink.realpath.parent == HOMEBREW_ALIASES.realpath + rescue NameError + false + end + + sig { void } + def link + FileUtils.rm symlink if File.symlink? symlink + FileUtils.ln_s script, symlink + end + + sig { params(opts: T::Hash[Symbol, T::Boolean]).void } + def write(opts = {}) + odie "'#{name}' is a reserved command. Sorry." if reserved? + odie "'brew #{name}' already exists. Sorry." if cmd_exists? + + return if !opts[:override] && script.exist? + + content = if command + <<~EOS + #: * `#{name}` [args...] + #: `brew #{name}` is an alias for `#{command}` + #{command} $* + EOS + else + <<~EOS + # + # This is a Homebrew alias script. It'll be called when the user + # types `brew #{name}`. Any remaining arguments are passed to + # this script. You can retrieve those with $*, or only the first + # one with $1. Please keep your script on one line. + + # TODO Replace the line below with your script + echo "Hello I'm brew alias "#{name}" and my args are:" $1 + EOS + end + + script.open("w") do |f| + f.write <<~EOS + #! #{`which bash`.chomp} + # alias: brew #{name} + #{content} + EOS + end + script.chmod 0744 + link + end + + sig { void } + def remove + odie "'brew #{name}' is not aliased to anything." if !symlink.exist? || !valid_symlink? + + script.unlink + symlink.unlink + end + + sig { void } + def edit + write(override: false) + exec_editor script.to_s + end + end + end +end diff --git a/Library/Homebrew/aliases/aliases.rb b/Library/Homebrew/aliases/aliases.rb new file mode 100644 index 0000000000000..48887b4c4c041 --- /dev/null +++ b/Library/Homebrew/aliases/aliases.rb @@ -0,0 +1,77 @@ +# typed: strict +# frozen_string_literal: true + +require "aliases/alias" + +module Homebrew + module Aliases + RESERVED = T.let(( + Commands.internal_commands + + Commands.internal_developer_commands + + Commands.internal_commands_aliases + + %w[alias unalias] + ).freeze, T::Array[String]) + + sig { void } + def self.init + FileUtils.mkdir_p HOMEBREW_ALIASES + end + + sig { params(name: String, command: String).void } + def self.add(name, command) + new_alias = Alias.new(name, command) + odie "alias 'brew #{name}' already exists!" if new_alias.script.exist? + new_alias.write + end + + sig { params(name: String).void } + def self.remove(name) + Alias.new(name).remove + end + + sig { params(only: T::Array[String], block: T.proc.params(target: String, cmd: String).void).void } + def self.each(only, &block) + Dir["#{HOMEBREW_ALIASES}/*"].each do |path| + next if path.end_with? "~" # skip Emacs-like backup files + next if File.directory?(path) + + _shebang, _meta, *lines = File.readlines(path) + target = File.basename(path) + next if !only.empty? && only.exclude?(target) + + lines.reject! { |line| line.start_with?("#") || line =~ /^\s*$/ } + first_line = T.must(lines.first) + cmd = first_line.chomp + cmd.sub!(/ \$\*$/, "") + + if cmd.start_with? "brew " + cmd.sub!(/^brew /, "") + else + cmd = "!#{cmd}" + end + + yield target, cmd if block.present? + end + end + + sig { params(aliases: String).void } + def self.show(*aliases) + each([*aliases]) do |target, cmd| + puts "brew alias #{target}='#{cmd}'" + existing_alias = Alias.new(target, cmd) + existing_alias.link unless existing_alias.symlink.exist? + end + end + + sig { params(name: String, command: T.nilable(String)).void } + def self.edit(name, command = nil) + Alias.new(name, command).write unless command.nil? + Alias.new(name, command).edit + end + + sig { void } + def self.edit_all + exec_editor(*Dir[HOMEBREW_ALIASES]) + end + end +end diff --git a/Library/Homebrew/cmd/alias.rb b/Library/Homebrew/cmd/alias.rb new file mode 100755 index 0000000000000..85272c67de62e --- /dev/null +++ b/Library/Homebrew/cmd/alias.rb @@ -0,0 +1,47 @@ +# typed: strict +# frozen_string_literal: true + +require "abstract_command" +require "aliases/aliases" + +module Homebrew + module Cmd + class Alias < AbstractCommand + cmd_args do + usage_banner "`alias` [ ... | =]" + description <<~EOS + Show existing aliases. If no aliases are given, print the whole list. + EOS + switch "--edit", + description: "Edit aliases in a text editor. Either one or all aliases may be opened at once. " \ + "If the given alias doesn't exist it'll be pre-populated with a template." + named_args max: 1 + end + + sig { override.void } + def run + name = args.named.first + name, command = name.split("=", 2) if name.present? + + Aliases.init + + if name.nil? + if args.edit? + Aliases.edit_all + else + Aliases.show + end + elsif command.nil? + if args.edit? + Aliases.edit name + else + Aliases.show name + end + else + Aliases.add name, command + Aliases.edit name if args.edit? + end + end + end + end +end diff --git a/Library/Homebrew/cmd/unalias.rb b/Library/Homebrew/cmd/unalias.rb new file mode 100755 index 0000000000000..a7799c6237c82 --- /dev/null +++ b/Library/Homebrew/cmd/unalias.rb @@ -0,0 +1,24 @@ +# typed: strict +# frozen_string_literal: true + +require "abstract_command" +require "aliases/aliases" + +module Homebrew + module Cmd + class Unalias < AbstractCommand + cmd_args do + description <<~EOS + Remove aliases. + EOS + named_args :alias, min: 1 + end + + sig { override.void } + def run + Aliases.init + args.named.each { |a| Aliases.remove a } + end + end + end +end diff --git a/Library/Homebrew/official_taps.rb b/Library/Homebrew/official_taps.rb index 65c0473342280..db5e78c32f24c 100644 --- a/Library/Homebrew/official_taps.rb +++ b/Library/Homebrew/official_taps.rb @@ -6,7 +6,6 @@ ].freeze OFFICIAL_CMD_TAPS = T.let({ - "homebrew/aliases" => ["alias", "unalias"], "homebrew/bundle" => ["bundle"], "homebrew/command-not-found" => ["command-not-found-init", "which-formula", "which-update"], "homebrew/test-bot" => ["test-bot"], @@ -14,6 +13,7 @@ }.freeze, T::Hash[String, T::Array[String]]) DEPRECATED_OFFICIAL_TAPS = %w[ + aliases apache binary cask-drivers diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/alias.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/alias.rbi new file mode 100644 index 0000000000000..f3d1819f35aea --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/alias.rbi @@ -0,0 +1,16 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Homebrew::Cmd::Alias`. +# Please instead update this file by running `bin/tapioca dsl Homebrew::Cmd::Alias`. + + +class Homebrew::Cmd::Alias + sig { returns(Homebrew::Cmd::Alias::Args) } + def args; end +end + +class Homebrew::Cmd::Alias::Args < Homebrew::CLI::Args + sig { returns(T::Boolean) } + def edit?; end +end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/unalias.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/unalias.rbi new file mode 100644 index 0000000000000..00279114313a5 --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/unalias.rbi @@ -0,0 +1,13 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Homebrew::Cmd::Unalias`. +# Please instead update this file by running `bin/tapioca dsl Homebrew::Cmd::Unalias`. + + +class Homebrew::Cmd::Unalias + sig { returns(Homebrew::Cmd::Unalias::Args) } + def args; end +end + +class Homebrew::Cmd::Unalias::Args < Homebrew::CLI::Args; end diff --git a/Library/Homebrew/startup/config.rb b/Library/Homebrew/startup/config.rb index 225a3a04a7c02..9a14eb095c5c2 100644 --- a/Library/Homebrew/startup/config.rb +++ b/Library/Homebrew/startup/config.rb @@ -65,3 +65,15 @@ ENV.fetch("HOMEBREW_RUBY_WARNINGS"), ENV.fetch("HOMEBREW_RUBY_DISABLE_OPTIONS"), ].freeze + +# Location for `brew alias` and `brew unalias` commands. +# +# Unix-Like systems store config in $HOME/.config whose location can be +# overridden by the XDG_CONFIG_HOME environment variable. Unfortunately +# Homebrew strictly filters environment variables in BuildEnvironment. +HOMEBREW_ALIASES = if (path = Pathname.new("~/.config/brew-aliases").expand_path).exist? || + (path = Pathname.new("~/.brew-aliases").expand_path).exist? + path.realpath +else + path +end.freeze diff --git a/Library/Homebrew/test/.brew-aliases/foo b/Library/Homebrew/test/.brew-aliases/foo new file mode 100755 index 0000000000000..2e3286c7a0d62 --- /dev/null +++ b/Library/Homebrew/test/.brew-aliases/foo @@ -0,0 +1,6 @@ +#! /bin/bash +# alias: brew foo +#: * `foo` [args...] +#: `brew foo` is an alias for `brew bar` +brew bar $* + diff --git a/Library/Homebrew/test/cmd/alias_spec.rb b/Library/Homebrew/test/cmd/alias_spec.rb new file mode 100644 index 0000000000000..35442f0ad87d5 --- /dev/null +++ b/Library/Homebrew/test/cmd/alias_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "cmd/alias" +require "cmd/shared_examples/args_parse" + +RSpec.describe Homebrew::Cmd::Alias do + it_behaves_like "parseable arguments" + + it "sets an alias", :integration_test do + expect { brew "alias", "foo=bar" } + .to not_to_output.to_stdout + .and not_to_output.to_stderr + .and be_a_success + expect { brew "alias" } + .to output(/brew alias foo='bar'/).to_stdout + .and not_to_output.to_stderr + .and be_a_success + end +end diff --git a/Library/Homebrew/test/cmd/unalias_spec.rb b/Library/Homebrew/test/cmd/unalias_spec.rb new file mode 100644 index 0000000000000..4a9d7994044c0 --- /dev/null +++ b/Library/Homebrew/test/cmd/unalias_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "cmd/unalias" +require "cmd/shared_examples/args_parse" + +RSpec.describe Homebrew::Cmd::Unalias do + it_behaves_like "parseable arguments" + + it "unsets an alias", :integration_test do + expect { brew "alias", "foo=bar" } + .to not_to_output.to_stdout + .and not_to_output.to_stderr + .and be_a_success + expect { brew "alias" } + .to output(/brew alias foo='bar'/).to_stdout + .and not_to_output.to_stderr + .and be_a_success + expect { brew "unalias", "foo" } + .to not_to_output.to_stdout + .and not_to_output.to_stderr + .and be_a_success + expect { brew "alias" } + .to not_to_output.to_stdout + .and not_to_output.to_stderr + .and be_a_success + end +end diff --git a/Library/Homebrew/test/spec_helper.rb b/Library/Homebrew/test/spec_helper.rb index 5d521a4f448f5..92c2135b5a34d 100644 --- a/Library/Homebrew/test/spec_helper.rb +++ b/Library/Homebrew/test/spec_helper.rb @@ -63,6 +63,7 @@ HOMEBREW_LOCKS, HOMEBREW_LOGS, HOMEBREW_TEMP, + HOMEBREW_ALIASES, ].freeze # Make `instance_double` and `class_double` diff --git a/Library/Homebrew/test/support/lib/startup/config.rb b/Library/Homebrew/test/support/lib/startup/config.rb index 796ca1ea62a8f..e2cf07ff7e3dc 100644 --- a/Library/Homebrew/test/support/lib/startup/config.rb +++ b/Library/Homebrew/test/support/lib/startup/config.rb @@ -24,6 +24,7 @@ # Paths redirected to a temporary directory and wiped at the end of the test run HOMEBREW_PREFIX = (Pathname(TEST_TMPDIR)/"prefix").freeze +HOMEBREW_ALIASES = (Pathname(TEST_TMPDIR)/"aliases").freeze HOMEBREW_REPOSITORY = HOMEBREW_PREFIX.dup.freeze HOMEBREW_LIBRARY = (HOMEBREW_REPOSITORY/"Library").freeze HOMEBREW_CACHE = (HOMEBREW_PREFIX.parent/"cache").freeze diff --git a/docs/How-to-Create-and-Maintain-a-Tap.md b/docs/How-to-Create-and-Maintain-a-Tap.md index 80ee676bf2a92..2f3342a4d8f74 100644 --- a/docs/How-to-Create-and-Maintain-a-Tap.md +++ b/docs/How-to-Create-and-Maintain-a-Tap.md @@ -50,7 +50,7 @@ Unlike formulae, casks must have globally unique names to avoid clashes. This ca You can provide your tap users with custom `brew` commands by adding them in a `cmd` subdirectory. [Read more on external commands](External-Commands.md). -See [homebrew/aliases](https://github.com/Homebrew/homebrew-aliases) for an example of a tap with external commands. +See [Homebrew/test-bot](https://github.com/Homebrew/homebrew-test-bot) for an example of a tap with external commands. ## Upstream taps