Add bump-unversioned-casks
command.
This commit is contained in:
parent
ff28d7c69b
commit
b57a448f2a
@ -40,6 +40,10 @@ module Cask
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def time_file_size
|
||||||
|
downloader.resolved_time_file_size
|
||||||
|
end
|
||||||
|
|
||||||
def clear_cache
|
def clear_cache
|
||||||
downloader.clear_cache
|
downloader.clear_cache
|
||||||
end
|
end
|
||||||
|
@ -30,7 +30,8 @@ module Cask
|
|||||||
def initialize(cask, command: SystemCommand, force: false,
|
def initialize(cask, command: SystemCommand, force: false,
|
||||||
skip_cask_deps: false, binaries: true, verbose: false,
|
skip_cask_deps: false, binaries: true, verbose: false,
|
||||||
require_sha: false, upgrade: false,
|
require_sha: false, upgrade: false,
|
||||||
installed_as_dependency: false, quarantine: true)
|
installed_as_dependency: false, quarantine: true,
|
||||||
|
verify_download_integrity: true)
|
||||||
@cask = cask
|
@cask = cask
|
||||||
@command = command
|
@command = command
|
||||||
@force = force
|
@force = force
|
||||||
@ -42,6 +43,7 @@ module Cask
|
|||||||
@upgrade = upgrade
|
@upgrade = upgrade
|
||||||
@installed_as_dependency = installed_as_dependency
|
@installed_as_dependency = installed_as_dependency
|
||||||
@quarantine = quarantine
|
@quarantine = quarantine
|
||||||
|
@verify_download_integrity = verify_download_integrity
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?,
|
attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?,
|
||||||
@ -150,13 +152,10 @@ module Cask
|
|||||||
s.freeze
|
s.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
sig { returns(Pathname) }
|
||||||
def download
|
def download
|
||||||
return @downloaded_path if @downloaded_path
|
@download ||= Download.new(@cask, quarantine: quarantine?)
|
||||||
|
.fetch(verify_download_integrity: @verify_download_integrity)
|
||||||
odebug "Downloading"
|
|
||||||
@downloaded_path = Download.new(@cask, quarantine: quarantine?).fetch
|
|
||||||
odebug "Downloaded to -> #{@downloaded_path}"
|
|
||||||
@downloaded_path
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_has_sha
|
def verify_has_sha
|
||||||
@ -171,15 +170,15 @@ module Cask
|
|||||||
|
|
||||||
def primary_container
|
def primary_container
|
||||||
@primary_container ||= begin
|
@primary_container ||= begin
|
||||||
download
|
downloaded_path = download
|
||||||
UnpackStrategy.detect(@downloaded_path, type: @cask.container&.type, merge_xattrs: true)
|
UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_primary_container
|
def extract_primary_container(to: @cask.staged_path)
|
||||||
odebug "Extracting primary container"
|
odebug "Extracting primary container"
|
||||||
|
|
||||||
odebug "Using container class #{primary_container.class} for #{@downloaded_path}"
|
odebug "Using container class #{primary_container.class} for #{primary_container.path}"
|
||||||
|
|
||||||
basename = CGI.unescape(File.basename(@cask.url.path))
|
basename = CGI.unescape(File.basename(@cask.url.path))
|
||||||
|
|
||||||
@ -191,16 +190,16 @@ module Cask
|
|||||||
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose?
|
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose?
|
||||||
|
|
||||||
UnpackStrategy.detect(tmpdir/nested_container, merge_xattrs: true)
|
UnpackStrategy.detect(tmpdir/nested_container, merge_xattrs: true)
|
||||||
.extract_nestedly(to: @cask.staged_path, verbose: verbose?)
|
.extract_nestedly(to: to, verbose: verbose?)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
primary_container.extract_nestedly(to: @cask.staged_path, basename: basename, verbose: verbose?)
|
primary_container.extract_nestedly(to: to, basename: basename, verbose: verbose?)
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless quarantine?
|
return unless quarantine?
|
||||||
return unless Quarantine.available?
|
return unless Quarantine.available?
|
||||||
|
|
||||||
Quarantine.propagate(from: @downloaded_path, to: @cask.staged_path)
|
Quarantine.propagate(from: primary_container.path, to: to)
|
||||||
end
|
end
|
||||||
|
|
||||||
def install_artifacts
|
def install_artifacts
|
||||||
|
@ -72,9 +72,10 @@ module Homebrew
|
|||||||
new_version = Cask::DSL::Version.new(new_version)
|
new_version = Cask::DSL::Version.new(new_version)
|
||||||
new_base_url = args.url
|
new_base_url = args.url
|
||||||
new_hash = args.sha256
|
new_hash = args.sha256
|
||||||
|
new_hash = :no_check if new_hash == ":no_check"
|
||||||
|
|
||||||
old_version = cask.version
|
old_version = cask.version
|
||||||
old_hash = cask.sha256.to_s
|
old_hash = cask.sha256
|
||||||
|
|
||||||
tap_full_name = cask.tap&.full_name
|
tap_full_name = cask.tap&.full_name
|
||||||
default_remote_branch = cask.tap.path.git_origin_branch if cask.tap
|
default_remote_branch = cask.tap.path.git_origin_branch if cask.tap
|
||||||
@ -95,7 +96,7 @@ module Homebrew
|
|||||||
elsif old_version.latest?
|
elsif old_version.latest?
|
||||||
opoo "No --url= argument specified!" unless new_base_url
|
opoo "No --url= argument specified!" unless new_base_url
|
||||||
elsif new_version.latest?
|
elsif new_version.latest?
|
||||||
opoo "Ignoring specified --sha256= argument." if new_hash
|
opoo "Ignoring specified --sha256= argument." if new_hash && new_check != :no_check
|
||||||
elsif Version.new(new_version) < Version.new(old_version)
|
elsif Version.new(new_version) < Version.new(old_version)
|
||||||
odie <<~EOS
|
odie <<~EOS
|
||||||
You need to bump this cask manually since changing the
|
You need to bump this cask manually since changing the
|
||||||
@ -136,7 +137,9 @@ module Homebrew
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
if !new_version.latest? && (new_hash.nil? || cask.languages.present?)
|
if new_version.latest?
|
||||||
|
new_hash = :no_check
|
||||||
|
elsif new_hash.nil? || cask.languages.present?
|
||||||
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
|
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
|
||||||
replacement_pairs.uniq.compact,
|
replacement_pairs.uniq.compact,
|
||||||
read_only_run: true,
|
read_only_run: true,
|
||||||
@ -172,15 +175,14 @@ module Homebrew
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
replacement_pairs << if old_version.latest?
|
p old_hash
|
||||||
|
|
||||||
|
replacement_pairs << if old_version.latest? || new_version.latest? || new_hash == :no_check
|
||||||
|
hash_regex = old_hash == :no_check ? ":no_check" : "[\"']#{Regexp.escape(old_hash.to_s)}[\"']"
|
||||||
|
|
||||||
[
|
[
|
||||||
"sha256 :no_check",
|
/sha256\s+#{hash_regex}/m,
|
||||||
"sha256 \"#{new_hash}\"",
|
"sha256 #{new_hash == :no_check ? ":no_check" : "\"#{new_hash}\""}",
|
||||||
]
|
|
||||||
elsif new_version.latest?
|
|
||||||
[
|
|
||||||
"sha256 \"#{old_hash}\"",
|
|
||||||
"sha256 :no_check",
|
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
[
|
[
|
||||||
|
201
Library/Homebrew/dev-cmd/bump-unversioned-casks.rb
Normal file
201
Library/Homebrew/dev-cmd/bump-unversioned-casks.rb
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
# typed: false
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "cask/download"
|
||||||
|
require "cask/installer"
|
||||||
|
require "cask/cask_loader"
|
||||||
|
require "cli/parser"
|
||||||
|
require "tap"
|
||||||
|
|
||||||
|
module Homebrew
|
||||||
|
extend T::Sig
|
||||||
|
|
||||||
|
extend SystemCommand::Mixin
|
||||||
|
|
||||||
|
sig { returns(CLI::Parser) }
|
||||||
|
def self.bump_unversioned_casks_args
|
||||||
|
Homebrew::CLI::Parser.new do
|
||||||
|
usage_banner <<~EOS
|
||||||
|
`bump-unversioned-casks` [<options>] [<tap>]
|
||||||
|
|
||||||
|
Check all casks with unversioned URLs in a given <tap> for updates.
|
||||||
|
EOS
|
||||||
|
switch "-n", "--dry-run",
|
||||||
|
description: "List what would be done, but do not actually do anything."
|
||||||
|
flag "--limit=",
|
||||||
|
description: "Maximum number of casks to update."
|
||||||
|
flag "--state-file=",
|
||||||
|
description: "File for keeping track of state."
|
||||||
|
|
||||||
|
named 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { void }
|
||||||
|
def self.bump_unversioned_casks
|
||||||
|
args = bump_unversioned_casks_args.parse
|
||||||
|
|
||||||
|
state_file = if args.state_file.present?
|
||||||
|
Pathname(args.state_file).expand_path
|
||||||
|
else
|
||||||
|
HOMEBREW_CACHE/"bump_unversioned_casks.json"
|
||||||
|
end
|
||||||
|
state_file.dirname.mkpath
|
||||||
|
|
||||||
|
tap = Tap.fetch(args.named.first)
|
||||||
|
|
||||||
|
old_state = state_file.exist? ? JSON.parse(state_file.read) : {}
|
||||||
|
|
||||||
|
new_state = {}
|
||||||
|
|
||||||
|
cask_files = tap.cask_files
|
||||||
|
unversioned_cask_files = cask_files.select do |cask_file|
|
||||||
|
url = cask_file.each_line do |line|
|
||||||
|
url = line[/\s*url\s+"([^"]+)"\s*/, 1]
|
||||||
|
break url if url
|
||||||
|
end
|
||||||
|
|
||||||
|
url.present? && url.exclude?('#{')
|
||||||
|
end.sort
|
||||||
|
|
||||||
|
unversioned_casks = unversioned_cask_files.map { |path| Cask::CaskLoader.load(path) }
|
||||||
|
|
||||||
|
ohai "Static Casks:"
|
||||||
|
puts "Total: #{unversioned_casks.count}"
|
||||||
|
puts "Single-App: #{unversioned_casks.count { |c| single_app_cask?(c) }}"
|
||||||
|
puts "Single-Pkg: #{unversioned_casks.count { |c| single_pkg_cask?(c) }}"
|
||||||
|
|
||||||
|
limit = args.limit.presence&.to_i || unversioned_casks.count
|
||||||
|
|
||||||
|
unversioned_casks.shuffle.each do |cask|
|
||||||
|
ohai "Checking #{cask.full_name}"
|
||||||
|
|
||||||
|
unless single_app_cask?(cask)
|
||||||
|
opoo "Skipping, not a single-app cask."
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
download = Cask::Download.new(cask)
|
||||||
|
time, file_size = begin
|
||||||
|
download.time_file_size
|
||||||
|
rescue
|
||||||
|
opoo "Skipping, cannot get time and file size."
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
odebug "Time: #{time.inspect}"
|
||||||
|
odebug "Size: #{file_size.inspect}"
|
||||||
|
|
||||||
|
last_state = old_state.fetch(cask.full_name, {})
|
||||||
|
last_check_time = last_state["check_time"]&.yield_self { |t| Time.parse(t) }
|
||||||
|
|
||||||
|
check_time = Time.now
|
||||||
|
if last_check_time && check_time < (last_check_time + 1.day)
|
||||||
|
opoo "Skipping, already checked within the last 24 hours."
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
last_sha256 = last_state["sha256"]
|
||||||
|
last_time = last_state["time"]&.yield_self { |t| Time.parse(t) }
|
||||||
|
last_file_size = last_state["file_size"]
|
||||||
|
|
||||||
|
next if last_time == time && last_file_size == file_size
|
||||||
|
|
||||||
|
installer = Cask::Installer.new(cask, verify_download_integrity: false)
|
||||||
|
|
||||||
|
begin
|
||||||
|
cached_download = installer.download
|
||||||
|
rescue => e
|
||||||
|
onoe e
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
sha256 = cached_download.sha256
|
||||||
|
|
||||||
|
if last_sha256 != sha256 && (version = guess_cask_version(cask, installer))
|
||||||
|
odebug "Version: #{version.inspect}"
|
||||||
|
|
||||||
|
if cask.version == version
|
||||||
|
oh1 "Cask #{cask} is up-to-date at #{version}"
|
||||||
|
else
|
||||||
|
bump_cask_pr_args = [
|
||||||
|
"bump-cask-pr",
|
||||||
|
"--version", version.to_s,
|
||||||
|
"--sha256", ":no_check",
|
||||||
|
"--message", "Automatic update via `brew bump-unversioned-casks`.",
|
||||||
|
cask.sourcefile_path
|
||||||
|
]
|
||||||
|
|
||||||
|
if args.dry_run?
|
||||||
|
bump_cask_pr_args << "--dry-run"
|
||||||
|
oh1 "Would bump #{cask} from #{cask.version} to #{version}"
|
||||||
|
else
|
||||||
|
oh1 "Bumping #{cask} from #{cask.version} to #{version}"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
system_command! HOMEBREW_BREW_FILE, args: bump_cask_pr_args
|
||||||
|
rescue ErrorDuringExecution => e
|
||||||
|
onoe e
|
||||||
|
Homebrew.failed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless args.dry_run?
|
||||||
|
new_state[cask.full_name] = {
|
||||||
|
"sha256" => sha256,
|
||||||
|
"check_time" => check_time.iso8601,
|
||||||
|
"time" => time&.iso8601,
|
||||||
|
"file_size" => file_size,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
break if (limit -= 1).zero?
|
||||||
|
end
|
||||||
|
|
||||||
|
state_file.atomic_write JSON.pretty_generate(old_state.merge(new_state))
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(cask: Cask::Cask, installer: Cask::Installer).returns(T.nilable(String)) }
|
||||||
|
def self.guess_cask_version(cask, installer)
|
||||||
|
apps = cask.artifacts.select { |a| a.is_a?(Cask::Artifact::App) }
|
||||||
|
return if apps.count != 1
|
||||||
|
|
||||||
|
Dir.mktmpdir do |dir|
|
||||||
|
dir = Pathname(dir)
|
||||||
|
|
||||||
|
installer.extract_primary_container(to: dir)
|
||||||
|
|
||||||
|
plists = apps.flat_map do |app|
|
||||||
|
Pathname.glob(dir/"**"/app.source.basename/"Contents"/"Info.plist")
|
||||||
|
end
|
||||||
|
next if plists.empty?
|
||||||
|
|
||||||
|
plist = plists.first
|
||||||
|
|
||||||
|
system_command! "plutil", args: ["-convert", "xml1", plist]
|
||||||
|
plist = Plist.parse_xml(plist.read)
|
||||||
|
|
||||||
|
short_version = plist["CFBundleShortVersionString"]
|
||||||
|
version = plist["CFBundleVersion"]
|
||||||
|
|
||||||
|
return "#{short_version},#{version}" if cask.version.include?(",")
|
||||||
|
|
||||||
|
return cask.version.to_s if [short_version, version].include?(cask.version.to_s)
|
||||||
|
|
||||||
|
return short_version if short_version&.match(/\A\d+(\.\d+)+\Z/)
|
||||||
|
return version if version&.match(/\A\d+(\.\d+)+\Z/)
|
||||||
|
|
||||||
|
short_version || version
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.single_app_cask?(cask)
|
||||||
|
cask.artifacts.count { |a| a.is_a?(Cask::Artifact::App) } == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.single_pkg_cask?(cask)
|
||||||
|
cask.artifacts.count { |a| a.is_a?(Cask::Artifact::Pkg) } == 1
|
||||||
|
end
|
||||||
|
end
|
@ -356,7 +356,7 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
|
|||||||
|
|
||||||
ohai "Downloading #{url}"
|
ohai "Downloading #{url}"
|
||||||
|
|
||||||
resolved_url, _, url_time = resolve_url_basename_time(url)
|
resolved_url, _, url_time, = resolve_url_basename_time_file_size(url)
|
||||||
|
|
||||||
fresh = if cached_location.exist? && url_time
|
fresh = if cached_location.exist? && url_time
|
||||||
url_time <= cached_location.mtime
|
url_time <= cached_location.mtime
|
||||||
@ -398,14 +398,19 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
|
|||||||
rm_rf(temporary_path)
|
rm_rf(temporary_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resolved_time_file_size
|
||||||
|
_, _, time, file_size = resolve_url_basename_time_file_size(url)
|
||||||
|
[time, file_size]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def resolved_url_and_basename
|
def resolved_url_and_basename
|
||||||
resolved_url, basename, = resolve_url_basename_time(url)
|
resolved_url, basename, = resolve_url_basename_time_file_size(url)
|
||||||
[resolved_url, basename]
|
[resolved_url, basename]
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_url_basename_time(url)
|
def resolve_url_basename_time_file_size(url)
|
||||||
@resolved_info_cache ||= {}
|
@resolved_info_cache ||= {}
|
||||||
return @resolved_info_cache[url] if @resolved_info_cache.include?(url)
|
return @resolved_info_cache[url] if @resolved_info_cache.include?(url)
|
||||||
|
|
||||||
@ -458,9 +463,15 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
|
|||||||
.map { |t| t.match?(/^\d+$/) ? Time.at(t.to_i) : Time.parse(t) }
|
.map { |t| t.match?(/^\d+$/) ? Time.at(t.to_i) : Time.parse(t) }
|
||||||
.last
|
.last
|
||||||
|
|
||||||
|
file_size =
|
||||||
|
lines.map { |line| line[/^Content-Length:\s*(\d+)/i, 1] }
|
||||||
|
.compact
|
||||||
|
.map(&:to_i)
|
||||||
|
.last
|
||||||
|
|
||||||
basename = filenames.last || parse_basename(redirect_url)
|
basename = filenames.last || parse_basename(redirect_url)
|
||||||
|
|
||||||
@resolved_info_cache[url] = [redirect_url, basename, time]
|
@resolved_info_cache[url] = [redirect_url, basename, time, file_size]
|
||||||
end
|
end
|
||||||
|
|
||||||
def _fetch(url:, resolved_url:)
|
def _fetch(url:, resolved_url:)
|
||||||
@ -526,7 +537,7 @@ class CurlApacheMirrorDownloadStrategy < CurlDownloadStrategy
|
|||||||
@combined_mirrors = [*@mirrors, *backup_mirrors]
|
@combined_mirrors = [*@mirrors, *backup_mirrors]
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_url_basename_time(url)
|
def resolve_url_basename_time_file_size(url)
|
||||||
if url == self.url
|
if url == self.url
|
||||||
super("#{apache_mirrors["preferred"]}#{apache_mirrors["path_info"]}")
|
super("#{apache_mirrors["preferred"]}#{apache_mirrors["path_info"]}")
|
||||||
else
|
else
|
||||||
|
@ -736,11 +736,12 @@ module GitHub
|
|||||||
EOS
|
EOS
|
||||||
user_message = args.message
|
user_message = args.message
|
||||||
if user_message
|
if user_message
|
||||||
pr_message += <<~EOS
|
pr_message = <<~EOS
|
||||||
|
#{user_message}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#{user_message}
|
#{pr_message}
|
||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ bump
|
|||||||
bump-cask-pr
|
bump-cask-pr
|
||||||
bump-formula-pr
|
bump-formula-pr
|
||||||
bump-revision
|
bump-revision
|
||||||
|
bump-unversioned-casks
|
||||||
cask
|
cask
|
||||||
cat
|
cat
|
||||||
cleanup
|
cleanup
|
||||||
|
@ -899,6 +899,17 @@ present, "revision 1" will be added.
|
|||||||
* `--message`:
|
* `--message`:
|
||||||
Append *`message`* to the default commit message.
|
Append *`message`* to the default commit message.
|
||||||
|
|
||||||
|
### `bump-unversioned-casks` [*`options`*] [*`tap`*]
|
||||||
|
|
||||||
|
Check all casks with unversioned URLs in a given *`tap`* for updates.
|
||||||
|
|
||||||
|
* `-n`, `--dry-run`:
|
||||||
|
List what would be done, but do not actually do anything.
|
||||||
|
* `--limit`:
|
||||||
|
Maximum number of casks to update.
|
||||||
|
* `--state-file`:
|
||||||
|
File for keeping track of state.
|
||||||
|
|
||||||
### `cat` *`formula`*|*`cask`*
|
### `cat` *`formula`*|*`cask`*
|
||||||
|
|
||||||
Display the source of a *`formula`* or *`cask`*.
|
Display the source of a *`formula`* or *`cask`*.
|
||||||
|
@ -1245,6 +1245,21 @@ Print what would be done rather than doing it\.
|
|||||||
\fB\-\-message\fR
|
\fB\-\-message\fR
|
||||||
Append \fImessage\fR to the default commit message\.
|
Append \fImessage\fR to the default commit message\.
|
||||||
.
|
.
|
||||||
|
.SS "\fBbump\-unversioned\-casks\fR [\fIoptions\fR] [\fItap\fR]"
|
||||||
|
Check all casks with unversioned URLs in a given \fItap\fR for updates\.
|
||||||
|
.
|
||||||
|
.TP
|
||||||
|
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||||
|
List what would be done, but do not actually do anything\.
|
||||||
|
.
|
||||||
|
.TP
|
||||||
|
\fB\-\-limit\fR
|
||||||
|
Maximum number of casks to update\.
|
||||||
|
.
|
||||||
|
.TP
|
||||||
|
\fB\-\-state\-file\fR
|
||||||
|
File for keeping track of state\.
|
||||||
|
.
|
||||||
.SS "\fBcat\fR \fIformula\fR|\fIcask\fR"
|
.SS "\fBcat\fR \fIformula\fR|\fIcask\fR"
|
||||||
Display the source of a \fIformula\fR or \fIcask\fR\.
|
Display the source of a \fIformula\fR or \fIcask\fR\.
|
||||||
.
|
.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user