Merge pull request #2807 from mitchellh/f-non-primary-commands

Non-primary subcommands

This enables plugins to define commands that are "non-primary": they won't be listed in the `vagrant -h` output, and are therefore somewhat hidden. The utility in this is that uncommon commands or commands that aren't friendly to beginners can be hidden. The full list of commands can be seen by executing `vagrant list-commands`.

As of this PR, no command uses this functionality (except `list-commands`). In the future, I'm going to introduce some non-primary commands for specialized tasks such as forcing a re-rsync.
This commit is contained in:
Mitchell Hashimoto 2014-01-11 10:06:55 -08:00
commit a241c9ac59
16 changed files with 277 additions and 53 deletions

View File

@ -23,15 +23,17 @@ module Vagrant
# If we reached this far then we must have a subcommand. If not, # If we reached this far then we must have a subcommand. If not,
# then we also just print the help and exit. # then we also just print the help and exit.
command_class = nil command_plugin = nil
if @sub_command 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 end
if !command_class || !@sub_command if !command_plugin || !@sub_command
help help
return 1 return 1
end end
command_class = command_plugin[0].call
@logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
# Initialize and execute the command class, returning the exit status. # 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("-v", "--version", "Print the version and exit.")
o.on("-h", "--help", "Print this help.") o.on("-h", "--help", "Print this help.")
o.separator "" o.separator ""
o.separator "Available subcommands:" o.separator "Common subcommands:"
# Add the available subcommands as separators in order to print them # Add the available subcommands as separators in order to print them
# out as well. # out as well.
commands = {} commands = {}
longest = 0 longest = 0
Vagrant.plugin("2").manager.commands.each do |key, klass| Vagrant.plugin("2").manager.commands.each do |key, data|
key = key.to_s # 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 commands[key] = klass.synopsis
longest = key.length if key.length > longest longest = key.length if key.length > longest
end end
commands.keys.sort.each do |key| commands.keys.sort.each do |key|
@ -70,6 +77,10 @@ module Vagrant
o.separator "" o.separator ""
o.separator "For help on any individual command run `vagrant COMMAND -h`" 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 end
@env.ui.info(opts.help, :prefix => false) @env.ui.info(opts.help, :prefix => false)

View File

@ -11,6 +11,13 @@ module Vagrant
# @return [Hash<Symbol, Array>] # @return [Hash<Symbol, Array>]
attr_reader :action_hooks 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<Symbol, Array<Proc, Hash>>]
attr_reader :commands
# This contains all the configuration plugins by scope. # This contains all the configuration plugins by scope.
# #
# @return [Hash<Symbol, Registry>] # @return [Hash<Symbol, Registry>]
@ -51,6 +58,7 @@ module Vagrant
# The action hooks hash defaults to [] # The action hooks hash defaults to []
@action_hooks = Hash.new { |h, k| h[k] = [] } @action_hooks = Hash.new { |h, k| h[k] = [] }
@commands = Registry.new
@configs = Hash.new { |h, k| h[k] = Registry.new } @configs = Hash.new { |h, k| h[k] = Registry.new }
@guests = Registry.new @guests = Registry.new
@guest_capabilities = Hash.new { |h, k| h[k] = Registry.new } @guest_capabilities = Hash.new { |h, k| h[k] = Registry.new }

View File

@ -30,11 +30,11 @@ module Vagrant
# This returns all the registered commands. # This returns all the registered commands.
# #
# @return [Hash] # @return [Registry<Symbol, Array<Proc, Hash>>]
def commands def commands
Registry.new.tap do |result| Registry.new.tap do |result|
@registered.each do |plugin| @registered.each do |plugin|
result.merge!(plugin.command) result.merge!(plugin.components.commands)
end end
end end
end end

View File

@ -81,21 +81,21 @@ module Vagrant
# "vagrant foo" becomes available. # "vagrant foo" becomes available.
# #
# @param [String] name Subcommand key. # @param [String] name Subcommand key.
def self.command(name=UNSET_VALUE, &block) def self.command(name, **opts, &block)
data[:command] ||= Registry.new # Validate the name of the command
if name.to_s !~ /^[-a-z0-9]+$/i
if name != UNSET_VALUE raise InvalidCommandName, "Commands can only contain letters, numbers, and hyphens"
# 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)
end end
# Return the registry # By default, the command is primary
data[:command] opts[:primary] = true if !opts.has_key?(:primary)
# Register the command
components.commands.register(name.to_sym) do
[block, opts]
end
nil
end end
# Defines additional communicators to be available. Communicators # Defines additional communicators to be available. Communicators

View File

@ -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

View File

@ -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

View File

