From bbb3cdaa9a286bb85086b3b60be9054da78cbe80 Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Sat, 10 Feb 2018 17:55:36 -0700 Subject: [PATCH 1/8] vagrant aliases proof-of-concept --- lib/vagrant.rb | 1 + lib/vagrant/alias.rb | 36 ++++++++++++++++++++++++++++++++++++ lib/vagrant/cli.rb | 8 ++++++++ 3 files changed, 45 insertions(+) create mode 100644 lib/vagrant/alias.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 5324c8950..f1e9c5ee6 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -83,6 +83,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..1d9ef2b8e --- /dev/null +++ b/lib/vagrant/alias.rb @@ -0,0 +1,36 @@ +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 + + aliases_file = env.home_path.join("aliases") + if aliases_file.file? + aliases_file.readlines.each do |line| + # skip comments + next if line.strip.start_with?("#") + + # separate keyword-command pairs + keyword, command = line.split("=").collect(&:strip) + + @aliases.register(keyword.to_sym) do + lambda do |args| + # directly execute shell commands + if command.start_with?("!") + return exec "#{command[1..-1]} #{args.join(" ")}".strip + end + + return CLI.new(command.split.concat(args), env).execute + end + end + end + end + end + + def commands + @aliases + end + end +end diff --git a/lib/vagrant/cli.rb b/lib/vagrant/cli.rb index cca8c0184..2e4902b39 100644 --- a/lib/vagrant/cli.rb +++ b/lib/vagrant/cli.rb @@ -26,6 +26,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 From 17f13785ce7eb2b19321380f365852469dd01b2e Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Sat, 10 Feb 2018 18:03:18 -0700 Subject: [PATCH 2/8] move alias registration into a separate register() method --- lib/vagrant/alias.rb | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/vagrant/alias.rb b/lib/vagrant/alias.rb index 1d9ef2b8e..660ec001f 100644 --- a/lib/vagrant/alias.rb +++ b/lib/vagrant/alias.rb @@ -15,22 +15,28 @@ module Vagrant # separate keyword-command pairs keyword, command = line.split("=").collect(&:strip) - @aliases.register(keyword.to_sym) do - lambda do |args| - # directly execute shell commands - if command.start_with?("!") - return exec "#{command[1..-1]} #{args.join(" ")}".strip - end - - return CLI.new(command.split.concat(args), env).execute - end - end + register(keyword, command) end end end + # This returns all the registered alias commands. def commands @aliases 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 exec "#{command[1..-1]} #{args.join(" ")}".strip + end + + return CLI.new(command.split.concat(args), env).execute + end + end + end end end From f16751a46d2b8190f18722f3a2d59ee6fd474270 Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Fri, 16 Feb 2018 13:00:39 -0700 Subject: [PATCH 3/8] start working through tests --- lib/vagrant/alias.rb | 19 +++++++++---- test/unit/vagrant/alias_test.rb | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 test/unit/vagrant/alias_test.rb diff --git a/lib/vagrant/alias.rb b/lib/vagrant/alias.rb index 660ec001f..29b924976 100644 --- a/lib/vagrant/alias.rb +++ b/lib/vagrant/alias.rb @@ -9,13 +9,12 @@ module Vagrant aliases_file = env.home_path.join("aliases") if aliases_file.file? aliases_file.readlines.each do |line| - # skip comments - next if line.strip.start_with?("#") - # separate keyword-command pairs - keyword, command = line.split("=").collect(&:strip) + keyword, command = interpret(line) - register(keyword, command) + if keyword && command + register(keyword, command) + end end end end @@ -25,6 +24,16 @@ module Vagrant @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) + + [keyword, command] + end + # This registers an alias. def register(keyword, command) @aliases.register(keyword.to_sym) do diff --git a/test/unit/vagrant/alias_test.rb b/test/unit/vagrant/alias_test.rb new file mode 100644 index 000000000..b15eac33d --- /dev/null +++ b/test/unit/vagrant/alias_test.rb @@ -0,0 +1,49 @@ +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 "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 + end +end From 57419fd12cc84ca88a5f99932359112a64a07ba1 Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Fri, 16 Feb 2018 13:28:41 -0700 Subject: [PATCH 4/8] quick fix to register method --- lib/vagrant/alias.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/vagrant/alias.rb b/lib/vagrant/alias.rb index 29b924976..caf070fe0 100644 --- a/lib/vagrant/alias.rb +++ b/lib/vagrant/alias.rb @@ -5,10 +5,10 @@ module Vagrant class Alias def initialize(env) @aliases = Registry.new + @env = env - aliases_file = env.home_path.join("aliases") - if aliases_file.file? - aliases_file.readlines.each do |line| + if env.aliases_path.file? + env.aliases_path.readlines.each do |line| # separate keyword-command pairs keyword, command = interpret(line) @@ -31,6 +31,11 @@ module Vagrant 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 @@ -43,7 +48,7 @@ module Vagrant return exec "#{command[1..-1]} #{args.join(" ")}".strip end - return CLI.new(command.split.concat(args), env).execute + return CLI.new(command.split.concat(args), @env).execute end end end From f46ebf52405936278ee01e532801ea56cab2429c Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Fri, 16 Feb 2018 13:28:57 -0700 Subject: [PATCH 5/8] throw an exception when whitespace is found within an alias keyword --- lib/vagrant/environment.rb | 4 ++++ lib/vagrant/errors.rb | 4 ++++ templates/locales/en.yml | 6 ++++++ test/unit/vagrant/alias_test.rb | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 92be3d13a..a584f3163 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -57,6 +57,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 @@ -123,6 +126,7 @@ module Vagrant @data_dir = @home_path.join("data") @gems_path = Vagrant::Bundler.instance.plugin_gem_path @tmp_path = @home_path.join("tmp") + @aliases_path = @home_path.join("aliases") @machine_index_dir = @data_dir.join("machine-index") # Prepare the directories diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index a6928bc12..af09512d3 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 b299481b4..6e6beba71 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -378,6 +378,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 index b15eac33d..5a106684d 100644 --- a/test/unit/vagrant/alias_test.rb +++ b/test/unit/vagrant/alias_test.rb @@ -25,6 +25,18 @@ describe Vagrant::Alias do 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") @@ -45,5 +57,12 @@ describe Vagrant::Alias do 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 From 42f72e1099aee7f37b699c273b28c5bdc0566726 Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Sun, 25 Feb 2018 22:21:16 -0700 Subject: [PATCH 6/8] some light changes per PR feedback --- lib/vagrant/alias.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/vagrant/alias.rb b/lib/vagrant/alias.rb index caf070fe0..26efc6d67 100644 --- a/lib/vagrant/alias.rb +++ b/lib/vagrant/alias.rb @@ -32,7 +32,7 @@ module Vagrant keyword, command = line.split("=", 2).collect(&:strip) # validate the keyword - if keyword.match(/[\s]/i) + if keyword.match(/\s/i) raise Errors::AliasInvalidError, alias: line, message: "Alias keywords must not contain any whitespace." end @@ -45,7 +45,7 @@ module Vagrant lambda do |args| # directly execute shell commands if command.start_with?("!") - return exec "#{command[1..-1]} #{args.join(" ")}".strip + return Util::SafeExec.exec "#{command[1..-1]} #{args.join(" ")}".strip end return CLI.new(command.split.concat(args), @env).execute From 3dbcdea6915de43d04bd74695c5bd809c524879e Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Sun, 25 Feb 2018 22:24:36 -0700 Subject: [PATCH 7/8] allow the alias file path to be defined via an environment variable --- lib/vagrant/environment.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index a584f3163..0f9bdee8e 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -126,9 +126,11 @@ module Vagrant @data_dir = @home_path.join("data") @gems_path = Vagrant::Bundler.instance.plugin_gem_path @tmp_path = @home_path.join("tmp") - @aliases_path = @home_path.join("aliases") @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 From 1fc2129e0c9c739e0ea072f4eb86aee10b7c477f Mon Sep 17 00:00:00 2001 From: Zachary Flower Date: Mon, 26 Feb 2018 13:41:38 -0700 Subject: [PATCH 8/8] add new cli alias documentation --- website/source/docs/cli/aliases.html.md | 74 +++++++++++++++++++ .../other/environmental-variables.html.md | 5 ++ website/source/layouts/docs.erb | 1 + 3 files changed, 80 insertions(+) create mode 100644 website/source/docs/cli/aliases.html.md 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 cec3e600f..da713467b 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 3bee5f94f..ecd0a7267 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