Skip to content

Commit 8136039

Browse files
schneemshsbt
authored andcommitted
[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. ruby/rubygems@9e081b0689
1 parent 8d5f00c commit 8136039

File tree

5 files changed

+152
-2
lines changed

5 files changed

+152
-2
lines changed

lib/bundler/cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def show(gem_name = nil)
299299
method_option "name-only", type: :boolean, banner: "print only the gem names"
300300
method_option "only-group", type: :array, default: [], banner: "print gems from a given set of groups"
301301
method_option "without-group", type: :array, default: [], banner: "print all gems except from a given set of groups"
302+
method_option "format", type: :string, banner: "format output ('json' is the only supported format)"
302303
method_option "paths", type: :boolean, banner: "print the path to each gem in the bundle"
303304
def list
304305
require_relative "cli/list"

lib/bundler/cli/list.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# frozen_string_literal: true
22

3+
require "json"
4+
35
module Bundler
46
class CLI::List
57
def initialize(options)
68
@options = options
79
@without_group = options["without-group"].map(&:to_sym)
810
@only_group = options["only-group"].map(&:to_sym)
11+
@format = options["format"]
912
end
1013

1114
def run
@@ -25,6 +28,36 @@ def run
2528
end
2629
end.reject {|s| s.name == "bundler" }.sort_by(&:name)
2730

31+
case @format
32+
when "json"
33+
print_json(specs: specs)
34+
when nil
35+
print_human(specs: specs)
36+
else
37+
raise InvalidOption, "Unknown option`--format=#{@format}`. Supported formats: `json`"
38+
end
39+
end
40+
41+
private
42+
43+
def print_json(specs:)
44+
gems = if @options["name-only"]
45+
specs.map {|s| { name: s.name } }
46+
else
47+
specs.map do |s|
48+
{
49+
name: s.name,
50+
version: s.version.to_s,
51+
git_version: s.git_version&.strip,
52+
}.tap do |h|
53+
h[:path] = s.full_gem_path if @options["paths"]
54+
end
55+
end
56+
end
57+
Bundler.ui.info({ gems: gems }.to_json)
58+
end
59+
60+
def print_human(specs:)
2861
return Bundler.ui.info "No gems in the Gemfile" if specs.empty?
2962

3063
return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"]
@@ -37,8 +70,6 @@ def run
3770
Bundler.ui.info "Use `bundle info` to print more detailed information about a gem"
3871
end
3972

40-
private
41-
4273
def verify_group_exists(groups)
4374
(@without_group + @only_group).each do |group|
4475
raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group)

lib/bundler/man/bundle-list.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ bundle list \-\-without\-group test
1919
bundle list \-\-only\-group dev
2020
.P
2121
bundle list \-\-only\-group dev test \-\-paths
22+
.P
23+
bundle list \-\-format json
2224
.SH "OPTIONS"
2325
.TP
2426
\fB\-\-name\-only\fR
@@ -32,4 +34,7 @@ A space\-separated list of groups of gems to skip during printing\.
3234
.TP
3335
\fB\-\-only\-group=<list>\fR
3436
A space\-separated list of groups of gems to print\.
37+
.TP
38+
\fB\-\-format=FORMAT\fR
39+
Format output ('json' is the only supported format)
3540

lib/bundler/man/bundle-list.1.ronn

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ bundle list --only-group dev
2121

2222
bundle list --only-group dev test --paths
2323

24+
bundle list --format json
25+
2426
## OPTIONS
2527

2628
* `--name-only`:
@@ -34,3 +36,6 @@ bundle list --only-group dev test --paths
3436

3537
* `--only-group=<list>`:
3638
A space-separated list of groups of gems to print.
39+
40+
* `--format=FORMAT`:
41+
Format output ('json' is the only supported format)

spec/bundler/commands/list_spec.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# frozen_string_literal: true
22

3+
require "json"
4+
35
RSpec.describe "bundle list" do
6+
def find_gem_name(json:, name:)
7+
parse_json(json)["gems"].detect {|h| h["name"] == name }
8+
end
9+
10+
def parse_json(json)
11+
JSON.parse(json)
12+
end
13+
414
context "in verbose mode" do
515
it "logs the actual flags passed to the command" do
616
install_gemfile <<-G
@@ -29,6 +39,20 @@
2939
end
3040
end
3141

42+
context "with invalid format option" do
43+
before do
44+
install_gemfile <<-G
45+
source "https://gem.repo1"
46+
G
47+
end
48+
49+
it "raises an error" do
50+
bundle "list --format=nope", raise_on_error: false
51+
52+
expect(err).to eq "Unknown option`--format=nope`. Supported formats: `json`"
53+
end
54+
end
55+
3256
describe "with without-group option" do
3357
before do
3458
install_gemfile <<-G
@@ -48,6 +72,17 @@
4872
expect(out).to include(" * rails (2.3.2)")
4973
expect(out).not_to include(" * rspec (1.2.7)")
5074
end
75+
76+
it "prints the gems not in the specified group with json" do
77+
bundle "list --without-group test --format=json"
78+
79+
gem = find_gem_name(json: out, name: "myrack")
80+
expect(gem["version"]).to eq("1.0.0")
81+
gem = find_gem_name(json: out, name: "rails")
82+
expect(gem["version"]).to eq("2.3.2")
83+
gem = find_gem_name(json: out, name: "rspec")
84+
expect(gem).to be_nil
85+
end
5186
end
5287

