Skip to content

Commit

Permalink
Merge pull request #19258 from Homebrew/brew_alias_import
Browse files Browse the repository at this point in the history
Import `brew alias` and `brew unalias` commands
  • Loading branch information
MikeMcQuaid authored Feb 7, 2025
2 parents 743971b + 8adc188 commit 5ae29d3
Show file tree
Hide file tree
Showing 15 changed files with 359 additions and 5 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
113 changes: 113 additions & 0 deletions Library/Homebrew/aliases/alias.rb
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions Library/Homebrew/aliases/aliases.rb
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions Library/Homebrew/cmd/alias.rb
Original file line number Diff line number Diff line change
@@ -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` [<alias> ... | <alias>=<command>]"
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
24 changes: 24 additions & 0 deletions Library/Homebrew/cmd/unalias.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Library/Homebrew/official_taps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
].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"],
"homebrew/services" => ["services"],
}.freeze, T::Hash[String, T::Array[String]])

DEPRECATED_OFFICIAL_TAPS = %w[
aliases
apache
binary
cask-drivers
Expand Down
16 changes: 16 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/alias.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/unalias.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Library/Homebrew/startup/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions Library/Homebrew/test/.brew-aliases/foo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#! /bin/bash
# alias: brew foo
#: * `foo` [args...]
#: `brew foo` is an alias for `brew bar`
brew bar $*

19 changes: 19 additions & 0 deletions Library/Homebrew/test/cmd/alias_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 5ae29d3

Please sign in to comment.