diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 985ec1926..ca48f40c8 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -84,6 +84,7 @@ require "vagrant/registry" module Vagrant autoload :Action, 'vagrant/action' + autoload :Alias, 'vagrant/alias' autoload :BatchAction, 'vagrant/batch_action' autoload :Box, 'vagrant/box' autoload :BoxCollection, 'vagrant/box_collection' diff --git a/lib/vagrant/alias.rb b/lib/vagrant/alias.rb new file mode 100644 index 000000000..26efc6d67 --- /dev/null +++ b/lib/vagrant/alias.rb @@ -0,0 +1,56 @@ +require "vagrant/registry" + +module Vagrant + # This class imports and processes CLI aliases stored in ~/.vagrant.d/aliases + class Alias + def initialize(env) + @aliases = Registry.new + @env = env + + if env.aliases_path.file? + env.aliases_path.readlines.each do |line| + # separate keyword-command pairs + keyword, command = interpret(line) + + if keyword && command + register(keyword, command) + end + end + end + end + + # This returns all the registered alias commands. + def commands + @aliases + end + + # This interprets a raw line from the aliases file. + def interpret(line) + # is it a comment? + return nil if line.strip.start_with?("#") + + keyword, command = line.split("=", 2).collect(&:strip) + + # validate the keyword + if keyword.match(/\s/i) + raise Errors::AliasInvalidError, alias: line, message: "Alias keywords must not contain any whitespace." + end + + [keyword, command] + end + + # This registers an alias. + def register(keyword, command) + @aliases.register(keyword.to_sym) do + lambda do |args| + # directly execute shell commands + if command.start_with?("!") + return Util::SafeExec.exec "#{command[1..-1]} #{args.join(" ")}".strip + end + + return CLI.new(command.split.concat(args), @env).execute + end + end + end + end +end diff --git a/lib/vagrant/cli.rb b/lib/vagrant/cli.rb index 920828407..2017c0fdc 100644 --- a/lib/vagrant/cli.rb +++ b/lib/vagrant/cli.rb @@ -28,6 +28,14 @@ module Vagrant command_plugin = nil if @sub_command command_plugin = Vagrant.plugin("2").manager.commands[@sub_command.to_sym] + + if !command_plugin + alias_command = Alias.new(@env).commands[@sub_command.to_sym] + + if alias_command + return alias_command.call(@sub_args) + end + end end if !command_plugin || !@sub_command diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 2b80b734b..60eb4a685 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -56,6 +56,9 @@ module Vagrant # The directory where temporary files for Vagrant go. attr_reader :tmp_path + # File where command line aliases go. + attr_reader :aliases_path + # The directory where boxes are stored. attr_reader :boxes_path @@ -124,6 +127,9 @@ module Vagrant @tmp_path = @home_path.join("tmp") @machine_index_dir = @data_dir.join("machine-index") + @aliases_path = Pathname.new(ENV["VAGRANT_ALIAS_FILE"]).expand_path if ENV.key?("VAGRANT_ALIAS_FILE") + @aliases_path ||= @home_path.join("aliases") + # Prepare the directories setup_home_path diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 95fae400d..128449cf8 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -108,6 +108,10 @@ module Vagrant error_key(:active_machine_with_different_provider) end + class AliasInvalidError < VagrantError + error_key(:alias_invalid_error) + end + class BatchMultiError < VagrantError error_key(:batch_multi_error) end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 4273395ee..f349dad21 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -402,6 +402,12 @@ en: Machine name: %{name} Active provider: %{active_provider} Requested provider: %{requested_provider} + alias_invalid_error: |- + The defined alias is not valid. Please review the information below + to help resolve the issue: + + Alias: %{alias} + Message: %{message} batch_multi_error: |- An error occurred while executing multiple actions in parallel. Any errors that occurred are shown below. diff --git a/test/unit/vagrant/alias_test.rb b/test/unit/vagrant/alias_test.rb new file mode 100644 index 000000000..5a106684d --- /dev/null +++ b/test/unit/vagrant/alias_test.rb @@ -0,0 +1,68 @@ +require_relative "../base" + +require "vagrant/alias" + +describe Vagrant::Alias do + include_context "unit" + include_context "command plugin helpers" + + let(:iso_env) { isolated_environment } + let(:env) { iso_env.create_vagrant_env } + + describe "#interpret" do + let(:interpreter) { described_class.new(env) } + + it "returns nil for comments" do + comments = [ + "# this is a comment", + "# so is this ", + " # and this", + " # this too " + ] + + comments.each do |comment| + expect(interpreter.interpret(comment)).to be_nil + end + end + + it "raises an error on invalid keywords" do + keywords = [ + "keyword with a space = command", + "keyword\twith a tab = command", + "keyword\nwith a newline = command", + ] + + keywords.each do |keyword| + expect { interpreter.interpret(keyword) }.to raise_error(Vagrant::Errors::AliasInvalidError) + end + end + + it "properly interprets a simple alias" do + keyword, command = interpreter.interpret("keyword=command") + + expect(keyword).to eq("keyword") + expect(command).to eq("command") + end + + it "properly interprets an alias with excess whitespace" do + keyword, command = interpreter.interpret(" keyword = command ") + + expect(keyword).to eq("keyword") + expect(command).to eq("command") + end + + it "properly interprets an alias with an equals sign in the command" do + keyword, command = interpreter.interpret(" keyword = command = command ") + + expect(keyword).to eq("keyword") + expect(command).to eq("command = command") + end + + it "allows keywords with non-alpha-numeric characters" do + keyword, command = interpreter.interpret("keyword! = command") + + expect(keyword).to eq("keyword!") + expect(command).to eq("command") + end + end +end diff --git a/website/source/docs/cli/aliases.html.md b/website/source/docs/cli/aliases.html.md new file mode 100644 index 000000000..d4f3d07b5 --- /dev/null +++ b/website/source/docs/cli/aliases.html.md @@ -0,0 +1,74 @@ +--- +layout: "docs" +page_title: "Aliases - Command-Line Interface" +sidebar_current: "cli-aliases" +description: |- + Custom Vagrant commands can be defined using aliases, allowing for a simpler, + easier, and more familiar command line interface. +--- + +# Aliases + +Inspired in part by Git's own +[alias functionality](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases), +aliases make your Vagrant experience simpler, easier, and more familiar by +allowing you to create your own custom Vagrant commands. + +Aliases can be defined within `VAGRANT_HOME/aliases` file, or in a custom file +defined using the `VAGRANT_ALIAS_FILE` environment variable, in the following +format: + +``` +# basic command-level aliases +start = up +stop = halt + +# advanced command-line aliases +eradicate = !vagrant destroy && rm -rf .vagrant +``` + +In a nutshell, aliases are defined using a standard `key = value` format, where +the `key` is the new Vagrant command, and the `value` is the aliased command. +Using this format, there are two types of aliases that can be defined: internal +and external aliases. + +## Internal Aliases + +Internal command aliases call the CLI class directly, allowing you to alias +one Vagrant command to another Vagrant command. This technique can be very +useful for creating commands that you think _should_ exist. For example, +if `vagrant stop` feels more intuitive than `vagrant halt`, the following alias +definitions would make that change possible: + +``` +stop = halt +``` + +This makes the following commands equivalent: + +``` +vagrant stop +vagrant halt +``` + +## External Aliases + +While internal aliases can be used to define more intuitive Vagrant commands, +external command aliases are used to define Vagrant commands with brand new +functionality. These aliases are prefixed with the `!` character, which +indicates to the interpreter that the alias should be executed as a shell +command. For example, let's say that you want to be able to view the processor +and memory utilization of the active project's virtual machine. To do this, you +could define a `vagrant metrics` command that returns the required information +in an easy-to-read format, like so: + +``` +metrics = !ps aux | grep "[V]BoxHeadless" | grep $(cat .vagrant/machines/default/virtualbox/id) | awk '{ printf("CPU: %.02f%%, Memory: %.02f%%", $3, $4) }' +``` + +The above alias, from within the context of an active Vagrant project, would +print the CPU and memory utilization directly to the console: + +``` +CPU: 4.20%, Memory: 11.00% +``` diff --git a/website/source/docs/other/environmental-variables.html.md b/website/source/docs/other/environmental-variables.html.md index 1f294035c..0ede26818 100644 --- a/website/source/docs/other/environmental-variables.html.md +++ b/website/source/docs/other/environmental-variables.html.md @@ -14,6 +14,11 @@ Vagrant has a set of environmental variables that can be used to configure and control it in a global way. This page lists those environmental variables. +## `VAGRANT_ALIAS_FILE` + +`VAGRANT_ALIAS_FILE` can be set to change the file where Vagrant aliases are +defined. By default, this is set to `~/.vagrant.d/aliases`. + ## `VAGRANT_DEBUG_LAUNCHER` For performance reasons, especially for Windows users, Vagrant uses a static diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index d83386004..984887372 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -44,6 +44,7 @@ >validate >version >More Commands + >Aliases >Machine Readable Output