Skip to content

Instantly share code, notes, and snippets.

@Velrok
Last active January 8, 2026 09:58
Show Gist options
  • Select an option

  • Save Velrok/de994796aa26f43e62d74e431fe5e8d1 to your computer and use it in GitHub Desktop.

Select an option

Save Velrok/de994796aa26f43e62d74e431fe5e8d1 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'tempfile'
require 'fileutils'
# Interactive Homebrew package upgrade manager
# Allows editing the list of outdated packages to selectively update, skip, or delete
# Functional pipeline for Homebrew package upgrades
module BrewUpgrade
# Get outdated packages from brew
def self.fetch_outdated_packages
`brew outdated 2>/dev/null`.lines.map(&:chomp).reject(&:empty?)
end
# Prefix each line with 'update '
def self.prefix_with_update(lines)
lines.map { |line| "update #{line}" }
end
# Create instructions header for the temp file
def self.create_header
[
"# Interactive Homebrew Upgrade",
"# Default: 'update' (or 'up', 'u') - packages will be upgraded",
"# Change to 'skip' (or 's') to skip packages",
"# Change to 'uninstall' (or 'remove', 'rm', 'del') to uninstall packages",
"# Delete lines to ignore packages entirely",
"#",
""
]
end
# Write content to temp file and return the file
def self.write_to_tempfile(content)
Tempfile.new(['brew-upgrade-', '.txt']).tap do |file|
file.write(content.join("\n"))
file.flush
end
end
# Open editor and return success status
def self.open_editor(filepath)
editor = ENV['EDITOR'] || 'vim'
system(editor, filepath)
end
# Read and parse edited file
def self.read_edited_file(filepath)
File.readlines(filepath).map(&:chomp)
end
# Filter lines by prefix
def self.filter_lines_by_prefix(lines, prefixes)
lines
.reject { |line| line.start_with?('#') || line.strip.empty? }
.select { |line| prefixes.any? { |prefix| line.start_with?("#{prefix} ") } }
end
# Extract package name from line (first word after prefix)
def self.extract_package_name(line)
line.split[1]
end
# Extract packages for a given action
def self.extract_packages_by_action(lines, prefixes)
filter_lines_by_prefix(lines, prefixes).map { |line| extract_package_name(line) }
end
# Uninstall packages using brew
def self.uninstall_packages(packages)
return if packages.empty?
puts "Uninstalling #{packages.size} package(s): #{packages.join(', ')}"
puts "Running: brew uninstall #{packages.join(' ')}"
system('brew', 'uninstall', *packages)
end
# Upgrade packages using brew
def self.upgrade_packages(packages)
return if packages.empty?
puts "Upgrading #{packages.size} package(s): #{packages.join(', ')}"
puts "Running: brew upgrade #{packages.join(' ')}"
system('brew', 'upgrade', *packages)
end
# Main execution pipeline
def self.run
puts "Fetching outdated packages..."
outdated = fetch_outdated_packages
if outdated.empty?
puts "All packages are up to date!"
return
end
content = create_header + prefix_with_update(outdated)
tempfile = write_to_tempfile(content)
begin
exit 1 unless open_editor(tempfile.path)
edited_lines = read_edited_file(tempfile.path)
# Extract packages for each action
uninstall_packages = extract_packages_by_action(edited_lines, ['uninstall', 'remove', 'rm', 'del'])
upgrade_packages_list = extract_packages_by_action(edited_lines, ['update', 'up', 'u'])
# Run uninstalls first, then upgrades
if uninstall_packages.empty? && upgrade_packages_list.empty?
puts "No packages selected for upgrade or uninstall."
return
end
uninstall_packages(uninstall_packages)
upgrade_packages(upgrade_packages_list)
puts "\nDone!"
ensure
tempfile.unlink
end
end
end
# Execute if run directly
BrewUpgrade.run if __FILE__ == $PROGRAM_NAME
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment