From 014b0d6d04daa5a3613da939ebf10a8437f2029f Mon Sep 17 00:00:00 2001 From: Harry Lascelles Date: Sun, 20 Jul 2025 19:11:08 +0100 Subject: [PATCH] Add tree flag This allows you to run `app --tree` to see a tree of all available commands. --- lib/thor.rb | 33 +++++++++++++++++++++++++++++++ lib/thor/base.rb | 3 ++- spec/tree_spec.rb | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 spec/tree_spec.rb diff --git a/lib/thor.rb b/lib/thor.rb index 3ae943c1..f3d5f850 100644 --- a/lib/thor.rb +++ b/lib/thor.rb @@ -671,4 +671,37 @@ def help(command = nil, subcommand = false) self.class.help(shell, subcommand) end end + + map TREE_MAPPINGS => :tree + + desc "tree", "Print a tree of all available commands" + def tree + build_command_tree(self.class, "") + end + +private + + def build_command_tree(klass, indent) + # Print current class name if it's not the root Thor class + unless klass == Thor + say "#{indent}#{klass.namespace || 'default'}", :blue + indent = "#{indent} " + end + + # Print all commands for this class + visible_commands = klass.commands.reject { |_, cmd| cmd.hidden? || cmd.name == "help" } + commands_count = visible_commands.count + visible_commands.sort.each_with_index do |(command_name, command), i| + description = command.description.split("\n").first || "" + icon = i == (commands_count - 1) ? "└─" : "├─" + say "#{indent}#{icon} ", nil, false + say command_name, :green, false + say " (#{description})" unless description.empty? + end + + # Print all subcommands (from registered Thor subclasses) + klass.subcommand_classes.each do |_, subclass| + build_command_tree(subclass, indent) + end + end end diff --git a/lib/thor/base.rb b/lib/thor/base.rb index d5f5bea0..58187893 100644 --- a/lib/thor/base.rb +++ b/lib/thor/base.rb @@ -13,8 +13,9 @@ class Thor autoload :RakeCompat, File.expand_path("rake_compat", __dir__) autoload :Group, File.expand_path("group", __dir__) - # Shortcuts for help. + # Shortcuts for help and tree commands. HELP_MAPPINGS = %w(-h -? --help -D) + TREE_MAPPINGS = %w(-t --tree) # Thor methods that should not be overwritten by the user. THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root diff --git a/spec/tree_spec.rb b/spec/tree_spec.rb new file mode 100644 index 00000000..e483146e --- /dev/null +++ b/spec/tree_spec.rb @@ -0,0 +1,49 @@ +require "helper" +require "thor" + +class TreeApp < Thor + desc "command1", "A top level command" + + def command1 + end + + desc "command2", "Another top level command" + + def command2 + end + + class SubApp < Thor + desc "subcommand1", "A subcommand" + + def subcommand1 + end + end + + desc "sub", "Subcommands" + subcommand "sub", SubApp +end + +RSpec.describe "Thor tree command" do + let(:shell) { Thor::Shell::Basic.new } + + it "prints a tree of all commands" do + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/├─ command1/) + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/├─ command2/) + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/└─ sub/) + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/subcommand1/) + end + + it "includes command descriptions" do + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/A top level command/) + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/Another top level command/) + expect(capture(:stdout) { TreeApp.start(["tree"]) }).to match(/A subcommand/) + end + + it "doesn't show hidden commands" do + expect(capture(:stdout) { TreeApp.start(["tree"]) }).not_to match(/help/) + end + + it "shows tree command in help" do + expect(capture(:stdout) { TreeApp.start(["help"]) }).to match(/tree.*Print a tree of all available commands/) + end +end