Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 197 additions & 12 deletions Library/Homebrew/cmd/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
require "cask/list"
require "system_command"
require "tab"
require "json"
require "descriptions"

module Homebrew
module Cmd
Expand All @@ -24,6 +26,8 @@ class List < AbstractCommand
description: "List only formulae, or treat all named arguments as formulae."
switch "--cask", "--casks",
description: "List only casks, or treat all named arguments as casks."
switch "--eval-all",
description: "List all available packages in the default tap, whether installed or not."
switch "--full-name",
description: "Print formulae with fully-qualified names. Unless `--full-name`, `--versions` " \
"or `--pinned` are passed, other options (i.e. `-1`, `-l`, `-r` and `-t`) are " \
Expand All @@ -45,6 +49,12 @@ class List < AbstractCommand
description: "List the formulae installed from a bottle."
switch "--built-from-source",
description: "List the formulae compiled from source."
switch "--desc",
description: "Show descriptions for listed formulae and casks."
flag "--json",
description: "Print output in JSON format. There are two versions: `v1` and `v2`. " \
"`v1` is deprecated and is currently the default if no version is specified. " \
"`v2` prints all packages (or optionally only formulae or casks)."

# passed through to ls
switch "-1",
Expand All @@ -64,6 +74,11 @@ class List < AbstractCommand
conflicts "--pinned", "--cask"
conflicts "--multiple", "--cask"
conflicts "--pinned", "--multiple"
conflicts "--eval-all", "--pinned"
conflicts "--eval-all", "--installed-on-request"
conflicts "--eval-all", "--installed-as-dependency"
conflicts "--eval-all", "--poured-from-bottle"
conflicts "--eval-all", "--built-from-source"
["--installed-on-request", "--installed-as-dependency",
"--poured-from-bottle", "--built-from-source"].each do |flag|
conflicts "--cask", flag
Expand All @@ -79,22 +94,88 @@ class List < AbstractCommand
conflicts "--full-name", flag
end

["--versions", "--pinned", "--installed-on-request", "--installed-as-dependency",
"--poured-from-bottle", "--built-from-source", "-1", "-l", "-r", "-t"].each do |flag|
conflicts "--desc", flag
end

named_args [:installed_formula, :installed_cask]
end

sig { override.void }
def run
if args.full_name? &&
!(args.installed_on_request? || args.installed_as_dependency? ||
args.poured_from_bottle? || args.built_from_source?)
case json_version(T.unsafe(args).json)
when :v2
output_json_v2
else
legacy_list
Comment on lines 107 to 113

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor list --json default and v1 outputs

The new run case only emits JSON when json_version returns :v2; --json (which maps to :default) and --json=v1 now fall through to legacy_list and print the normal text listing. The CLI description still promises JSON output with v1 as the default, so brew list --json/--json=v1 will no longer return JSON at all.

Useful? React with 👍 / 👎.

end
end

sig { void }
def output_json_v2
formulae = if T.unsafe(args).eval_all?
if T.unsafe(args).formula? || (!T.unsafe(args).cask? && !T.unsafe(args).formula?)
Formula.all(eval_all: true)
else
[]
end
elsif T.unsafe(args).cask?
[]
else
args.no_named? ? Formula.installed : args.named.to_resolved_formulae
end

casks = if T.unsafe(args).eval_all?
if T.unsafe(args).cask? || (!T.unsafe(args).cask? && !T.unsafe(args).formula?)
Cask::Cask.all(eval_all: true)
else
[]
end
elsif T.unsafe(args).cask?
args.no_named? ? Cask::Cask.all(eval_all: true) : args.named.to_casks
elsif T.unsafe(args).formula?
[]
else
args.no_named? ? Cask::Caskroom.casks : args.named.to_casks
end

json = {
formulae: json_info(formulae),
casks: json_info_casks(casks),
}
# json v2.8.1 is inconsistent in how it renders empty arrays,
# so we use `[]` for consistency:
puts JSON.pretty_generate(json).gsub(/\[\n\n\s*\]/, "[]")
end

sig { void }
def legacy_list
if args.desc?
list_descriptions
elsif args.full_name? &&
!(args.installed_on_request? || args.installed_as_dependency? ||
args.poured_from_bottle? || args.built_from_source?)
unless args.cask?
formula_names = args.no_named? ? Formula.installed : args.named.to_resolved_formulae
formula_names = if T.unsafe(args).eval_all?
Formula.all(eval_all: true)
elsif args.no_named?
Formula.installed
else
args.named.to_resolved_formulae
end
full_formula_names = formula_names.map(&:full_name).sort(&tap_and_name_comparison)
full_formula_names = Formatter.columns(full_formula_names) unless args.public_send(:"1?")
puts full_formula_names if full_formula_names.present?
end
if args.cask? || (!args.formula? && args.no_named?)
cask_names = if args.no_named?
cask_names = if T.unsafe(args).eval_all?
if args.no_named?
Cask::Cask.all(eval_all: true)
else
args.named.to_casks
end
elsif args.no_named?
Cask::Caskroom.casks
else
args.named.to_formulae_and_casks(only: :cask, method: :resolve)
Expand Down Expand Up @@ -157,14 +238,32 @@ def run
ls_args << "-r" if args.r?
ls_args << "-t" if args.t?