@ -61,6 +61,11 @@ en:
Key inserted! Disconnecting and reconnecting using new SSH key... Key inserted! Disconnecting and reconnecting using new SSH key...
inserting_insecure_key: |- inserting_insecure_key: |-
Inserting Vagrant public key within guest... 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: |- plugin_needs_reinstall: |-
The following plugins were installed with a version of Vagrant The following plugins were installed with a version of Vagrant
that had different versions of underlying components. Because that had different versions of underlying components. Because

View File

@ -15,6 +15,7 @@ require "unit/support/dummy_provider"
require "unit/support/shared/base_context" require "unit/support/shared/base_context"
require "unit/support/shared/action_synced_folders_context" require "unit/support/shared/action_synced_folders_context"
require "unit/support/shared/capability_helpers_context" require "unit/support/shared/capability_helpers_context"
require "unit/support/shared/plugin_command_context"
require "unit/support/shared/virtualbox_context" require "unit/support/shared/virtualbox_context"
# Do not buffer output # Do not buffer output

View File

@ -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

View File

@ -1,14 +1,23 @@
require File.expand_path("../../../../base", __FILE__) 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 "unit"
include_context "virtualbox" 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(:argv) { [] }
let(:env) { Vagrant::Environment.new }
let(:machine) { double("Vagrant::Machine", :name => nil) }
let(:ssh_info) {{ let(:ssh_info) {{
:host => "testhost.vagrant.dev", :host => "testhost.vagrant.dev",
:port => 1234, :port => 1234,
@ -18,17 +27,17 @@ describe "VagrantPlugins::CommandSSHConfig::Command" do
:forward_x11 => false :forward_x11 => false
}} }}
subject { described_class.new(argv, env) } subject { described_class.new(argv, iso_env) }
before do before do
machine.stub(ssh_info: ssh_info)
subject.stub(:with_target_vms) { |&block| block.call machine } subject.stub(:with_target_vms) { |&block| block.call machine }
end end
describe "execute" do describe "execute" do
it "prints out the ssh config for the given machine" 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) subject.should_receive(:safe_puts).with(<<-SSHCONFIG)
Host vagrant Host #{machine.name}
HostName testhost.vagrant.dev HostName testhost.vagrant.dev
User testuser User testuser
Port 1234 Port 1234

View File

@ -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

View File

@ -1,29 +1,50 @@
require_relative "../base"
require "vagrant/cli"
describe Vagrant::CLI do describe Vagrant::CLI do
describe "parsing options" do include_context "unit"
let(:klass) do include_context "command plugin helpers"
Class.new(described_class)
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 end
let(:environment) do it "invokes command and returns its exit status if the command is valid" do
ui = double("UI::Silent") commands[:destroy] = [command_lambda("destroy", 42), {}]
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 "returns a non-zero exit status if an invalid command is given" do subject = described_class.new(["destroy"], env)
result = klass.new(["destroypp"], environment).execute subject.should_not_receive(:help)
result.should_not == 0 expect(subject.execute).to eql(42)
end end
end
it "returns an exit status of zero if a valid command is given" do describe "#help" do
result = klass.new(["destroy"], environment).execute subject { described_class.new([], env) }
result.should == 0
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 end
end end

View File

@ -53,7 +53,28 @@ describe Vagrant::Plugin::V2::Plugin do
command("foo") { "bar" } command("foo") { "bar" }
end 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 end
["spaces bad", "sym^bols"].each do |bad| ["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 # Now verify when we actually get the command key that
# a proper error is raised. # a proper error is raised.
expect { expect {
plugin.command[:foo] plugin.components.commands[:foo][0].call
}.to raise_error(StandardError) }.to raise_error(StandardError, "FAIL!")
end end
end end

View File

@ -119,6 +119,7 @@
<li<%= sidebar_current("cli-status") %>><a href="/v2/cli/status.html">status</a></li> <li<%= sidebar_current("cli-status") %>><a href="/v2/cli/status.html">status</a></li>
<li<%= sidebar_current("cli-suspend") %>><a href="/v2/cli/suspend.html">suspend</a></li> <li<%= sidebar_current("cli-suspend") %>><a href="/v2/cli/suspend.html">suspend</a></li>
<li<%= sidebar_current("cli-up") %>><a href="/v2/cli/up.html">up</a></li> <li<%= sidebar_current("cli-up") %>><a href="/v2/cli/up.html">up</a></li>
<li<%= sidebar_current("cli-nonprimary") %>><a href="/v2/cli/non-primary.html">More Commands</a></li>
<li<%= sidebar_current("cli-machinereadable") %>><a href="/v2/cli/machine-readable.html">Machine Readable Output</a></li> <li<%= sidebar_current("cli-machinereadable") %>><a href="/v2/cli/machine-readable.html">Machine Readable Output</a></li>
</ul> </ul>
<% end %> <% end %>

View File

@ -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`.

View File

@ -32,7 +32,23 @@ end
Commands are defined with the `command` method, which takes as an argument 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 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 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 ## Implementation