mirror of
https://github.com/donl/homebrew-cask-versions.git
synced 2026-07-17 06:16:47 -06:00
* "Canonical App Name" becomes "Simplified App Name"
* devscript `cask_namer` renamed to `generate_cask_token`
* doc file `CASK_NAMING_REFERENCE.md` renamed to `cask_token_reference.md`
* DSL uses `"#{token}"` for interpolation instead of `"#{title}"`
* documentation text
* backend code (variables, method, class names)
* error message text
* tests
* code comments
* Cask comments
* emphasize `tags :name`
* doc: use "vendor" consistently instead of "developer"
* doc: many man page argument descriptions were incorrect
* incidental clarifications
Many backend variables similar to `cask_name` or `cask` have
been standardized to `cask_token`, `token`, etc, resolving a long-
standing ambiguity in which variables named `cask` might contain
a Cask instance or a string token.
In many places the docs could be shortened from "Cask name" to
simply "token", which is desirable because we use the term "Cask"
in too many contexts.
441 lines
13 KiB
Ruby
Executable file
441 lines
13 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
#
|
|
# generate_cask_token
|
|
#
|
|
# todo:
|
|
#
|
|
# detect Cask files which differ only by the placement of hyphens.
|
|
#
|
|
|
|
###
|
|
### dependencies
|
|
###
|
|
|
|
require 'pathname'
|
|
require 'open3'
|
|
|
|
begin
|
|
# not available by default
|
|
require 'active_support/inflector'
|
|
rescue LoadError
|
|
end
|
|
|
|
###
|
|
### configurable constants
|
|
###
|
|
|
|
EXPANDED_NUMBERS = {
|
|
'0' => 'zero',
|
|
'1' => 'one',
|
|
'2' => 'two',
|
|
'3' => 'three',
|
|
'4' => 'four',
|
|
'5' => 'five',
|
|
'6' => 'six',
|
|
'7' => 'seven',
|
|
'8' => 'eight',
|
|
'9' => 'nine',
|
|
}
|
|
|
|
EXPANDED_SYMBOLS = {
|
|
'+' => 'plus',
|
|
}
|
|
|
|
CASK_FILE_EXTENSION = '.rb'
|
|
|
|
# Hardcode App names that cannot be transformed automatically.
|
|
# Example: in "x48.app", "x48" is not a version number.
|
|
# The value in the hash should be a valid Cask token.
|
|
APP_EXCEPTION_PATS = {
|
|
# looks like a trailing version, but is not.
|
|
%r{\Aiterm\Z}i => 'iterm2',
|
|
%r{\Aiterm2\Z}i => 'iterm2',
|
|
%r{\Apgadmin3\Z}i => 'pgadmin3',
|
|
%r{\Ax48\Z}i => 'x48',
|
|
%r{\Avitamin-r[\s\d\.]*\Z}i => 'vitamin-r',
|
|
%r{\Aimagealpha\Z}i => 'imagealpha',
|
|
# upstream is in the midst of changing branding
|
|
%r{\Abitcoin-?qt\Z}i => 'bitcoin-core',
|
|
# "mac" cannot be separated from the name because it is in an English phrase
|
|
%r{\Aplayonmac\Z}i => 'playonmac',
|
|
%r{\Acleanmymac[\s\d\.]*\Z}i => 'cleanmymac',
|
|
# arguably we should not have kept these two exceptions
|
|
%r{\Akismac\Z}i => 'kismac',
|
|
%r{\Avoicemac\Z}i => 'voicemac',
|
|
}
|
|
|
|
# Preserve trailing patterns on App names that could be mistaken
|
|
# for version numbers, etc
|
|
PRESERVE_TRAILING_PATS = [
|
|
%r{id3}i,
|
|
%r{mp3}i,
|
|
%r{3[\s-]*d}i,
|
|
%r{diff3}i,
|
|
%r{\A[^\d]+\+\Z}i,
|
|
]
|
|
|
|
# The code that employs these patterns against App names
|
|
# - hacks a \b (word-break) between CamelCase and snake_case transitions
|
|
# - anchors the pattern to end-of-string
|
|
# - applies the patterns repeatedly until there is no match
|
|
REMOVE_TRAILING_PATS = [
|
|
# spaces
|
|
%r{\s+}i,
|
|
|
|
# generic terms
|
|
%r{\bapp}i,
|
|
# idea, but never discussed
|
|
# %r{\blauncher}i,
|
|
|
|
# "mac", "for mac", "for OS X".
|
|
%r{\b(?:for)?[\s-]*mac(?:intosh)?}i,
|
|
%r{\b(?:for)?[\s-]*os[\s-]*x}i,
|
|
|
|
# hardware designations such as "for x86", "32-bit", "ppc"
|
|
%r{(?:\bfor\s*)?x.?86}i,
|
|
%r{(?:\bfor\s*)?\bppc}i,
|
|
%r{(?:\bfor\s*)?\d+.?bits?}i,
|
|
|
|
# frameworks
|
|
%r{\b(?:for)?[\s-]*(?:oracle|apple|sun)*[\s-]*(?:jvm|java|jre)}i,
|
|
%r{\bgtk}i,
|
|
%r{\bqt}i,
|
|
%r{\bwx}i,
|
|
%r{\bcocoa}i,
|
|
|
|
# localizations
|
|
%r{en\s*-\s*us}i,
|
|
|
|
# version numbers
|
|
%r{[^a-z0-9]+}i,
|
|
%r{\b(?:version|alpha|beta|gamma|release|release.?candidate)(?:[\s\.\d-]*\d[\s\.\d-]*)?}i,
|
|
%r{\b(?:v|ver|vsn|r|rc)[\s\.\d-]*\d[\s\.\d-]*}i,
|
|
%r{\d+(?:[a-z\.]\d+)*}i,
|
|
%r{\b\d+\s*[a-z]}i,
|
|
%r{\d+\s*[a-c]}i, # constrained to a-c b/c of false positives
|
|
]
|
|
|
|
# Patterns which are permitted (undisturbed) following an interior version number
|
|
AFTER_INTERIOR_VERSION_PATS = [
|
|
%r{ce}i,
|
|
%r{pro}i,
|
|
%r{professional}i,
|
|
%r{client}i,
|
|
%r{server}i,
|
|
%r{host}i,
|
|
%r{viewer}i,
|
|
%r{launcher}i,
|
|
%r{installer}i,
|
|
]
|
|
|
|
###
|
|
### classes
|
|
###
|
|
|
|
class AppName < String
|
|
def self.remove_trailing_pat
|
|
@@remove_trailing_pat ||= %r{(?<=.)(?:#{REMOVE_TRAILING_PATS.join('|')})\Z}i
|
|
end
|
|
|
|
def self.preserve_trailing_pat
|
|
@@preserve_trailing_pat ||= %r{(?:#{PRESERVE_TRAILING_PATS.join('|')})\Z}i
|
|
end
|
|
|
|
def self.after_interior_version_pat
|
|
@@after_interior_version_pat ||= %r{(?:#{AFTER_INTERIOR_VERSION_PATS.join('|')})}i
|
|
end
|
|
|
|
def english_from_app_bundle
|
|
return self if self.ascii_only?
|
|
return self unless File.exist?(self)
|
|
|
|
# check Info.plist CFBundleDisplayName
|
|
bundle_name = Open3.popen3(*%w[
|
|
/usr/libexec/PlistBuddy -c
|
|
],
|
|
'Print CFBundleDisplayName',
|
|
Pathname.new(self).join('Contents', 'Info.plist').to_s
|
|
) do |stdin, stdout, stderr|
|
|
begin
|
|
stdout.gets.force_encoding("UTF-8").chomp
|
|
rescue
|
|
end
|
|
end
|
|
return AppName.new(bundle_name) if bundle_name and bundle_name.ascii_only?
|
|
|
|
# check Info.plist CFBundleName
|
|
bundle_name = Open3.popen3(*%w[
|
|
/usr/libexec/PlistBuddy -c
|
|
],
|
|
'Print CFBundleName',
|
|
Pathname.new(self).join('Contents', 'Info.plist').to_s
|
|
) do |stdin, stdout, stderr|
|
|
begin
|
|
stdout.gets.force_encoding("UTF-8").chomp
|
|
rescue
|
|
end
|
|
end
|
|
return AppName.new(bundle_name) if bundle_name and bundle_name.ascii_only?
|
|
|
|
# check localization strings
|
|
local_strings_file = Pathname.new(self).join('Contents', 'Resources', 'en.lproj', 'InfoPlist.strings')
|
|
local_strings_file = Pathname.new(self).join('Contents', 'Resources', 'English.lproj', 'InfoPlist.strings') unless local_strings_file.exist?
|
|
if local_strings_file.exist?
|
|
bundle_name = File.open(local_strings_file, 'r:UTF-16LE:UTF-8') do |fh|
|
|
%r{\ACFBundle(?:Display)?Name\s*=\s*"(.*)";\Z}.match(fh.readlines.grep(/^CFBundle(?:Display)?Name\s*=\s*/).first) do |match|
|
|
match.captures.first
|
|
end
|
|
end
|
|
return AppName.new(bundle_name) if bundle_name and bundle_name.ascii_only?
|
|
end
|
|
|
|
# check Info.plist CFBundleExecutable
|
|
bundle_name = Open3.popen3(*%w[
|
|
/usr/libexec/PlistBuddy -c
|
|
],
|
|
'Print CFBundleExecutable',
|
|
Pathname.new(self).join('Contents', 'Info.plist').to_s
|
|
) do |stdin, stdout, stderr|
|
|
begin
|
|
stdout.gets.force_encoding("UTF-8").chomp
|
|
rescue
|
|
end
|
|
end
|
|
return AppName.new(bundle_name) if bundle_name and bundle_name.ascii_only?
|
|
|
|
self
|
|
end
|
|
|
|
def basename
|
|
if Pathname.new(self).exist? then
|
|
AppName.new(Pathname.new(self).basename.to_s)
|
|
else
|
|
self
|
|
end
|
|
end
|
|
|
|
def remove_extension
|
|
self.sub(/\.app\Z/i, '')
|
|
end
|
|
|
|
def decompose_to_ascii
|
|
# crudely (and incorrectly) decompose extended latin characters to ASCII
|
|
return self if self.ascii_only?
|
|
return self unless self.respond_to?(:mb_chars)
|
|
AppName.new(self.mb_chars.normalize(:kd).each_char.select(&:ascii_only?).join)
|
|
end
|
|
|
|
def hardcoded_exception
|
|
APP_EXCEPTION_PATS.each do |regexp, exception|
|
|
if regexp.match(self) then
|
|
return AppName.new(exception)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def insert_vertical_tabs_for_camel_case
|
|
app_name = AppName.new(self)
|
|
if app_name.sub!(/(#{self.class.preserve_trailing_pat})\Z/i, '')
|
|
trailing = $1
|
|
end
|
|
app_name.gsub!(/([^A-Z])([A-Z])/, "\\1\v\\2")
|
|
app_name.sub!(/\Z/, trailing) if trailing
|
|
app_name
|
|
end
|
|
|
|
def insert_vertical_tabs_for_snake_case
|
|
self.gsub(/_/, "\v")
|
|
end
|
|
|
|
def clean_up_vertical_tabs
|
|
self.gsub(/\v/, '')
|
|
end
|
|
|
|
def remove_interior_versions!
|
|
# done separately from REMOVE_TRAILING_PATS because this
|
|
# requires a substitution with a backreference
|
|
self.sub!(%r{(?<=.)[\.\d]+(#{self.class.after_interior_version_pat})\Z}i, '\1')
|
|
self.sub!(%r{(?<=.)[\s\.\d-]*\d[\s\.\d-]*(#{self.class.after_interior_version_pat})\Z}i, '-\1')
|
|
end
|
|
|
|
def remove_trailing_strings_and_versions
|
|
app_name = self.insert_vertical_tabs_for_camel_case
|
|
.insert_vertical_tabs_for_snake_case
|
|
while self.class.remove_trailing_pat.match(app_name) and
|
|
not self.class.preserve_trailing_pat.match(app_name)
|
|
app_name.sub!(self.class.remove_trailing_pat, '')
|
|
end
|
|
app_name.remove_interior_versions!
|
|
app_name.clean_up_vertical_tabs
|
|
end
|
|
|
|
def simplified
|
|
return @simplified if @simplified
|
|
@simplified = self.english_from_app_bundle
|
|
.basename
|
|
.decompose_to_ascii
|
|
.remove_extension
|
|
@simplified = @simplified.hardcoded_exception || @simplified.remove_trailing_strings_and_versions
|
|
@simplified
|
|
end
|
|
end
|
|
|
|
class CaskFileName < String
|
|
def spaces_to_hyphens
|
|
self.gsub(/ +/, '-')
|
|
end
|
|
|
|
def delete_invalid_chars
|
|
self.gsub(/[^a-z0-9-]+/, '')
|
|
end
|
|
|
|
def collapse_multiple_hyphens
|
|
self.gsub(/--+/, '-')
|
|
end
|
|
|
|
def delete_leading_hyphens
|
|
self.gsub(/^--+/, '')
|
|
end
|
|
|
|
def delete_hyphens_before_numbers
|
|
self.gsub(/-([0-9])/, '\1')
|
|
end
|
|
|
|
def spell_out_leading_numbers
|
|
cask_file_name = self
|
|
EXPANDED_NUMBERS.each do |k, v|
|
|
cask_file_name.sub!(/^#{k}/, v)
|
|
end
|
|
cask_file_name
|
|
end
|
|
|
|
def spell_out_symbols
|
|
cask_file_name = self
|
|
EXPANDED_SYMBOLS.each do |k, v|
|
|
cask_file_name.gsub!(k, " #{v} ")
|
|
end
|
|
cask_file_name.sub(/ +\Z/, '')
|
|
end
|
|
|
|
def add_extension
|
|
self.sub(/(?:#{escaped_cask_file_extension})?\Z/i, CASK_FILE_EXTENSION)
|
|
end
|
|
|
|
def remove_extension
|
|
self.sub(/#{escaped_cask_file_extension}\Z/i, '')
|
|
end
|
|
|
|
def from_simplified_app_name
|
|
return @from_simplified_app_name if @from_simplified_app_name
|
|
@from_simplified_app_name = if APP_EXCEPTION_PATS.rassoc(self.remove_extension)
|
|
self.remove_extension
|
|
else
|
|
self.remove_extension
|
|
.downcase
|
|
.spell_out_symbols
|
|
.spaces_to_hyphens
|
|
.delete_invalid_chars
|
|
.collapse_multiple_hyphens
|
|
.delete_leading_hyphens
|
|
.delete_hyphens_before_numbers
|
|
.spell_out_leading_numbers
|
|
end
|
|
raise "Could not determine Simplified App name" unless @from_simplified_app_name.length > 0
|
|
@from_simplified_app_name.add_extension
|
|
end
|
|
end
|
|
|
|
###
|
|
### methods
|
|
###
|
|
|
|
def project_root
|
|
Dir.chdir File.dirname(File.expand_path(__FILE__))
|
|
@git_root ||= Open3.popen3(*%w[
|
|
git rev-parse --show-toplevel
|
|
]) do |stdin, stdout, stderr|
|
|
begin
|
|
Pathname.new(stdout.gets.chomp)
|
|
rescue
|
|
raise "could not find project root"
|
|
end
|
|
end
|
|
raise "could not find project root" unless @git_root.exist?
|
|
@git_root
|
|
end
|
|
|
|
def escaped_cask_file_extension
|
|
@escaped_cask_file_extension ||= Regexp.escape(CASK_FILE_EXTENSION)
|
|
end
|
|
|
|
def simplified_app_name
|
|
@simplified_app_name ||= AppName.new("#{ARGV.first}".force_encoding("UTF-8")).simplified
|
|
end
|
|
|
|
def cask_file_name
|
|
@cask_file_name ||= CaskFileName.new(simplified_app_name).from_simplified_app_name
|
|
end
|
|
|
|
def cask_token
|
|
@cask_token ||= cask_file_name.remove_extension
|
|
end
|
|
|
|
def warnings
|
|
return @warnings if @warnings
|
|
@warnings = []
|
|
unless APP_EXCEPTION_PATS.rassoc(cask_token)
|
|
if %r{\d}.match(cask_token)
|
|
@warnings.push "WARNING: '#{cask_token}' contains digits. Digits which are version numbers should be removed."
|
|
end
|
|
end
|
|
filename = project_root.join('Casks', cask_file_name)
|
|
if filename.exist?
|
|
@warnings.push "WARNING: the file '#{filename}' already exists. Prepend the vendor name if this is not a duplicate."
|
|
end
|
|
@warnings
|
|
end
|
|
|
|
def report
|
|
puts "Proposed Simplified App name: #{simplified_app_name}" if $debug
|
|
puts "Proposed token: #{cask_token}"
|
|
puts "Proposed file name: #{cask_file_name}"
|
|
puts "Cask Header Line: cask :v1 => '#{cask_token}' do"
|
|
if warnings.length > 0
|
|
STDERR.puts "\n"
|
|
STDERR.puts warnings
|
|
STDERR.puts "\n"
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
###
|
|
### main
|
|
###
|
|
|
|
usage = <<-EOS
|
|
Usage: generate_cask_token [ -debug ] <application.app>
|
|
|
|
Given an Application name or a path to an Application, propose a
|
|
Cask token, filename, and header line.
|
|
|
|
With -debug, also provide the internal "Simplified App Name".
|
|
|
|
EOS
|
|
|
|
if ARGV.first =~ %r{^-+h(elp)?$}i
|
|
puts usage
|
|
exit 0
|
|
end
|
|
|
|
if ARGV.first =~ %r{^-+debug?$}i
|
|
$debug = 1
|
|
ARGV.shift
|
|
end
|
|
|
|
unless ARGV.length == 1
|
|
puts usage
|
|
exit 1
|
|
end
|
|
|
|
report
|