From 3899ad02edea313458f3e77390f2d733d1af9e7f Mon Sep 17 00:00:00 2001 From: Ashique Saidalavi Date: Thu, 16 Apr 2026 17:33:06 +0530 Subject: [PATCH 1/2] Fixed the issue with knife -h command showing invalid subcommands Signed-off-by: Ashique Saidalavi --- lib/chef/knife.rb | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 4e2eef91..600c53e8 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -102,17 +102,33 @@ def self.reset_subcommands! def self.inherited(subclass) super unless subclass.unnamed? + caller_path = if subclass.superclass.to_s == "Chef::ChefFS::Knife" + # ChefFS-based commands have a superclass that defines an + # inherited method which calls super. This means that the + # top of the call stack is not the class definition for + # our subcommand. Try the second entry in the call stack. + path_from_caller(caller[1]) + else + path_from_caller(caller[0]) + end + + # Skip classes defined inside a subdirectory of the lib/chef/knife/ tree + # (e.g. lib/chef/knife/cloud/server/create_command.rb). By convention, + # real knife subcommands live directly at lib/chef/knife/.rb — + # the same flat pattern that GemGlobLoader uses when discovering + # commands. Abstract base classes from plugins like knife-cloud are + # nested in subdirs and are only loaded transitively; registering them + # causes spurious categories (e.g. ** SERVER COMMANDS **) to appear in + # 'knife --help'. + # + # NOTE: The regex anchors to /lib/chef/knife/ to avoid false positives + # from Habitat package paths, where the package origin and name appear + # in the path (e.g. /hab/pkgs/chef/knife/19.0.99/…) and would otherwise + # match a looser pattern like /chef/knife/[^/]+/. + return if caller_path.match?(%r{/lib/chef/knife/[^/]+/}) + subcommands[subclass.snake_case_name] = subclass - subcommand_files[subclass.snake_case_name] += - if subclass.superclass.to_s == "Chef::ChefFS::Knife" - # ChefFS-based commands have a superclass that defines an - # inherited method which calls super. This means that the - # top of the call stack is not the class definition for - # our subcommand. Try the second entry in the call stack. - [path_from_caller(caller[1])] - else - [path_from_caller(caller[0])] - end + subcommand_files[subclass.snake_case_name] += [caller_path] end end From 2fbf3551ad8a51b712543de22ccf05a92984c04b Mon Sep 17 00:00:00 2001 From: Ashique Saidalavi Date: Wed, 22 Apr 2026 15:40:34 +0530 Subject: [PATCH 2/2] Added specs for the sub command filtering Signed-off-by: Ashique Saidalavi --- spec/unit/knife_spec.rb | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index 8da7f531..0bd3c5ca 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -158,6 +158,68 @@ class AwesomeCheffsCommand < Chef::ChefFS::Knife end + describe "subdirectory subcommand filtering" do + before do + Chef::Knife.reset_subcommands! + end + + after do + %i{CloudSubdirCommand FlatKnifeCommand HabitatKnifeCommand CheffsSubdirCommand}.each do |const| + KnifeSpecs.send(:remove_const, const) if KnifeSpecs.const_defined?(const) + end + end + + it "does not register subcommands defined in a subdirectory of lib/chef/knife/" do + allow(Chef::Knife).to receive(:path_from_caller) + .and_return("/some/gem/lib/chef/knife/cloud/server/create_command.rb") + + module KnifeSpecs + class CloudSubdirCommand < Chef::Knife; end + end + + expect(Chef::Knife.subcommands).not_to have_key("cloud_subdir_command") + expect(Chef::Knife.subcommand_files).not_to have_key("cloud_subdir_command") + end + + it "registers subcommands defined directly in lib/chef/knife/ (flat path)" do + allow(Chef::Knife).to receive(:path_from_caller) + .and_return("/some/gem/lib/chef/knife/flat_knife_command.rb") + + module KnifeSpecs + class FlatKnifeCommand < Chef::Knife; end + end + + expect(Chef::Knife.subcommands).to have_key("flat_knife_command") + expect(Chef::Knife.subcommand_files["flat_knife_command"]).to eq(["/some/gem/lib/chef/knife/flat_knife_command.rb"]) + end + + it "does not filter subcommands loaded from Habitat package paths" do + # Habitat paths contain /chef/knife/ as the package origin/name segment, + # e.g. /hab/pkgs/chef/knife/19.0.99/lib/chef/knife/foo.rb. The regex must + # anchor to /lib/chef/knife/ to avoid false positives on these paths. + allow(Chef::Knife).to receive(:path_from_caller) + .and_return("/hab/pkgs/chef/knife/19.0.99/lib/chef/knife/habitat_knife_command.rb") + + module KnifeSpecs + class HabitatKnifeCommand < Chef::Knife; end + end + + expect(Chef::Knife.subcommands).to have_key("habitat_knife_command") + end + + it "does not register ChefFS-based subcommands defined in subdirectories of lib/chef/knife/" do + # ChefFS commands use caller[1] for path detection; filtering should still apply. + allow(Chef::Knife).to receive(:path_from_caller) + .and_return("/some/gem/lib/chef/knife/cloud/cheffs_subdir_command.rb") + + module KnifeSpecs + class CheffsSubdirCommand < Chef::ChefFS::Knife; end + end + + expect(Chef::Knife.subcommands).not_to have_key("cheffs_subdir_command") + end + end + describe "the headers include X-Remote-Request-Id" do let(:headers) do