5388
context "when group is not found" do
@@ -66,6 +101,17 @@
66101
expect(out).not_to include(" * rails (2.3.2)")
67102
expect(out).not_to include(" * rspec (1.2.7)")
68103
end
104+
105+
it "prints the gems not in the specified groups with json" do
106+
bundle "list --without-group test production --format=json"
107+
108+
gem = find_gem_name(json: out, name: "myrack")
109+
expect(gem["version"]).to eq("1.0.0")
110+
gem = find_gem_name(json: out, name: "rails")
111+
expect(gem).to be_nil
112+
gem = find_gem_name(json: out, name: "rspec")
113+
expect(gem).to be_nil
114+
end
69115
end
70116
end
71117

@@ -87,6 +133,15 @@
87133
expect(out).to include(" * myrack (1.0.0)")
88134
expect(out).not_to include(" * rspec (1.2.7)")
89135
end
136+
137+
it "prints the gems in the specified group with json" do
138+
bundle "list --only-group default --format=json"
139+
140+
gem = find_gem_name(json: out, name: "myrack")
141+
expect(gem["version"]).to eq("1.0.0")
142+
gem = find_gem_name(json: out, name: "rspec")
143+
expect(gem).to be_nil
144+
end
90145
end
91146

92147
context "when group is not found" do
@@ -105,6 +160,17 @@
105160
expect(out).to include(" * rails (2.3.2)")
106161
expect(out).not_to include(" * rspec (1.2.7)")
107162
end
163+
164+
it "prints the gems in the specified groups with json" do
165+
bundle "list --only-group default production --format=json"
166+
167+
gem = find_gem_name(json: out, name: "myrack")
168+
expect(gem["version"]).to eq("1.0.0")
169+
gem = find_gem_name(json: out, name: "rails")
170+
expect(gem["version"]).to eq("2.3.2")
171+
gem = find_gem_name(json: out, name: "rspec")
172+
expect(gem).to be_nil
173+
end
108174
end
109175
end
110176

@@ -124,6 +190,15 @@
124190
expect(out).to include("myrack")
125191
expect(out).to include("rspec")
126192
end
193+
194+
it "prints only the name of the gems in the bundle with json" do
195+
bundle "list --name-only --format=json"
196+
197+
gem = find_gem_name(json: out, name: "myrack")
198+
expect(gem.keys).to eq(["name"])
199+
gem = find_gem_name(json: out, name: "rspec")
200+
expect(gem.keys).to eq(["name"])
201+
end
127202
end
128203

129204
context "with paths option" do
@@ -158,6 +233,27 @@
158233
expect(out).to match(%r{.*\/git_test\-\w})
159234
expect(out).to match(%r{.*\/gemspec_test})
160235
end
236+
237+
it "prints the path of each gem in the bundle with json" do
238+
bundle "list --paths --format=json"
239+
240+
gem = find_gem_name(json: out, name: "rails")
241+
expect(gem["path"]).to match(%r{.*\/rails\-2\.3\.2})
242+
expect(gem["git_version"]).to be_nil
243+
244+
gem = find_gem_name(json: out, name: "myrack")
245+
expect(gem["path"]).to match(%r{.*\/myrack\-1\.2})
246+
expect(gem["git_version"]).to be_nil
247+
248+
gem = find_gem_name(json: out, name: "git_test")
249+
expect(gem["path"]).to match(%r{.*\/git_test\-\w})
250+
expect(gem["git_version"]).to be_truthy
251+
expect(gem["git_version"].strip).to eq(gem["git_version"])
252+
253+
gem = find_gem_name(json: out, name: "gemspec_test")
254+
expect(gem["path"]).to match(%r{.*\/gemspec_test})
255+
expect(gem["git_version"]).to be_nil
256+
end
161257
end
162258

163259
context "when no gems are in the gemfile" do
@@ -171,6 +267,11 @@
171267
bundle "list"
172268
expect(out).to include("No gems in the Gemfile")
173269
end
270+
271+
it "prints empty json" do
272+
bundle "list --format=json"
273+
expect(parse_json(out)["gems"]).to eq([])
274+
end
174275
end
175276

176277
context "without options" do
@@ -187,6 +288,13 @@
187288
bundle "list"
188289
expect(out).to include(" * myrack (1.0.0)")
189290
end
291+
292+
it "lists gems installed in the bundle with json" do
293+
bundle "list --format=json"
294+
295+
gem = find_gem_name(json: out, name: "myrack")
296+
expect(gem["version"]).to eq("1.0.0")
297+
end
190298
end
191299

192300
context "when using the ls alias" do

0 commit comments

Comments
 (0)