From 4086d856d1cb6a94665d4c215930f3978bb119c1 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Fri, 17 Apr 2026 17:15:16 +0100 Subject: [PATCH] Implement exact-match flag for bundle info / show / open --- bundler/lib/bundler/cli.rb | 3 +++ bundler/lib/bundler/cli/common.rb | 4 +++ bundler/lib/bundler/cli/info.rb | 2 +- bundler/lib/bundler/cli/open.rb | 8 +++++- bundler/lib/bundler/cli/show.rb | 6 ++++- bundler/lib/bundler/man/bundle-info.1 | 5 +++- bundler/lib/bundler/man/bundle-info.1.ronn | 4 +++ bundler/lib/bundler/man/bundle-open.1 | 5 +++- bundler/lib/bundler/man/bundle-open.1.ronn | 5 +++- bundler/lib/bundler/man/bundle-show.1 | 5 +++- bundler/lib/bundler/man/bundle-show.1.ronn | 4 +++ spec/commands/info_spec.rb | 30 ++++++++++++++++++++-- spec/commands/open_spec.rb | 29 +++++++++++++++++++++ spec/commands/show_spec.rb | 29 +++++++++++++++++++-- 14 files changed, 128 insertions(+), 11 deletions(-) diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index 9a0f756bcf82..123f26f24f13 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -334,6 +334,7 @@ def update(*gems) D method_option "paths", type: :boolean, banner: "List the paths of all gems that are required by your Gemfile." method_option "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)." + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def show(gem_name = nil) if ARGV.include?("--outdated") removed_message = "the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`" @@ -359,6 +360,7 @@ def list desc "info GEM [OPTIONS]", "Show information for the given gem" method_option "path", type: :boolean, banner: "Print full path to gem" method_option "version", type: :boolean, banner: "Print gem version" + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def info(gem_name) require_relative "cli/info" Info.new(options, gem_name).run @@ -519,6 +521,7 @@ def exec(*args) desc "open GEM", "Opens the source directory of the given bundled gem" method_option "path", type: :string, lazy_default: "", banner: "Open relative path of the gem source." + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def open(name) require_relative "cli/open" Open.new(options, name).run diff --git a/bundler/lib/bundler/cli/common.rb b/bundler/lib/bundler/cli/common.rb index 2f332ff36440..e2dc6210e73c 100644 --- a/bundler/lib/bundler/cli/common.rb +++ b/bundler/lib/bundler/cli/common.rb @@ -78,6 +78,10 @@ def self.select_spec(name, regex_match = nil) raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) end + def self.select_spec_with_match_type(name, options) + select_spec(name, options["exact-match"] ? nil : :regex_match) + end + def self.default_gem_spec(name) gem_spec = Gem::Specification.find_all_by_name(name).last gem_spec if gem_spec&.default_gem? diff --git a/bundler/lib/bundler/cli/info.rb b/bundler/lib/bundler/cli/info.rb index cd01d4949bc9..a0dd5a54876a 100644 --- a/bundler/lib/bundler/cli/info.rb +++ b/bundler/lib/bundler/cli/info.rb @@ -26,7 +26,7 @@ def run private def spec_for_gem(name) - Bundler::CLI::Common.select_spec(name, :regex_match) + Bundler::CLI::Common.select_spec_with_match_type(name, options) end def print_gem_version(spec) diff --git a/bundler/lib/bundler/cli/open.rb b/bundler/lib/bundler/cli/open.rb index f24693b84367..188006ba0cb9 100644 --- a/bundler/lib/bundler/cli/open.rb +++ b/bundler/lib/bundler/cli/open.rb @@ -13,7 +13,7 @@ def run raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty? editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? } return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor - return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match) + return unless spec = spec_for_gem(name) if spec.default_gem? Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist." else @@ -25,5 +25,11 @@ def run end || Bundler.ui.info("Could not run '#{command.join(" ")}'") end end + + private + + def spec_for_gem(name) + Bundler::CLI::Common.select_spec_with_match_type(name, options) + end end end diff --git a/bundler/lib/bundler/cli/show.rb b/bundler/lib/bundler/cli/show.rb index 67fdcc797e85..6e88bd476515 100644 --- a/bundler/lib/bundler/cli/show.rb +++ b/bundler/lib/bundler/cli/show.rb @@ -20,7 +20,7 @@ def run if gem_name == "bundler" path = File.expand_path("../../..", __dir__) else - spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match) + spec = spec_for_gem(gem_name) return unless spec path = spec.full_gem_path unless File.directory?(path) @@ -55,6 +55,10 @@ def run private + def spec_for_gem(name) + Bundler::CLI::Common.select_spec_with_match_type(name, options) + end + def fetch_latest_specs definition = Bundler.definition(true) Bundler.ui.info "Fetching remote specs for outdated check...\n\n" diff --git a/bundler/lib/bundler/man/bundle-info.1 b/bundler/lib/bundler/man/bundle-info.1 index b18b70309c50..5fcb14733a0d 100644 --- a/bundler/lib/bundler/man/bundle-info.1 +++ b/bundler/lib/bundler/man/bundle-info.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" -\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] +\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] [\-\-exact\-match] .SH "DESCRIPTION" Given a gem name present in your bundle, print the basic information about it such as homepage, version, path and summary\. .SH "OPTIONS" @@ -14,4 +14,7 @@ Print the path of the given gem .TP \fB\-\-version\fR Print gem version +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/bundler/lib/bundler/man/bundle-info.1.ronn b/bundler/lib/bundler/man/bundle-info.1.ronn index e99db8c6149c..38cd19f4312c 100644 --- a/bundler/lib/bundler/man/bundle-info.1.ronn +++ b/bundler/lib/bundler/man/bundle-info.1.ronn @@ -6,6 +6,7 @@ bundle-info(1) -- Show information for the given gem in your bundle `bundle info` [GEM_NAME] [--path] [--version] + [--exact-match] ## DESCRIPTION @@ -19,3 +20,6 @@ Given a gem name present in your bundle, print the basic information about it * `--version`: Print gem version + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/bundler/lib/bundler/man/bundle-open.1 b/bundler/lib/bundler/man/bundle-open.1 index 99166e8580f4..d644eb3a8737 100644 --- a/bundler/lib/bundler/man/bundle-open.1 +++ b/bundler/lib/bundler/man/bundle-open.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" -\fBbundle open\fR [GEM] [\-\-path=PATH] +\fBbundle open\fR [GEM] [\-\-path=PATH] [\-\-exact\-match] .SH "DESCRIPTION" Opens the source directory of the provided GEM in your editor\. .P @@ -29,4 +29,7 @@ Will open the README\.md file of the 'rack' gem source in your bundle\. .TP \fB\-\-path[=PATH]\fR Specify GEM source relative path to open\. +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/bundler/lib/bundler/man/bundle-open.1.ronn b/bundler/lib/bundler/man/bundle-open.1.ronn index 24dbe97e4456..c345b88adc58 100644 --- a/bundler/lib/bundler/man/bundle-open.1.ronn +++ b/bundler/lib/bundler/man/bundle-open.1.ronn @@ -3,7 +3,7 @@ bundle-open(1) -- Opens the source directory for a gem in your bundle ## SYNOPSIS -`bundle open` [GEM] [--path=PATH] +`bundle open` [GEM] [--path=PATH] [--exact-match] ## DESCRIPTION @@ -26,3 +26,6 @@ Will open the README.md file of the 'rack' gem source in your bundle. * `--path[=PATH]`: Specify GEM source relative path to open. + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/bundler/lib/bundler/man/bundle-show.1 b/bundler/lib/bundler/man/bundle-show.1 index aaf146fa271b..aba002cac1c9 100644 --- a/bundler/lib/bundler/man/bundle-show.1 +++ b/bundler/lib/bundler/man/bundle-show.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" -\fBbundle show\fR [GEM] [\-\-paths] +\fBbundle show\fR [GEM] [\-\-paths] [\-\-exact\-match] .SH "DESCRIPTION" Without the [GEM] option, \fBshow\fR will print a list of the names and versions of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by name\. .P @@ -13,4 +13,7 @@ Calling show with [GEM] will list the exact location of that gem on your machine .TP \fB\-\-paths\fR List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\. +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/bundler/lib/bundler/man/bundle-show.1.ronn b/bundler/lib/bundler/man/bundle-show.1.ronn index a6a59a1445dc..2f679e5f7d1f 100644 --- a/bundler/lib/bundler/man/bundle-show.1.ronn +++ b/bundler/lib/bundler/man/bundle-show.1.ronn @@ -5,6 +5,7 @@ bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem `bundle show` [GEM] [--paths] + [--exact-match] ## DESCRIPTION @@ -19,3 +20,6 @@ machine. * `--paths`: List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by gem name. + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/spec/commands/info_spec.rb b/spec/commands/info_spec.rb index a26b1696fbdf..317a88863c9e 100644 --- a/spec/commands/info_spec.rb +++ b/spec/commands/info_spec.rb @@ -207,7 +207,31 @@ end context "with a valid regexp for gem name" do - it "presents alternatives", :readline do + it "returns the exact match without prompting when requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info myrack --exact-match" + expect(out).to include("* myrack (1.0.0)") + expect(out).not_to include("0 : - exit -") + end + + it "does not fall back to regexp matching when exact matching is requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info rac --exact-match", raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do install_gemfile <<-G source "https://gem.repo1" gem "myrack" @@ -215,7 +239,9 @@ G bundle "info rac" - expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") end end diff --git a/spec/commands/open_spec.rb b/spec/commands/open_spec.rb index 664dc58919cf..085105b8c19b 100644 --- a/spec/commands/open_spec.rb +++ b/spec/commands/open_spec.rb @@ -172,4 +172,33 @@ expect(out).to include("Unable to open json because it's a default gem, so the directory it would normally be installed to does not exist.") end end + + context "with a valid regexp for gem name" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + end + + it "returns the exact match without prompting when requested" do + bundle "open myrack --exact-match", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "myrack-1.0.0")}") + expect(out).not_to include("0 : - exit -") + end + + it "does not fall back to regexp matching when exact matching is requested" do + bundle "open rac --exact-match", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do + bundle "open rac", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") + end + end end diff --git a/spec/commands/show_spec.rb b/spec/commands/show_spec.rb index d0d55ffbb9b0..868aca83e0df 100644 --- a/spec/commands/show_spec.rb +++ b/spec/commands/show_spec.rb @@ -185,7 +185,30 @@ end context "with a valid regexp for gem name" do - it "presents alternatives", :readline do + it "returns the exact match without prompting when requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show myrack --exact-match" + expect(out).to include(default_bundle_path("gems", "myrack-1.0.0").to_s) + end + + it "does not fall back to regexp matching when exact matching is requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show rac --exact-match", raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do install_gemfile <<-G source "https://gem.repo1" gem "myrack" @@ -193,7 +216,9 @@ G bundle "show rac" - expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") end end