ruby/lib/bundler/cli/list.rb
Schneems 813603994a [rubygems/rubygems] Introduce bundle list --format=json
The `bundle list` command is a convenient way for human to know what gems and versions are available. By introducing a `--format=json` option, we can provide the same information to machines in a stable format that is robust to UI additions or modifications. It indirectly supports  `Gemfile.lock` modifications by discouraging external tools from attempting to parse that format.

This addition allows for the scripting of installation tools, such as buildpacks, that wish to branch logic based on gem versions. For example:

```ruby
require "json"

command = "bundle list --format=json"
output = `#{command}`
raise "Command `#{command}` errored: #{output}" unless $?.success?

railties = JSON.parse(output).find {|gem| gem["name"] == railties }
if railties && Gem::Version.new(railties["version"]) >= Gem::Version.new("7")
  puts "Using Rails greater than 7!"
end
```

The top level is an object with a single key, "gems", this structure allows us to add other information in the future (should we desire) without having to change the json schema.

https://github.com/rubygems/rubygems/commit/9e081b0689
2025-08-18 12:31:51 +09:00

98 lines
2.8 KiB
Ruby

# frozen_string_literal: true
require "json"
module Bundler
class CLI::List
def initialize(options)
@options = options
@without_group = options["without-group"].map(&:to_sym)
@only_group = options["only-group"].map(&:to_sym)
@format = options["format"]
end
def run
raise InvalidOption, "The `--only-group` and `--without-group` options cannot be used together" if @only_group.any? && @without_group.any?
raise InvalidOption, "The `--name-only` and `--paths` options cannot be used together" if @options["name-only"] && @options[:paths]
specs = if @only_group.any? || @without_group.any?
filtered_specs_by_groups
else
begin
Bundler.load.specs
rescue GemNotFound => e
Bundler.ui.error e.message
Bundler.ui.warn "Install missing gems with `bundle install`."
exit 1
end
end.reject {|s| s.name == "bundler" }.sort_by(&:name)
case @format
when "json"
print_json(specs: specs)
when nil
print_human(specs: specs)
else
raise InvalidOption, "Unknown option`--format=#{@format}`. Supported formats: `json`"
end
end
private
def print_json(specs:)
gems = if @options["name-only"]
specs.map {|s| { name: s.name } }
else
specs.map do |s|
{
name: s.name,
version: s.version.to_s,
git_version: s.git_version&.strip,
}.tap do |h|
h[:path] = s.full_gem_path if @options["paths"]
end
end
end
Bundler.ui.info({ gems: gems }.to_json)
end
def print_human(specs:)
return Bundler.ui.info "No gems in the Gemfile" if specs.empty?
return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"]
return specs.each {|s| Bundler.ui.info s.full_gem_path } if @options["paths"]
Bundler.ui.info "Gems included by the bundle:"
specs.each {|s| Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" }
Bundler.ui.info "Use `bundle info` to print more detailed information about a gem"
end
def verify_group_exists(groups)
(@without_group + @only_group).each do |group|
raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group)
end
end
def filtered_specs_by_groups
definition = Bundler.definition
groups = definition.groups
verify_group_exists(groups)
show_groups =
if @without_group.any?
groups.reject {|g| @without_group.include?(g) }
elsif @only_group.any?
groups.select {|g| @only_group.include?(g) }
else
groups
end.map(&:to_sym)
definition.specs_for(show_groups)
end
end
end