if !args.cask? && HOMEBREW_CELLAR.exist? && HOMEBREW_CELLAR.children.any?
ohai "Formulae" if $stdout.tty? && !args.formula?
safe_system "ls", *ls_args, HOMEBREW_CELLAR
puts if $stdout.tty? && !args.formula?
# List formulae if --cask is not specified
unless args.cask?
if T.unsafe(args).eval_all?
# List all available formulae
ohai "Formulae" if $stdout.tty?
all_formulae = Formula.all(eval_all: true).map(&:name).sort
puts Formatter.columns(all_formulae) unless all_formulae.empty?
puts if $stdout.tty? && !args.formula?
elsif HOMEBREW_CELLAR.exist? && HOMEBREW_CELLAR.children.any?
ohai "Formulae" if $stdout.tty? && !args.formula?
safe_system "ls", *ls_args, HOMEBREW_CELLAR
puts if $stdout.tty? && !args.formula?
end
end
if !args.formula? && Cask::Caskroom.any_casks_installed?
ohai "Casks" if $stdout.tty? && !args.cask?
safe_system "ls", *ls_args, Cask::Caskroom.path

# List casks if --formula is not specified
unless args.formula?
if T.unsafe(args).eval_all?
# List all available casks
ohai "Casks" if $stdout.tty?
all_casks = Cask::Cask.all(eval_all: true).map(&:token).sort
puts Formatter.columns(all_casks) unless all_casks.empty?
elsif Cask::Caskroom.any_casks_installed?
ohai "Casks" if $stdout.tty? && !args.cask?
safe_system "ls", *ls_args, Cask::Caskroom.path
end
end
else
kegs, casks = args.named.to_kegs_to_casks
Expand All @@ -180,8 +279,94 @@ def run
end
end

sig { params(version: T.nilable(T.any(TrueClass, String))).returns(T.nilable(Symbol)) }
def json_version(version)
version_hash = {
nil => nil,
true => :default,
"v1" => :v1,
"v2" => :v2,
}
version_hash.fetch(version) { raise UsageError, "invalid JSON version: #{version}" }
end

sig {
params(
formulae: T::Array[Formula],
).returns(T::Array[T::Hash[Symbol, T.untyped]])
}
def json_info(formulae)
formulae.map do |formula|
{
name: formula.full_name,
full_name: formula.full_name,
version: formula.version.to_s,
installed: formula.rack.exist?,
desc: formula.desc,
}
end
end

sig {
params(
casks: T::Array[Cask::Cask],
).returns(T::Array[T::Hash[Symbol, T.untyped]])
}
def json_info_casks(casks)
casks.map do |cask|
{
name: cask.token,
full_name: cask.full_name,
version: cask.version.to_s,
installed: cask.installed?,
desc: cask.desc,
}
end
end

private

sig { void }
def list_descriptions
formulae = if T.unsafe(args).eval_all?
if T.unsafe(args).formula? || (!T.unsafe(args).cask? && !T.unsafe(args).formula?)
Formula.all(eval_all: true)
else
[]
end
elsif T.unsafe(args).cask?
[]
else
args.no_named? ? Formula.installed : args.named.to_resolved_formulae
end

casks = if T.unsafe(args).eval_all?
if T.unsafe(args).cask? || (!T.unsafe(args).cask? && !T.unsafe(args).formula?)
Cask::Cask.all(eval_all: true)
else
[]
end
elsif T.unsafe(args).cask?
args.no_named? ? Cask::Cask.all(eval_all: true) : args.named.to_casks

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge --desc --cask dumps all casks instead of installed set

In list_descriptions, the --cask branch always loads Cask::Cask.all(eval_all: true) when no names are provided, even without --eval-all. That means brew list --cask --desc now scans and prints descriptions for every available cask rather than the installed casks that brew list --cask would normally show, producing incorrect and extremely large output.

Useful? React with 👍 / 👎.

elsif T.unsafe(args).formula?
[]
else
args.no_named? ? Cask::Caskroom.casks : args.named.to_casks
end

descriptions = {}

formulae.each do |formula|
descriptions[formula.full_name] = formula.desc
end

casks.each do |cask|
descriptions[cask.full_name] = [cask.name.join(", "), cask.desc.presence]
end

Descriptions.new(descriptions).print
end

sig { void }
def filtered_list
names = if args.no_named?
Expand Down
3 changes: 3 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/list.rbi

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

43 changes: 43 additions & 0 deletions Library/Homebrew/test/cmd/list_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,47 @@
.to be_a_success
.and not_to_output.to_stderr
end

it "prints JSON output with --json=v2", :integration_test do
formulae.each do |f|
(HOMEBREW_CELLAR/f/"1.0/somedir").mkpath
end

expect { brew "list", "--json=v2", "--formula" }
.to output(/\{.*"formulae".*\}/).to_stdout
.and not_to_output.to_stderr
.and be_a_success
end

it "evaluates all formulae with --eval-all --json=v2", :integration_test do
expect { brew "list", "--eval-all", "--json=v2", "--formula" }
.to output(/\{.*"formulae".*\}/).to_stdout
.and not_to_output.to_stderr
.and be_a_success
end

it "evaluates all casks with --eval-all --json=v2", :integration_test do
expect { brew "list", "--eval-all", "--json=v2", "--cask" }
.to output(/\{.*"casks".*\}/).to_stdout
.and not_to_output.to_stderr
.and be_a_success
end

it "evaluates all packages with --eval-all without --json", :integration_test do
expect { brew "list", "--eval-all", "--formula" }
.to be_a_success
.and not_to_output.to_stderr
end

it "evaluates all casks with --eval-all without --json", :integration_test do
expect { brew "list", "--eval-all", "--cask" }
.to be_a_success
.and not_to_output.to_stderr
end

it "evaluates all packages with --eval-all without --json or flags", :integration_test do
expect { brew_sh "list", "--eval-all" }
.to be_a_success
.and not_to_output.to_stderr
end
end
Loading