diff --git a/lib/vagrant/cli.rb b/lib/vagrant/cli.rb index f98c045b3..76ebc9abe 100644 --- a/lib/vagrant/cli.rb +++ b/lib/vagrant/cli.rb @@ -23,15 +23,17 @@ module Vagrant # If we reached this far then we must have a subcommand. If not, # then we also just print the help and exit. - command_class = nil + command_plugin = nil if @sub_command - command_class = Vagrant.plugin("2").manager.commands[@sub_command.to_sym] + command_plugin = Vagrant.plugin("2").manager.commands[@sub_command.to_sym] end - if !command_class || !@sub_command + if !command_plugin || !@sub_command help return 1 end + + command_class = command_plugin[0].call @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") # Initialize and execute the command class, returning the exit status. @@ -51,16 +53,21 @@ module Vagrant o.on("-v", "--version", "Print the version and exit.") o.on("-h", "--help", "Print this help.") o.separator "" - o.separator "Available subcommands:" + o.separator "Common subcommands:" # Add the available subcommands as separators in order to print them # out as well. commands = {} longest = 0 - Vagrant.plugin("2").manager.commands.each do |key, klass| - key = key.to_s + Vagrant.plugin("2").manager.commands.each do |key, data| + # Skip non-primary commands. These only show up in extended + # help output. + next if !data[1][:primary] + + key = key.to_s + klass = data[0].call commands[key] = klass.synopsis - longest = key.length if key.length > longest + longest = key.length if key.length > longest end commands.keys.sort.each do |key| @@ -70,6 +77,10 @@ module Vagrant o.separator "" o.separator "For help on any individual command run `vagrant COMMAND -h`" + o.separator "" + o.separator "Additional subcommands are available, but are either more advanced" + o.separator "or not commonly used. To see all subcommands, run the command" + o.separator "`vagrant list-commands`." end @env.ui.info(opts.help, :prefix => false) diff --git a/lib/vagrant/plugin/v2/components.rb b/lib/vagrant/plugin/v2/components.rb index d5c2c4d62..c99d14967 100644 --- a/lib/vagrant/plugin/v2/components.rb +++ b/lib/vagrant/plugin/v2/components.rb @@ -11,6 +11,13 @@ module Vagrant # @return [Hash] attr_reader :action_hooks + # This contains all the command plugins by name, and returns + # the command class and options. The command class is wrapped + # in a Proc so that it can be lazy loaded. + # + # @return [Registry>] + attr_reader :commands + # This contains all the configuration plugins by scope. # # @return [Hash] @@ -51,6 +58,7 @@ module Vagrant # The action hooks hash defaults to [] @action_hooks = Hash.new { |h, k| h[k] = [] } + @commands = Registry.new @configs = Hash.new { |h, k| h[k] = Registry.new } @guests = Registry.new @guest_capabilities = Hash.new { |h, k| h[k] = Registry.new } diff --git a/lib/vagrant/plugin/v2/manager.rb b/lib/vagrant/plugin/v2/manager.rb index 17e4b7a8c..dd4788dd3 100644 --- a/lib/vagrant/plugin/v2/manager.rb +++ b/lib/vagrant/plugin/v2/manager.rb @@ -30,11 +30,11 @@ module Vagrant # This returns all the registered commands. # - # @return [Hash] + # @return [Registry>] def commands Registry.new.tap do |result| @registered.each do |plugin| - result.merge!(plugin.command) + result.merge!(plugin.components.commands) end end end diff --git a/lib/vagrant/plugin/v2/plugin.rb b/lib/vagrant/plugin/v2/plugin.rb index f2e82445f..bacf891dc 100644 --- a/lib/vagrant/plugin/v2/plugin.rb +++ b/lib/vagrant/plugin/v2/plugin.rb @@ -81,21 +81,21 @@ module Vagrant # "vagrant foo" becomes available. # # @param [String] name Subcommand key. - def self.command(name=UNSET_VALUE, &block) - data[:command] ||= Registry.new - - if name != UNSET_VALUE - # Validate the name of the command - if name.to_s !~ /^[-a-z0-9]+$/i - raise InvalidCommandName, "Commands can only contain letters, numbers, and hyphens" - end - - # Register a new command class only if a name was given. - data[:command].register(name.to_sym, &block) + def self.command(name, **opts, &block) + # Validate the name of the command + if name.to_s !~ /^[-a-z0-9]+$/i + raise InvalidCommandName, "Commands can only contain letters, numbers, and hyphens" end - # Return the registry - data[:command] + # By default, the command is primary + opts[:primary] = true if !opts.has_key?(:primary) + + # Register the command + components.commands.register(name.to_sym) do + [block, opts] + end + + nil end # Defines additional communicators to be available. Communicators diff --git a/plugins/commands/list-commands/command.rb b/plugins/commands/list-commands/command.rb new file mode 100644 index 000000000..cefc71755 --- /dev/null +++ b/plugins/commands/list-commands/command.rb @@ -0,0 +1,43 @@ +require "optparse" + +module VagrantPlugins + module CommandListCommands + class Command < Vagrant.plugin("2", :command) + def self.synopsis + "outputs all available Vagrant subcommands, even non-primary ones" + end + + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant list-commands" + end + + argv = parse_options(opts) + return if !argv + + # Add the available subcommands as separators in order to print them + # out as well. + commands = {} + longest = 0 + Vagrant.plugin("2").manager.commands.each do |key, data| + key = key.to_s + klass = data[0].call + commands[key] = klass.synopsis + longest = key.length if key.length > longest + end + + command_output = [] + commands.keys.sort.each do |key| + command_output << "#{key.ljust(longest+2)} #{commands[key]}" + @env.ui.machine("cli-command", key.dup) + end + + @env.ui.info( + I18n.t("vagrant.list_commands", list: command_output.join("\n"))) + + # Success, exit status 0 + 0 + end + end + end +end diff --git a/plugins/commands/list-commands/plugin.rb b/plugins/commands/list-commands/plugin.rb new file mode 100644 index 000000000..53f4a6a82 --- /dev/null +++ b/plugins/commands/list-commands/plugin.rb @@ -0,0 +1,18 @@ +require "vagrant" + +module VagrantPlugins + module CommandListCommands + class Plugin < Vagrant.plugin("2") + name "list-commands command" + description <<-DESC + The `list-commands` command will list all commands that Vagrant + understands, even hidden ones. + DESC + + command("list-commands", primary: false) do + require_relative "command" + Command + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2a80ae186..0d41d8e23 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -61,6 +61,11 @@ en: Key inserted! Disconnecting and reconnecting using new SSH key... inserting_insecure_key: |- Inserting Vagrant public key within guest... + list_commands: |- + Below is a listing of all available Vagrant commands and a brief + description of what they do. + + %{list} plugin_needs_reinstall: |- The following plugins were installed with a version of Vagrant that had different versions of underlying components. Because diff --git a/test/unit/base.rb b/test/unit/base.rb index c9ffe6654..0934e1b9d 100644 --- a/test/unit/base.rb +++ b/test/unit/base.rb @@ -15,6 +15,7 @@ require "unit/support/dummy_provider" require "unit/support/shared/base_context" require "unit/support/shared/action_synced_folders_context" require "unit/support/shared/capability_helpers_context" +require "unit/support/shared/plugin_command_context" require "unit/support/shared/virtualbox_context" # Do not buffer output diff --git a/test/unit/plugins/commands/list-commands/command_test.rb b/test/unit/plugins/commands/list-commands/command_test.rb new file mode 100644 index 000000000..006396ba8 --- /dev/null +++ b/test/unit/plugins/commands/list-commands/command_test.rb @@ -0,0 +1,40 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/list-commands/command") + +describe VagrantPlugins::CommandListCommands::Command do + include_context "unit" + include_context "command plugin helpers" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:argv) { [] } + let(:commands) { {} } + + subject { described_class.new(argv, iso_env) } + + before do + Vagrant.plugin("2").manager.stub(commands: commands) + end + + describe "execute" do + it "includes all subcommands" do + commands[:foo] = [command_lambda("foo", 0), { primary: true }] + commands[:bar] = [command_lambda("bar", 0), { primary: true }] + commands[:baz] = [command_lambda("baz", 0), { primary: false }] + + iso_env.ui.should_receive(:info).with do |message, opts| + expect(message).to include("foo") + expect(message).to include("bar") + expect(message).to include("baz") + end + + subject.execute + end + end +end diff --git a/test/unit/plugins/commands/ssh_config/command_test.rb b/test/unit/plugins/commands/ssh_config/command_test.rb index df862bd54..4f5c51459 100644 --- a/test/unit/plugins/commands/ssh_config/command_test.rb +++ b/test/unit/plugins/commands/ssh_config/command_test.rb @@ -1,14 +1,23 @@ require File.expand_path("../../../../base", __FILE__) -describe "VagrantPlugins::CommandSSHConfig::Command" do +require Vagrant.source_root.join("plugins/commands/ssh_config/command") + +describe VagrantPlugins::CommandSSHConfig::Command do include_context "unit" include_context "virtualbox" - let(:described_class) { Vagrant.plugin("2").manager.commands[:"ssh-config"] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:argv) { [] } - let(:env) { Vagrant::Environment.new } - let(:machine) { double("Vagrant::Machine", :name => nil) } let(:ssh_info) {{ :host => "testhost.vagrant.dev", :port => 1234, @@ -18,17 +27,17 @@ describe "VagrantPlugins::CommandSSHConfig::Command" do :forward_x11 => false }} - subject { described_class.new(argv, env) } + subject { described_class.new(argv, iso_env) } before do + machine.stub(ssh_info: ssh_info) subject.stub(:with_target_vms) { |&block| block.call machine } end describe "execute" do it "prints out the ssh config for the given machine" do - machine.stub(:ssh_info) { ssh_info } subject.should_receive(:safe_puts).with(<<-SSHCONFIG) -Host vagrant +Host #{machine.name} HostName testhost.vagrant.dev User testuser Port 1234 diff --git a/test/unit/support/shared/plugin_command_context.rb b/test/unit/support/shared/plugin_command_context.rb new file mode 100644 index 000000000..e5f5a5612 --- /dev/null +++ b/test/unit/support/shared/plugin_command_context.rb @@ -0,0 +1,11 @@ +shared_context "command plugin helpers" do + def command_lambda(name, result) + lambda do + Class.new(Vagrant.plugin("2", "command")) do + define_method(:execute) do + result + end + end + end + end +end diff --git a/test/unit/vagrant/cli_test.rb b/test/unit/vagrant/cli_test.rb index 2945cd978..620193eda 100644 --- a/test/unit/vagrant/cli_test.rb +++ b/test/unit/vagrant/cli_test.rb @@ -1,29 +1,50 @@ +require_relative "../base" + +require "vagrant/cli" + describe Vagrant::CLI do - describe "parsing options" do - let(:klass) do - Class.new(described_class) + include_context "unit" + include_context "command plugin helpers" + + let(:commands) { {} } + let(:iso_env) { isolated_environment } + let(:env) { iso_env.create_vagrant_env } + + before do + Vagrant.plugin("2").manager.stub(commands: commands) + end + + describe "#execute" do + it "invokes help and exits with 1 if invalid command" do + subject = described_class.new(["i-dont-exist"], env) + subject.should_receive(:help).once + expect(subject.execute).to eql(1) end - let(:environment) do - ui = double("UI::Silent") - ui.stub(:machine => "bar") - ui.stub(:info => "bar") - env = double("Vagrant::Environment") - env.stub(:active_machines => []) - env.stub(:ui => ui) - env.stub(:root_path => "foo") - env.stub(:machine_names => []) - env - end + it "invokes command and returns its exit status if the command is valid" do + commands[:destroy] = [command_lambda("destroy", 42), {}] - it "returns a non-zero exit status if an invalid command is given" do - result = klass.new(["destroypp"], environment).execute - result.should_not == 0 + subject = described_class.new(["destroy"], env) + subject.should_not_receive(:help) + expect(subject.execute).to eql(42) end + end - it "returns an exit status of zero if a valid command is given" do - result = klass.new(["destroy"], environment).execute - result.should == 0 + describe "#help" do + subject { described_class.new([], env) } + + it "includes all primary subcommands" do + commands[:foo] = [command_lambda("foo", 0), { primary: true }] + commands[:bar] = [command_lambda("bar", 0), { primary: true }] + commands[:baz] = [command_lambda("baz", 0), { primary: false }] + + env.ui.should_receive(:info).with do |message, opts| + expect(message).to include("foo") + expect(message).to include("bar") + expect(message.include?("baz")).to be_false + end + + subject.help end end end diff --git a/test/unit/vagrant/plugin/v2/plugin_test.rb b/test/unit/vagrant/plugin/v2/plugin_test.rb index f6472e762..4a7a32feb 100644 --- a/test/unit/vagrant/plugin/v2/plugin_test.rb +++ b/test/unit/vagrant/plugin/v2/plugin_test.rb @@ -53,7 +53,28 @@ describe Vagrant::Plugin::V2::Plugin do command("foo") { "bar" } end - plugin.command[:foo].should == "bar" + expect(plugin.components.commands.keys).to be_include(:foo) + expect(plugin.components.commands[:foo][0].call).to eql("bar") + end + + it "should register command classes with options" do + plugin = Class.new(described_class) do + command("foo", opt: :bar) { "bar" } + end + + expect(plugin.components.commands.keys).to be_include(:foo) + expect(plugin.components.commands[:foo][0].call).to eql("bar") + expect(plugin.components.commands[:foo][1][:opt]).to eql(:bar) + end + + it "should register commands as primary by default" do + plugin = Class.new(described_class) do + command("foo") { "bar" } + command("bar", primary: false) { "bar" } + end + + expect(plugin.components.commands[:foo][1][:primary]).to be_true + expect(plugin.components.commands[:bar][1][:primary]).to be_false end ["spaces bad", "sym^bols"].each do |bad| @@ -79,8 +100,8 @@ describe Vagrant::Plugin::V2::Plugin do # Now verify when we actually get the command key that # a proper error is raised. expect { - plugin.command[:foo] - }.to raise_error(StandardError) + plugin.components.commands[:foo][0].call + }.to raise_error(StandardError, "FAIL!") end end diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 44940e9b9..ab7f31057 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -119,6 +119,7 @@ >status >suspend >up + >More Commands >Machine Readable Output <% end %> diff --git a/website/docs/source/v2/cli/non-primary.html.md b/website/docs/source/v2/cli/non-primary.html.md new file mode 100644 index 000000000..76b3cd63c --- /dev/null +++ b/website/docs/source/v2/cli/non-primary.html.md @@ -0,0 +1,19 @@ +--- +page_title: "More Vagrant Commands - Command-Line Interface" +sidebar_current: "cli-nonprimary" +--- + +# More Commands + +In addition to the commands listed in the sidebar and shown in `vagrant -h`, +Vagrant comes with some more commands that are hidden from basic help output. +These commands are hidden because they're not useful to beginners or they're +not commonly used. We call these commands "non-primary subcommands". + +You can view all subcommands, including the non-primary subcommands, +by running `vagrant list-commands`, which itself is a non-primary subcommand! + +Note that while you have to run a special command to list the non-primary +subcommands, you don't have to do anything special to actually _run_ the +non-primary subcommands. They're executed just like any other subcommand: +`vagrant COMMAND`. diff --git a/website/docs/source/v2/plugins/commands.html.md b/website/docs/source/v2/plugins/commands.html.md index f3a8ed86d..26b723d80 100644 --- a/website/docs/source/v2/plugins/commands.html.md +++ b/website/docs/source/v2/plugins/commands.html.md @@ -32,7 +32,23 @@ end Commands are defined with the `command` method, which takes as an argument the name of the command, in this case "foo." This means the command will be invokable via `vagrant foo`. Then the block argument returns a class that -implements the `Vagrant.plugin(2, :command)` interface. +implements the `Vagrant.plugin(2, "command")` interface. + +You can also define _non-primary commands_. These commands do not show +up in the `vagrant -h` output. They only show up if the user explicitly +does a `vagrant list-commands` which shows the full listing of available +commands. This is useful for highly specific commands or plugins that a +beginner to Vagrant would not be using anyways. Vagrant itself uses non-primary +commands to expose some internal functions, as well. + +To define a non-primary command: + +```ruby +command("foo", primary: false) do + require_relative "command" + Command +end +``` ## Implementation