Browse Source

CLITool: Port PluginManager to use CLITool

In order to unify the handling and reuse the CLITool infrastructure
the plugin manager should make use of this as well.

This obsolets the -i and --install options but requires the user
to use `install` as the first argument of the CLI.

This is basically just a port of the existing functionality, which
is also the reason why this is not a refactoring of the plugin manager,
which will come in a separate commit.
Alexander Reelsen 10 years ago
parent
commit
2f54b89a23

+ 1 - 10
core/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java

@@ -24,6 +24,7 @@ import org.elasticsearch.ElasticsearchTimeoutException;
 import org.elasticsearch.Version;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.SuppressForbidden;
+import org.elasticsearch.common.cli.Terminal;
 import org.elasticsearch.common.unit.TimeValue;
 
 import java.io.*;
@@ -135,22 +136,12 @@ public class HttpDownloadHelper {
     /**
      * verbose progress system prints to some output stream
      */
-    @SuppressForbidden(reason = "System#out")
     public static class VerboseProgress implements DownloadProgress {
         private int dots = 0;
         // CheckStyle:VisibilityModifier OFF - bc
         PrintWriter writer;
         // CheckStyle:VisibilityModifier ON
 
-        /**
-         * Construct a verbose progress reporter.
-         *
-         * @param out the output stream.
-         */
-        public VerboseProgress(PrintStream out) {
-            this.writer = new PrintWriter(out);
-        }
-
         /**
          * Construct a verbose progress reporter.
          *

+ 39 - 288
core/src/main/java/org/elasticsearch/plugins/PluginManager.java

@@ -22,20 +22,17 @@ package org.elasticsearch.plugins;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterators;
-
 import org.apache.lucene.util.IOUtils;
-import org.elasticsearch.*;
+import org.elasticsearch.ElasticsearchTimeoutException;
+import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.Version;
 import org.elasticsearch.bootstrap.JarHell;
 import org.elasticsearch.common.SuppressForbidden;
 import org.elasticsearch.common.cli.Terminal;
-import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.http.client.HttpDownloadHelper;
 import org.elasticsearch.common.io.FileSystemUtils;
-import org.elasticsearch.common.logging.log4j.LogConfigurator;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.env.Environment;
-import org.elasticsearch.node.internal.InternalSettingsPreparer;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -52,27 +49,18 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
 import static org.elasticsearch.common.Strings.hasLength;
+import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE;
 import static org.elasticsearch.common.io.FileSystemUtils.moveFilesWithoutOverwriting;
-import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS;
 
 /**
  *
  */
 public class PluginManager {
-    public static final class ACTION {
-        public static final int NONE = 0;
-        public static final int INSTALL = 1;
-        public static final int REMOVE = 2;
-        public static final int LIST = 3;
-    }
 
     public enum OutputMode {
         DEFAULT, SILENT, VERBOSE
     }
 
-    // By default timeout is 0 which means no timeout
-    public static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueMillis(0);
-
     private static final ImmutableSet<String> BLACKLIST = ImmutableSet.<String>builder()
             .add("elasticsearch",
                     "elasticsearch.bat",
@@ -81,7 +69,7 @@ public class PluginManager {
                     "plugin.bat",
                     "service.bat").build();
 
-    private static final ImmutableSet<String> OFFICIAL_PLUGINS = ImmutableSet.<String>builder()
+    static final ImmutableSet<String> OFFICIAL_PLUGINS = ImmutableSet.<String>builder()
             .add(
                     "elasticsearch-analysis-icu",
                     "elasticsearch-analysis-kuromoji",
@@ -108,9 +96,9 @@ public class PluginManager {
         this.timeout = timeout;
     }
 
-    public void downloadAndExtract(String name) throws IOException {
+    public void downloadAndExtract(String name, Terminal terminal) throws IOException {
         if (name == null) {
-            throw new IllegalArgumentException("plugin name must be supplied with --install [name].");
+            throw new IllegalArgumentException("plugin name must be supplied with install [name].");
         }
         HttpDownloadHelper downloadHelper = new HttpDownloadHelper();
         boolean downloaded = false;
@@ -118,7 +106,7 @@ public class PluginManager {
         if (outputMode == OutputMode.SILENT) {
             progress = new HttpDownloadHelper.NullProgress();
         } else {
-            progress = new HttpDownloadHelper.VerboseProgress(SysOut.getOut());
+            progress = new HttpDownloadHelper.VerboseProgress(terminal.writer());
         }
 
         if (!Files.isWritable(environment.pluginsFile())) {
@@ -132,13 +120,13 @@ public class PluginManager {
         // extract the plugin
         final Path extractLocation = pluginHandle.extractedDir(environment);
         if (Files.exists(extractLocation)) {
-            throw new IOException("plugin directory " + extractLocation.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using --remove " + name + " command");
+            throw new IOException("plugin directory " + extractLocation.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using remove " + name + " command");
         }
 
         // first, try directly from the URL provided
         if (url != null) {
             URL pluginUrl = new URL(url);
-            log("Trying " + pluginUrl.toExternalForm() + "...");
+            terminal.println("Trying %s ...", pluginUrl.toExternalForm());
             try {
                 downloadHelper.download(pluginUrl, pluginFile, progress, this.timeout);
                 downloaded = true;
@@ -146,7 +134,7 @@ public class PluginManager {
                 throw e;
             } catch (Exception e) {
                 // ignore
-                log("Failed: " + ExceptionsHelper.detailedMessage(e));
+                terminal.println("Failed: %s", ExceptionsHelper.detailedMessage(e));
             }
         } else {
             if (PluginHandle.isOfficialPlugin(pluginHandle.repo, pluginHandle.user, pluginHandle.version)) {
@@ -157,7 +145,7 @@ public class PluginManager {
         if (!downloaded) {
             // We try all possible locations
             for (URL url : pluginHandle.urls()) {
-                log("Trying " + url.toExternalForm() + "...");
+                terminal.println("Trying %s ...", url.toExternalForm());
                 try {
                     downloadHelper.download(url, pluginFile, progress, this.timeout);
                     downloaded = true;
@@ -165,7 +153,7 @@ public class PluginManager {
                 } catch (ElasticsearchTimeoutException e) {
                     throw e;
                 } catch (Exception e) {
-                    debug("Failed: " + ExceptionsHelper.detailedMessage(e));
+                    terminal.println(VERBOSE, "Failed: %s", ExceptionsHelper.detailedMessage(e));
                 }
             }
         }
@@ -181,9 +169,7 @@ public class PluginManager {
         final List<URL> jars = new ArrayList<>();
         ClassLoader loader = PluginManager.class.getClassLoader();
         if (loader instanceof URLClassLoader) {
-            for (URL url : ((URLClassLoader) loader).getURLs()) {
-                jars.add(url);
-            }
+            Collections.addAll(jars, ((URLClassLoader) loader).getURLs());
         }
 
         // add any jars we find in the plugin to the list
@@ -199,7 +185,7 @@ public class PluginManager {
 
         // check combined (current classpath + new jars to-be-added)
         try {
-            JarHell.checkJarHell(jars.toArray(new URL[0]));
+            JarHell.checkJarHell(jars.toArray(new URL[jars.size()]));
         } catch (Exception ex) {
             throw new RuntimeException(ex);
         }
@@ -240,24 +226,24 @@ public class PluginManager {
 
                 });
             }
-            log("Installed " + name + " into " + extractLocation.toAbsolutePath());
+            terminal.println("Installed %s into %s", name, extractLocation.toAbsolutePath());
         } catch (Exception e) {
-            log("failed to extract plugin [" + pluginFile + "]: " + ExceptionsHelper.detailedMessage(e));
+            terminal.printError("failed to extract plugin [%s]: %s", pluginFile, ExceptionsHelper.detailedMessage(e));
             return;
         } finally {
             try {
                 Files.delete(pluginFile);
             } catch (Exception ex) {
-                log("Failed to delete plugin file" + pluginFile + " " + ex);
+                terminal.printError("Failed to delete plugin file %s %s", pluginFile, ex);
             }
         }
 
         if (FileSystemUtils.hasExtensions(extractLocation, ".java")) {
-            debug("Plugin installation assumed to be site plugin, but contains source code, aborting installation...");
+            terminal.printError("Plugin installation assumed to be site plugin, but contains source code, aborting installation...");
             try {
                 IOUtils.rm(extractLocation);
             } catch(Exception ex) {
-                debug("Failed to remove site plugin from path " + extractLocation + " - " + ex.getMessage());
+                terminal.printError("Failed to remove site plugin from path %s - %s", extractLocation, ex.getMessage());
             }
             throw new IllegalArgumentException("Plugin installation assumed to be site plugin, but contains source code, aborting installation.");
         }
@@ -267,7 +253,7 @@ public class PluginManager {
         Path binFile = extractLocation.resolve("bin");
         if (Files.isDirectory(binFile)) {
             Path toLocation = pluginHandle.binDir(environment);
-            debug("Found bin, moving to " + toLocation.toAbsolutePath());
+            terminal.println(VERBOSE, "Found bin, moving to %s", toLocation.toAbsolutePath());
             if (Files.exists(toLocation)) {
                 IOUtils.rm(toLocation);
             }
@@ -298,18 +284,18 @@ public class PluginManager {
                     }
                 });
             } else {
-                debug("Skipping posix permissions - filestore doesn't support posix permission");
+                terminal.println(VERBOSE, "Skipping posix permissions - filestore doesn't support posix permission");
             }
-            debug("Installed " + name + " into " + toLocation.toAbsolutePath());
+            terminal.println(VERBOSE, "Installed %s into %s", name, toLocation.toAbsolutePath());
             potentialSitePlugin = false;
         }
 
         Path configFile = extractLocation.resolve("config");
         if (Files.isDirectory(configFile)) {
             Path configDestLocation = pluginHandle.configDir(environment);
-            debug("Found config, moving to " + configDestLocation.toAbsolutePath());
+            terminal.println(VERBOSE, "Found config, moving to %s", configDestLocation.toAbsolutePath());
             moveFilesWithoutOverwriting(configFile, configDestLocation, ".new");
-            debug("Installed " + name + " into " + configDestLocation.toAbsolutePath());
+            terminal.println(VERBOSE, "Installed %s into %s", name, configDestLocation.toAbsolutePath());
             potentialSitePlugin = false;
         }
 
@@ -317,13 +303,13 @@ public class PluginManager {
         // so its probably a _site, and it it does not have a _site in it, move everything to _site
         if (!Files.exists(extractLocation.resolve("_site"))) {
             if (potentialSitePlugin && !FileSystemUtils.hasExtensions(extractLocation, ".class", ".jar")) {
-                log("Identified as a _site plugin, moving to _site structure ...");
+                terminal.println(VERBOSE, "Identified as a _site plugin, moving to _site structure ...");
                 Path site = extractLocation.resolve("_site");
                 Path tmpLocation = environment.pluginsFile().resolve(extractLocation.getFileName() + ".tmp");
                 Files.move(extractLocation, tmpLocation);
                 Files.createDirectories(extractLocation);
                 Files.move(tmpLocation, site);
-                debug("Installed " + name + " into " + site.toAbsolutePath());
+                terminal.println(VERBOSE, "Installed " + name + " into " + site.toAbsolutePath());
             }
         }
     }
@@ -355,9 +341,9 @@ public class PluginManager {
         return tmp;
     }
 
-    public void removePlugin(String name) throws IOException {
+    public void removePlugin(String name, Terminal terminal) throws IOException {
         if (name == null) {
-            throw new IllegalArgumentException("plugin name must be supplied with --remove [name].");
+            throw new IllegalArgumentException("plugin name must be supplied with remove [name].");
         }
         PluginHandle pluginHandle = PluginHandle.parse(name);
         boolean removed = false;
@@ -365,7 +351,7 @@ public class PluginManager {
         checkForForbiddenName(pluginHandle.name);
         Path pluginToDelete = pluginHandle.extractedDir(environment);
         if (Files.exists(pluginToDelete)) {
-            debug("Removing: " + pluginToDelete);
+            terminal.println(VERBOSE, "Removing: %s", pluginToDelete);
             try {
                 IOUtils.rm(pluginToDelete);
             } catch (IOException ex){
@@ -376,7 +362,7 @@ public class PluginManager {
         }
         pluginToDelete = pluginHandle.distroFile(environment);
         if (Files.exists(pluginToDelete)) {
-            debug("Removing: " + pluginToDelete);
+            terminal.println(VERBOSE, "Removing: %s", pluginToDelete);
             try {
                 Files.delete(pluginToDelete);
             } catch (Exception ex) {
@@ -387,7 +373,7 @@ public class PluginManager {
         }
         Path binLocation = pluginHandle.binDir(environment);
         if (Files.exists(binLocation)) {
-            debug("Removing: " + binLocation);
+            terminal.println(VERBOSE, "Removing: %s", binLocation);
             try {
                 IOUtils.rm(binLocation);
             } catch (IOException ex){
@@ -398,9 +384,9 @@ public class PluginManager {
         }
 
         if (removed) {
-            log("Removed " + name);
+            terminal.println("Removed %s", name);
         } else {
-            log("Plugin " + name + " not found. Run plugin --list to get list of installed plugins.");
+            terminal.println("Plugin %s not found. Run plugin --list to get list of installed plugins.", name);
         }
     }
 
@@ -425,250 +411,15 @@ public class PluginManager {
         }
     }
 
-    public void listInstalledPlugins() throws IOException {
+    public void listInstalledPlugins(Terminal terminal) throws IOException {
         Path[] plugins = getListInstalledPlugins();
-        log("Installed plugins in " + environment.pluginsFile().toAbsolutePath() + ":");
+        terminal.println("Installed plugins in %s:", environment.pluginsFile().toAbsolutePath());
         if (plugins == null || plugins.length == 0) {
-            log("    - No plugin detected");
+            terminal.println("    - No plugin detected");
         } else {
-            for (int i = 0; i < plugins.length; i++) {
-                log("    - " + plugins[i].getFileName());
-            }
-        }
-    }
-
-    private static final int EXIT_CODE_OK = 0;
-    private static final int EXIT_CODE_CMD_USAGE = 64;
-    private static final int EXIT_CODE_IO_ERROR = 74;
-    private static final int EXIT_CODE_ERROR = 70;
-
-    public static void main(String[] args) {
-        Tuple<Settings, Environment> initialSettings = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, Terminal.DEFAULT);
-        LogConfigurator.configure(initialSettings.v1());
-
-        try {
-            Files.createDirectories(initialSettings.v2().pluginsFile());
-        } catch (IOException e) {
-            displayHelp("Unable to create plugins dir: " + initialSettings.v2().pluginsFile());
-            System.exit(EXIT_CODE_ERROR);
-        }
-
-        String url = null;
-        OutputMode outputMode = OutputMode.DEFAULT;
-        String pluginName = null;
-        TimeValue timeout = DEFAULT_TIMEOUT;
-        int action = ACTION.NONE;
-
-        if (args.length < 1) {
-            displayHelp(null);
-        }
-
-        try {
-            for (int c = 0; c < args.length; c++) {
-                String command = args[c];
-                switch (command) {
-                    case "-u":
-                    case "--url":
-                    // deprecated versions:
-                    case "url":
-                    case "-url":
-                        url = getCommandValue(args, ++c, "--url");
-                        // Until update is supported, then supplying a URL implies installing
-                        // By specifying this action, we also avoid silently failing without
-                        //  dubious checks.
-                        action = ACTION.INSTALL;
-                        break;
-                    case "-v":
-                    case "--verbose":
-                    // deprecated versions:
-                    case "verbose":
-                    case "-verbose":
-                        outputMode = OutputMode.VERBOSE;
-                        break;
-                    case "-s":
-                    case "--silent":
-                    // deprecated versions:
-                    case "silent":
-                    case "-silent":
-                        outputMode = OutputMode.SILENT;
-                        break;
-                    case "-i":
-                    case "--install":
-                    // deprecated versions:
-                    case "install":
-                    case "-install":
-                        pluginName = getCommandValue(args, ++c, "--install");
-                        action = ACTION.INSTALL;
-                        break;
-                    case "-r":
-                    case "--remove":
-                    // deprecated versions:
-                    case "remove":
-                    case "-remove":
-                        pluginName = getCommandValue(args, ++c, "--remove");
-                        action = ACTION.REMOVE;
-                        break;
-                    case "-t":
-                    case "--timeout":
-                    // deprecated versions:
-                    case "timeout":
-                    case "-timeout":
-                        String timeoutValue = getCommandValue(args, ++c, "--timeout");
-                        timeout = TimeValue.parseTimeValue(timeoutValue, DEFAULT_TIMEOUT, command);
-                        break;
-                    case "-l":
-                    case "--list":
-                        action = ACTION.LIST;
-                        break;
-                    case "-h":
-                    case "--help":
-                        displayHelp(null);
-                        break;
-                    default:
-                        displayHelp("Command [" + command + "] unknown.");
-                        // Unknown command. We break...
-                        System.exit(EXIT_CODE_CMD_USAGE);
-                }
-            }
-        } catch (Throwable e) {
-            displayHelp("Error while parsing options: " + e.getClass().getSimpleName() +
-                    ": " + e.getMessage());
-            System.exit(EXIT_CODE_CMD_USAGE);
-        }
-
-        if (action > ACTION.NONE) {
-            int exitCode = EXIT_CODE_ERROR; // we fail unless it's reset
-            PluginManager pluginManager = new PluginManager(initialSettings.v2(), url, outputMode, timeout);
-            switch (action) {
-                case ACTION.INSTALL:
-                    try {
-                        pluginManager.log("-> Installing " + Strings.nullToEmpty(pluginName) + "...");
-                        pluginManager.downloadAndExtract(pluginName);
-                        exitCode = EXIT_CODE_OK;
-                    } catch (IOException e) {
-                        exitCode = EXIT_CODE_IO_ERROR;
-                        pluginManager.log("Failed to install " + pluginName + ", reason: " + e.getMessage());
-                    } catch (Throwable e) {
-                        exitCode = EXIT_CODE_ERROR;
-                        displayHelp("Error while installing plugin, reason: " + e.getClass().getSimpleName() +
-                                ": " + e.getMessage());
-                    }
-                    break;
-                case ACTION.REMOVE:
-                    try {
-                        pluginManager.log("-> Removing " + Strings.nullToEmpty(pluginName) + "...");
-                        pluginManager.removePlugin(pluginName);
-                        exitCode = EXIT_CODE_OK;
-                    } catch (IllegalArgumentException e) {
-                        exitCode = EXIT_CODE_CMD_USAGE;
-                        pluginManager.log("Failed to remove " + pluginName + ", reason: " + e.getMessage());
-                    } catch (IOException e) {
-                        exitCode = EXIT_CODE_IO_ERROR;
-                        pluginManager.log("Failed to remove " + pluginName + ", reason: " + e.getMessage());
-                    } catch (Throwable e) {
-                        exitCode = EXIT_CODE_ERROR;
-                        displayHelp("Error while removing plugin, reason: " + e.getClass().getSimpleName() +
-                                ": " + e.getMessage());
-                    }
-                    break;
-                case ACTION.LIST:
-                    try {
-                        pluginManager.listInstalledPlugins();
-                        exitCode = EXIT_CODE_OK;
-                    } catch (Throwable e) {
-                        displayHelp("Error while listing plugins, reason: " + e.getClass().getSimpleName() +
-                                ": " + e.getMessage());
-                    }
-                    break;
-
-                default:
-                    pluginManager.log("Unknown Action [" + action + "]");
-                    exitCode = EXIT_CODE_ERROR;
-
+            for (Path plugin : plugins) {
+                terminal.println("    - " + plugin.getFileName());
             }
-            System.exit(exitCode); // exit here!
-        }
-    }
-
-    /**
-     * Get the value for the {@code flag} at the specified {@code arg} of the command line {@code args}.
-     * <p />
-     * This is useful to avoid having to check for multiple forms of unset (e.g., "   " versus "" versus {@code null}).
-     * @param args Incoming command line arguments.
-     * @param arg Expected argument containing the value.
-     * @param flag The flag whose value is being retrieved.
-     * @return Never {@code null}. The trimmed value.
-     * @throws NullPointerException if {@code args} is {@code null}.
-     * @throws ArrayIndexOutOfBoundsException if {@code arg} is negative.
-     * @throws IllegalStateException if {@code arg} is &gt;= {@code args.length}.
-     * @throws IllegalArgumentException if the value evaluates to blank ({@code null} or only whitespace)
-     */
-    private static String getCommandValue(String[] args, int arg, String flag) {
-        if (arg >= args.length) {
-            throw new IllegalStateException("missing value for " + flag + ". Usage: " + flag + " [value]");
-        }
-
-        // avoid having to interpret multiple forms of unset
-        String trimmedValue = Strings.emptyToNull(args[arg].trim());
-
-        // If we had a value that is blank, then fail immediately
-        if (trimmedValue == null) {
-            throw new IllegalArgumentException(
-                    "value for " + flag + "('" + args[arg] + "') must be set. Usage: " + flag + " [value]");
-        }
-
-        return trimmedValue;
-    }
-
-    private static void displayHelp(String message) {
-        SysOut.println("Usage:");
-        SysOut.println("    -u, --url     [plugin location]   : Set exact URL to download the plugin from");
-        SysOut.println("    -i, --install [plugin name]       : Downloads and installs listed plugins [*]");
-        SysOut.println("    -t, --timeout [duration]          : Timeout setting: 30s, 1m, 1h... (infinite by default)");
-        SysOut.println("    -r, --remove  [plugin name]       : Removes listed plugins");
-        SysOut.println("    -l, --list                        : List installed plugins");
-        SysOut.println("    -v, --verbose                     : Prints verbose messages");
-        SysOut.println("    -s, --silent                      : Run in silent mode");
-        SysOut.println("    -h, --help                        : Prints this help message");
-        SysOut.newline();
-        SysOut.println(" [*] Plugin name could be:");
-        SysOut.println("     elasticsearch-plugin-name    for Elasticsearch 2.0 Core plugin (download from download.elastic.co)");
-        SysOut.println("     elasticsearch/plugin/version for elasticsearch commercial plugins (download from download.elastic.co)");
-        SysOut.println("     groupId/artifactId/version   for community plugins (download from maven central or oss sonatype)");
-        SysOut.println("     username/repository          for site plugins (download from github master)");
-        SysOut.newline();
-        SysOut.println("Elasticsearch Core plugins:");
-        for (String o : OFFICIAL_PLUGINS) {
-            SysOut.println(" - " + o);
-        }
-
-        if (message != null) {
-            SysOut.newline();
-            SysOut.println("Message:");
-            SysOut.println("   " + message);
-        }
-    }
-
-    private void debug(String line) {
-        if (outputMode == OutputMode.VERBOSE) SysOut.println(line);
-    }
-
-    private void log(String line) {
-        if (outputMode != OutputMode.SILENT) SysOut.println(line);
-    }
-
-    @SuppressForbidden(reason = "System#out")
-    static class SysOut {
-
-        public static void newline() {
-            System.out.println();
-        }
-        public static void println(String msg) {
-            System.out.println(msg);
-        }
-
-        public static PrintStream getOut() {
-            return System.out;
         }
     }
 

+ 215 - 0
core/src/main/java/org/elasticsearch/plugins/PluginManagerCliParser.java

@@ -0,0 +1,215 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.plugins;
+
+import com.google.common.base.Strings;
+import org.apache.commons.cli.CommandLine;
+import org.elasticsearch.common.cli.CliTool;
+import org.elasticsearch.common.cli.CliToolConfig;
+import org.elasticsearch.common.cli.Terminal;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.logging.log4j.LogConfigurator;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.env.Environment;
+import org.elasticsearch.node.internal.InternalSettingsPreparer;
+import org.elasticsearch.plugins.PluginManager.OutputMode;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
+import static org.elasticsearch.common.cli.CliToolConfig.Builder.option;
+import static org.elasticsearch.common.settings.Settings.EMPTY;
+
+public class PluginManagerCliParser extends CliTool {
+
+    // By default timeout is 0 which means no timeout
+    public static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueMillis(0);
+
+    private static final CliToolConfig CONFIG = CliToolConfig.config("plugin", PluginManagerCliParser.class)
+            .cmds(ListPlugins.CMD, Install.CMD, Remove.CMD)
+            .build();
+
+    public static void main(String[] args) {
+        Tuple<Settings, Environment> initialSettings = InternalSettingsPreparer.prepareSettings(EMPTY, true, Terminal.DEFAULT);
+        LogConfigurator.configure(initialSettings.v1());
+        int status = new PluginManagerCliParser().execute(args);
+        System.exit(status);
+    }
+
+    public PluginManagerCliParser() {
+        super(CONFIG);
+    }
+
+    public PluginManagerCliParser(Terminal terminal) {
+        super(CONFIG, terminal);
+    }
+
+    @Override
+    protected Command parse(String cmdName, CommandLine cli) throws Exception {
+        switch (cmdName.toLowerCase(Locale.ROOT)) {
+            case Install.NAME:
+                return Install.parse(terminal, cli);
+            case ListPlugins.NAME:
+                return ListPlugins.parse(terminal, cli);
+            case Remove.NAME:
+                return Remove.parse(terminal, cli);
+            default:
+                assert false : "can't get here as cmd name is validated before this method is called";
+                return exitCmd(ExitStatus.USAGE);
+        }
+    }
+
+    /**
+     * List all installed plugins
+     */
+    static class ListPlugins extends CliTool.Command {
+
+        private static final String NAME = "list";
+
+        private static final CliToolConfig.Cmd CMD = cmd(NAME, ListPlugins.class).build();
+        private final OutputMode outputMode;
+
+        public static Command parse(Terminal terminal, CommandLine cli) {
+            OutputMode outputMode = OutputMode.DEFAULT;
+            if (cli.hasOption("s")) {
+                outputMode = OutputMode.SILENT;
+            }
+            if (cli.hasOption("v")) {
+                outputMode = OutputMode.VERBOSE;
+            }
+
+            return new ListPlugins(terminal, outputMode);
+        }
+
+        ListPlugins(Terminal terminal, OutputMode outputMode) {
+            super(terminal);
+            this.outputMode = outputMode;
+        }
+
+        @Override
+        public ExitStatus execute(Settings settings, Environment env) throws Exception {
+            PluginManager pluginManager = new PluginManager(env, null, outputMode, DEFAULT_TIMEOUT);
+            pluginManager.listInstalledPlugins(terminal);
+            return ExitStatus.OK;
+        }
+    }
+
+    /**
+     * Remove a plugin
+     */
+    static class Remove extends CliTool.Command {
+
+        private static final String NAME = "remove";
+
+        private static final CliToolConfig.Cmd CMD = cmd(NAME, Remove.class).build();
+
+        public static Command parse(Terminal terminal, CommandLine cli) {
+            String[] args = cli.getArgs();
+            if (args.length == 0) {
+                return exitCmd(ExitStatus.USAGE, terminal, "plugin name is missing (type -h for help)");
+            }
+
+            OutputMode outputMode = OutputMode.DEFAULT;
+            if (cli.hasOption("s")) {
+                outputMode = OutputMode.SILENT;
+            }
+            if (cli.hasOption("v")) {
+                outputMode = OutputMode.VERBOSE;
+            }
+
+            return new Remove(terminal, outputMode, args[0]);
+        }
+
+        private OutputMode outputMode;
+        final String pluginName;
+
+        Remove(Terminal terminal, OutputMode outputMode, String pluginToRemove) {
+            super(terminal);
+            this.outputMode = outputMode;
+            this.pluginName = pluginToRemove;
+        }
+
+        @Override
+        public ExitStatus execute(Settings settings, Environment env) throws Exception {
+
+            PluginManager pluginManager = new PluginManager(env, null, outputMode, DEFAULT_TIMEOUT);
+            terminal.println("-> Removing " + Strings.nullToEmpty(pluginName) + "...");
+            pluginManager.removePlugin(pluginName, terminal);
+            return ExitStatus.OK;
+        }
+    }
+
+    /**
+     * Installs a plugin
+     */
+    static class Install extends Command {
+
+        private static final String NAME = "install";
+
+        private static final CliToolConfig.Cmd CMD = cmd(NAME, Install.class)
+                .options(option("u", "url").required(false).hasArg(true))
+                .options(option("t", "timeout").required(false).hasArg(false))
+                .build();
+
+        static Command parse(Terminal terminal, CommandLine cli) {
+            String[] args = cli.getArgs();
+            if ((args == null) || (args.length == 0)) {
+                return exitCmd(ExitStatus.USAGE, terminal, "plugin name is missing (type -h for help)");
+            }
+
+            String name = args[0];
+            TimeValue timeout = TimeValue.parseTimeValue(cli.getOptionValue("t"), DEFAULT_TIMEOUT, "cli");
+            String url = cli.getOptionValue("u");
+
+            OutputMode outputMode = OutputMode.DEFAULT;
+            if (cli.hasOption("s")) {
+                outputMode = OutputMode.SILENT;
+            }
+            if (cli.hasOption("v")) {
+                outputMode = OutputMode.VERBOSE;
+            }
+
+            return new Install(terminal, name, outputMode, url, timeout);
+        }
+
+        final String name;
+        private OutputMode outputMode;
+        final String url;
+        final TimeValue timeout;
+
+        Install(Terminal terminal, String name, OutputMode outputMode, String url, TimeValue timeout) {
+            super(terminal);
+            this.name = name;
+            this.outputMode = outputMode;
+            this.url = url;
+            this.timeout = timeout;
+        }
+
+        @Override
+        public ExitStatus execute(Settings settings, Environment env) throws Exception {
+            PluginManager pluginManager = new PluginManager(env, url, outputMode, timeout);
+            terminal.println("-> Installing " + Strings.nullToEmpty(name) + "...");
+            pluginManager.downloadAndExtract(name, terminal);
+            return ExitStatus.OK;
+        }
+    }
+}

+ 56 - 0
core/src/main/resources/org/elasticsearch/plugins/plugin-install.help

@@ -0,0 +1,56 @@
+NAME
+
+    install - Install a plugin
+
+SYNOPSIS
+
+    plugin install <name>
+
+DESCRIPTION
+
+    This command installs an elasticsearch plugin
+
+    <name> can be one of the official plugins, or refer to a github repository, or to one of the official plugins
+
+    The notation of just specifying a plugin name, downloads an officially supported plugin.
+
+    The notation of 'elasticsearch/plugin/version' allows to easily download a commercial elastic plugin.
+
+    The notation of 'groupId/artifactId/version' refers to community plugins using maven central or sonatype
+
+    The notation of 'username/repository' refers to a github repository.
+
+EXAMPLES
+
+    plugin install elasticsearch-analysis-kuromoji
+
+    plugin install elasticsearch/shield/latest
+
+    plugin install lmenezes/elasticsearch-kopf
+
+OFFICIAL PLUGINS
+
+    The following plugins are officially supported and can be installed by just referring to their name
+
+    - elasticsearch-analysis-icu
+    - elasticsearch-analysis-kuromoji
+    - elasticsearch-analysis-phonetic
+    - elasticsearch-analysis-smartcn
+    - elasticsearch-analysis-stempel
+    - elasticsearch-cloud-aws
+    - elasticsearch-cloud-azure
+    - elasticsearch-cloud-gce
+    - elasticsearch-delete-by-query
+    - elasticsearch-lang-javascript
+    - elasticsearch-lang-python
+
+
+OPTIONS
+
+    -u,--url                     URL to retrive the plugin from
+
+    -t,--timeout                 Timeout until the plugin download is abort
+
+    -v,--verbose                 Verbose output
+
+    -h,--help                    Shows this message

+ 12 - 0
core/src/main/resources/org/elasticsearch/plugins/plugin-list.help

@@ -0,0 +1,12 @@
+NAME
+
+    list - List all plugins
+
+SYNOPSIS
+
+    plugin list
+
+DESCRIPTION
+
+    This command lists all installed elasticsearch plugins
+

+ 12 - 0
core/src/main/resources/org/elasticsearch/plugins/plugin-remove.help

@@ -0,0 +1,12 @@
+NAME
+
+    remove - Remove a plugin
+
+SYNOPSIS
+
+    plugin remove <name>
+
+DESCRIPTION
+
+    This command removes an elasticsearch plugin
+

+ 24 - 0
core/src/main/resources/org/elasticsearch/plugins/plugin.help

@@ -0,0 +1,24 @@
+NAME
+
+    plugin - Manages plugins
+
+SYNOPSIS
+
+    plugin <command>
+
+DESCRIPTION
+
+    Manage plugins
+
+COMMANDS
+
+    install    Install a plugin
+
+    remove     Remove a plugin
+
+    list       List installed plugins
+
+NOTES
+
+    [*] For usage help on specific commands please type "plugin <command> -h"
+

+ 1 - 1
core/src/test/java/org/elasticsearch/common/cli/CliToolTestCase.java

@@ -49,7 +49,7 @@ public abstract class CliToolTestCase extends ElasticsearchTestCase {
         System.clearProperty("es.default.path.home");
     }
 
-    protected static String[] args(String command) {
+    public static String[] args(String command) {
         if (!Strings.hasLength(command)) {
             return Strings.EMPTY_ARRAY;
         }

+ 232 - 241
core/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java

@@ -18,17 +18,18 @@
  */
 package org.elasticsearch.plugins;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Predicate;
 import org.apache.http.impl.client.HttpClients;
-import org.apache.lucene.util.IOUtils;
 import org.apache.lucene.util.LuceneTestCase;
 import org.elasticsearch.ElasticsearchException;
-import org.elasticsearch.ElasticsearchTimeoutException;
 import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
 import org.elasticsearch.action.admin.cluster.node.info.PluginInfo;
+import org.elasticsearch.common.cli.CliTool;
+import org.elasticsearch.common.cli.CliToolTestCase.CaptureOutputTerminal;
 import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.io.Streams;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.node.internal.InternalSettingsPreparer;
 import org.elasticsearch.rest.RestStatus;
@@ -37,6 +38,8 @@ import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
 import org.elasticsearch.test.junit.annotations.Network;
 import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
 import org.elasticsearch.test.rest.client.http.HttpResponse;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -46,14 +49,16 @@ import java.nio.file.Path;
 import java.nio.file.attribute.PosixFileAttributeView;
 import java.nio.file.attribute.PosixFileAttributes;
 import java.nio.file.attribute.PosixFilePermission;
+import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
+import static org.elasticsearch.common.cli.CliTool.ExitStatus.USAGE;
+import static org.elasticsearch.common.cli.CliToolTestCase.args;
 import static org.elasticsearch.common.io.FileSystemUtilsTests.assertFileContent;
+import static org.elasticsearch.common.settings.Settings.settingsBuilder;
 import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertDirectoryExists;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileExists;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.*;
 
 @ClusterScope(scope = Scope.TEST, numDataNodes = 0, transportClientRatio = 0.0)
@@ -62,20 +67,50 @@ import static org.hamcrest.Matchers.*;
 // if its in your classpath, then do not use plugins!!!!!!
 public class PluginManagerTests extends ElasticsearchIntegrationTest {
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testDownloadAndExtract_NullName_ThrowsException() throws IOException {
-        pluginManager(getPluginUrlForResource("plugin_single_folder.zip")).downloadAndExtract(null);
+    private Tuple<Settings, Environment> initialSettings;
+    private CaptureOutputTerminal terminal = new CaptureOutputTerminal();
+
+    @Before
+    public void setup() throws Exception {
+        initialSettings = buildInitialSettings();
+        System.setProperty("es.default.path.home", initialSettings.v1().get("path.home"));
+        try {
+            Files.createDirectories(initialSettings.v2().pluginsFile());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        Path binDir = initialSettings.v2().homeFile().resolve("bin");
+        if (!Files.exists(binDir)) {
+            Files.createDirectories(binDir);
+        }
+        Path configDir = initialSettings.v2().configFile();
+        if (!Files.exists(configDir)) {
+            Files.createDirectories(configDir);
+        }
+
+    }
+
+    @After
+    public void clearPathHome() {
+        System.clearProperty("es.default.path.home");
+    }
+
+    @Test
+    public void testThatPluginNameMustBeSupplied() throws IOException {
+        String pluginUrl = getPluginUrlForResource("plugin_single_folder.zip");
+        assertStatus("install --url " + pluginUrl, USAGE);
     }
 
     @Test
     public void testLocalPluginInstallSingleFolder() throws Exception {
         //When we have only a folder in top-level (no files either) we remove that folder while extracting
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
-        downloadAndExtract(pluginName, initialSettings, getPluginUrlForResource("plugin_single_folder.zip"));
+        String pluginUrl = getPluginUrlForResource("plugin_single_folder.zip");
+        String installCommand = String.format(Locale.ROOT, "install %s --url %s", pluginName, pluginUrl);
+        assertStatusOk(installCommand);
 
         internalCluster().startNode(initialSettings.v1());
-
         assertPluginLoaded(pluginName);
         assertPluginAvailable(pluginName);
     }
@@ -83,45 +118,30 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     @Test
     public void testLocalPluginInstallWithBinAndConfig() throws Exception {
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
         Environment env = initialSettings.v2();
         Path binDir = env.homeFile().resolve("bin");
-        if (!Files.exists(binDir)) {
-            Files.createDirectories(binDir);
-        }
         Path pluginBinDir = binDir.resolve(pluginName);
-        Path configDir = env.configFile();
-        if (!Files.exists(configDir)) {
-            Files.createDirectories(configDir);
-        }
-        Path pluginConfigDir =configDir.resolve(pluginName);
-        try {
-
-            PluginManager pluginManager = pluginManager(getPluginUrlForResource("plugin_with_bin_and_config.zip"), initialSettings);
-
-            pluginManager.downloadAndExtract(pluginName);
-
-            Path[] plugins = pluginManager.getListInstalledPlugins();
-
-            assertThat(plugins, arrayWithSize(1));
-            assertDirectoryExists(pluginBinDir);
-            assertDirectoryExists(pluginConfigDir);
-            Path toolFile = pluginBinDir.resolve("tool");
-            assertFileExists(toolFile);
 
-            // check that the file is marked executable, without actually checking that we can execute it.
-            PosixFileAttributeView view = Files.getFileAttributeView(toolFile, PosixFileAttributeView.class);
-            // the view might be null, on e.g. windows, there is nothing to check there!
-            if (view != null) {
-                PosixFileAttributes attributes = view.readAttributes();
-                assertTrue("unexpected permissions: " + attributes.permissions(),
-                           attributes.permissions().contains(PosixFilePermission.OWNER_EXECUTE));
-                assertTrue("unexpected permissions: " + attributes.permissions(),
-                        attributes.permissions().contains(PosixFilePermission.OWNER_READ));
-            }
-        } finally {
-            // we need to clean up the copied dirs
-            IOUtils.rm(pluginBinDir, pluginConfigDir);
+        Path pluginConfigDir = env.configFile().resolve(pluginName);
+        String pluginUrl = getPluginUrlForResource("plugin_with_bin_and_config.zip");
+        assertStatusOk("install " + pluginName + " --url " + pluginUrl + " --verbose");
+
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("list");
+        assertThat(terminal.getTerminalOutput(), hasItem(containsString(pluginName)));
+
+        assertDirectoryExists(pluginBinDir);
+        assertDirectoryExists(pluginConfigDir);
+        Path toolFile = pluginBinDir.resolve("tool");
+        assertFileExists(toolFile);
+
+        // check that the file is marked executable, without actually checking that we can execute it.
+        PosixFileAttributeView view = Files.getFileAttributeView(toolFile, PosixFileAttributeView.class);
+        // the view might be null, on e.g. windows, there is nothing to check there!
+        if (view != null) {
+            PosixFileAttributes attributes = view.readAttributes();
+            assertThat(attributes.permissions(), hasItem(PosixFilePermission.OWNER_EXECUTE));
+            assertThat(attributes.permissions(), hasItem(PosixFilePermission.OWNER_READ));
         }
     }
 
@@ -131,102 +151,76 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     @Test
     public void testLocalPluginInstallWithBinAndConfigInAlreadyExistingConfigDir_7890() throws Exception {
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
         Environment env = initialSettings.v2();
-
-        Path configDir = env.configFile();
-        if (!Files.exists(configDir)) {
-            Files.createDirectories(configDir);
-        }
-        Path pluginConfigDir = configDir.resolve(pluginName);
-
-        try {
-            PluginManager pluginManager = pluginManager(getPluginUrlForResource("plugin_with_config_v1.zip"), initialSettings);
-            pluginManager.downloadAndExtract(pluginName);
-
-            Path[] plugins = pluginManager.getListInstalledPlugins();
-            assertThat(plugins, arrayWithSize(1));
-
-            /*
-            First time, our plugin contains:
-            - config/test.txt (version1)
-             */
-            assertFileContent(pluginConfigDir, "test.txt", "version1\n");
-
-            // We now remove the plugin
-            pluginManager.removePlugin(pluginName);
-            // We should still have test.txt
-            assertFileContent(pluginConfigDir, "test.txt", "version1\n");
-
-            // Installing a new plugin version
-            /*
-            Second time, our plugin contains:
-            - config/test.txt (version2)
-            - config/dir/testdir.txt (version1)
-            - config/dir/subdir/testsubdir.txt (version1)
-             */
-            pluginManager = pluginManager(getPluginUrlForResource("plugin_with_config_v2.zip"), initialSettings);
-            pluginManager.downloadAndExtract(pluginName);
-
-            assertFileContent(pluginConfigDir, "test.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "test.txt.new", "version2\n");
-            assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1\n");
-
-            // Removing
-            pluginManager.removePlugin(pluginName);
-            assertFileContent(pluginConfigDir, "test.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "test.txt.new", "version2\n");
-            assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1\n");
-
-            // Installing a new plugin version
-            /*
-            Third time, our plugin contains:
-            - config/test.txt (version3)
-            - config/test2.txt (version1)
-            - config/dir/testdir.txt (version2)
-            - config/dir/testdir2.txt (version1)
-            - config/dir/subdir/testsubdir.txt (version2)
-             */
-            pluginManager = pluginManager(getPluginUrlForResource("plugin_with_config_v3.zip"), initialSettings);
-            pluginManager.downloadAndExtract(pluginName);
-
-            assertFileContent(pluginConfigDir, "test.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "test2.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "test.txt.new", "version3\n");
-            assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "dir/testdir.txt.new", "version2\n");
-            assertFileContent(pluginConfigDir, "dir/testdir2.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1\n");
-            assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt.new", "version2\n");
-        } finally {
-            // we need to clean up the copied dirs
-            IOUtils.rm(pluginConfigDir);
-        }
+        Path pluginConfigDir = env.configFile().resolve(pluginName);
+
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_with_config_v1.zip")));
+
+        /*
+        First time, our plugin contains:
+        - config/test.txt (version1)
+         */
+        assertFileContent(pluginConfigDir, "test.txt", "version1\n");
+
+        // We now remove the plugin
+        assertStatusOk("remove " + pluginName);
+
+        // We should still have test.txt
+        assertFileContent(pluginConfigDir, "test.txt", "version1\n");
+
+        // Installing a new plugin version
+        /*
+        Second time, our plugin contains:
+        - config/test.txt (version2)
+        - config/dir/testdir.txt (version1)
+        - config/dir/subdir/testsubdir.txt (version1)
+         */
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_with_config_v2.zip")));
+
+        assertFileContent(pluginConfigDir, "test.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "test.txt.new", "version2\n");
+        assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1\n");
+
+        // Removing
+        assertStatusOk("remove " + pluginName);
+        assertFileContent(pluginConfigDir, "test.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "test.txt.new", "version2\n");
+        assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1\n");
+
+        // Installing a new plugin version
+        /*
+        Third time, our plugin contains:
+        - config/test.txt (version3)
+        - config/test2.txt (version1)
+        - config/dir/testdir.txt (version2)
+        - config/dir/testdir2.txt (version1)
+        - config/dir/subdir/testsubdir.txt (version2)
+         */
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_with_config_v3.zip")));
+
+        assertFileContent(pluginConfigDir, "test.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "test2.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "test.txt.new", "version3\n");
+        assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "dir/testdir.txt.new", "version2\n");
+        assertFileContent(pluginConfigDir, "dir/testdir2.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1\n");
+        assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt.new", "version2\n");
     }
 
     // For #7152
     @Test
     public void testLocalPluginInstallWithBinOnly_7152() throws Exception {
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
         Environment env = initialSettings.v2();
         Path binDir = env.homeFile().resolve("bin");
-        if (!Files.exists(binDir)) {
-            Files.createDirectories(binDir);
-        }
         Path pluginBinDir = binDir.resolve(pluginName);
-        try {
-            PluginManager pluginManager = pluginManager(getPluginUrlForResource("plugin_with_bin_only.zip"), initialSettings);
-            pluginManager.downloadAndExtract(pluginName);
-            Path[] plugins = pluginManager.getListInstalledPlugins();
-            assertThat(plugins.length, is(1));
-            assertDirectoryExists(pluginBinDir);
-        } finally {
-            // we need to clean up the copied dirs
-            IOUtils.rm(pluginBinDir);
-        }
+
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_with_bin_only.zip")));
+        assertThatPluginIsListed(pluginName);
+        assertDirectoryExists(pluginBinDir);
     }
 
     @Test
@@ -234,8 +228,7 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
         //When we have only a folder in top-level (no files either) but it's called _site, we make it work
         //we can either remove the folder while extracting and then re-add it manually or just leave it as it is
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
-        downloadAndExtract(pluginName, initialSettings, getPluginUrlForResource("plugin_folder_site.zip"));
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_folder_site.zip")));
 
         internalCluster().startNode(initialSettings.v1());
 
@@ -247,8 +240,7 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     public void testLocalPluginWithoutFolders() throws Exception {
         //When we don't have folders at all in the top-level, but only files, we don't modify anything
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
-        downloadAndExtract(pluginName, initialSettings, getPluginUrlForResource("plugin_without_folders.zip"));
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_without_folders.zip")));
 
         internalCluster().startNode(initialSettings.v1());
 
@@ -260,8 +252,7 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     public void testLocalPluginFolderAndFile() throws Exception {
         //When we have a single top-level folder but also files in the top-level, we don't modify anything
         String pluginName = "plugin-test";
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
-        downloadAndExtract(pluginName, initialSettings, getPluginUrlForResource("plugin_folder_file.zip"));
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_folder_file.zip")));
 
         internalCluster().startNode(initialSettings.v1());
 
@@ -269,37 +260,13 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
         assertPluginAvailable(pluginName);
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testSitePluginWithSourceThrows() throws Exception {
+    @Test
+    public void testSitePluginWithSourceDoesNotInstall() throws Exception {
         String pluginName = "plugin-with-source";
-        downloadAndExtract(pluginName, buildInitialSettings(), getPluginUrlForResource("plugin_with_sourcefiles.zip"));
-    }
-
-    private PluginManager pluginManager(String pluginUrl) throws IOException {
-        return pluginManager(pluginUrl, buildInitialSettings());
-    }
-
-    private Tuple<Settings, Environment> buildInitialSettings() throws IOException {
-        Settings settings = Settings.settingsBuilder()
-            .put("discovery.zen.ping.multicast.enabled", false)
-            .put("http.enabled", true)
-            .put("path.home", createTempDir()).build();
-        return InternalSettingsPreparer.prepareSettings(settings, false);
-    }
-
-    /**
-     * We build a plugin manager instance which wait only for 30 seconds before
-     * raising an ElasticsearchTimeoutException
-     */
-    private PluginManager pluginManager(String pluginUrl, Tuple<Settings, Environment> initialSettings) throws IOException {
-        if (!Files.exists(initialSettings.v2().pluginsFile())) {
-            Files.createDirectories(initialSettings.v2().pluginsFile());
-        }
-        return new PluginManager(initialSettings.v2(), pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(30));
-    }
-
-    private void downloadAndExtract(String pluginName, Tuple<Settings, Environment> initialSettings, String pluginUrl) throws IOException {
-        pluginManager(pluginUrl, initialSettings).downloadAndExtract(pluginName);
+        String cmd = String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_with_sourcefiles.zip"));
+        int status = new PluginManagerCliParser(terminal).execute(args(cmd));
+        assertThat(status, is(USAGE.status()));
+        assertThat(terminal.getTerminalOutput(), hasItem(containsString("Plugin installation assumed to be site plugin, but contains source code, aborting installation")));
     }
 
     private void assertPluginLoaded(String pluginName) {
@@ -352,61 +319,44 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
 
     @Test
     public void testListInstalledEmpty() throws IOException {
-        Path[] plugins = pluginManager(null).getListInstalledPlugins();
-        assertThat(plugins, notNullValue());
-        assertThat(plugins.length, is(0));
+        assertStatusOk("list");
+        assertThat(terminal.getTerminalOutput(), hasItem(containsString("No plugin detected")));
     }
 
-    @Test(expected = IOException.class)
-    public void testInstallPluginNull() throws IOException {
-        pluginManager(null).downloadAndExtract("plugin-test");
-    }
-
-
     @Test
     public void testInstallPlugin() throws IOException {
-        PluginManager pluginManager = pluginManager(getPluginUrlForResource("plugin_with_classfile.zip"));
-
-        pluginManager.downloadAndExtract("plugin-classfile");
-        Path[] plugins = pluginManager.getListInstalledPlugins();
-        assertThat(plugins, notNullValue());
-        assertThat(plugins.length, is(1));
+        String pluginName = "plugin-classfile";
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_with_classfile.zip")));
+        assertThatPluginIsListed("plugin-classfile");
     }
 
     @Test
     public void testInstallSitePlugin() throws IOException {
-        Tuple<Settings, Environment> initialSettings = buildInitialSettings();
-        PluginManager pluginManager = pluginManager(getPluginUrlForResource("plugin_without_folders.zip"), initialSettings);
-
-        pluginManager.downloadAndExtract("plugin-site");
-        Path[] plugins = pluginManager.getListInstalledPlugins();
-        assertThat(plugins, notNullValue());
-        assertThat(plugins.length, is(1));
-
+        String pluginName = "plugin-site";
+        assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginName, getPluginUrlForResource("plugin_without_folders.zip")));
+        assertThatPluginIsListed(pluginName);
         // We want to check that Plugin Manager moves content to _site
         assertFileExists(initialSettings.v2().pluginsFile().resolve("plugin-site/_site"));
     }
 
 
-    private void singlePluginInstallAndRemove(String pluginShortName, String pluginCoordinates) throws IOException {
-        logger.info("--> trying to download and install [{}]", pluginShortName);
-        PluginManager pluginManager = pluginManager(pluginCoordinates);
-        try {
-            pluginManager.downloadAndExtract(pluginShortName);
-            Path[] plugins = pluginManager.getListInstalledPlugins();
-            assertThat(plugins, notNullValue());
-            assertThat(plugins.length, is(1));
-
-            // We remove it
-            pluginManager.removePlugin(pluginShortName);
-            plugins = pluginManager.getListInstalledPlugins();
-            assertThat(plugins, notNullValue());
-            assertThat(plugins.length, is(0));
-        } catch (IOException e) {
-            logger.warn("--> IOException raised while downloading plugin [{}]. Skipping test.", e, pluginShortName);
-        } catch (ElasticsearchTimeoutException e) {
-            logger.warn("--> timeout exception raised while downloading plugin [{}]. Skipping test.", pluginShortName);
+    private void singlePluginInstallAndRemove(String pluginDescriptor, String pluginName, String pluginCoordinates) throws IOException {
+        logger.info("--> trying to download and install [{}]", pluginDescriptor);
+        if (pluginCoordinates == null) {
+            assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginDescriptor));
+        } else {
+            assertStatusOk(String.format(Locale.ROOT, "install %s --url %s --verbose", pluginDescriptor, pluginCoordinates));
         }
+        assertThatPluginIsListed(pluginName);
+
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("remove " + pluginDescriptor);
+        assertThat(terminal.getTerminalOutput(), hasItem(containsString("Removing " + pluginDescriptor)));
+
+        // not listed anymore
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("list");
+        assertThat(terminal.getTerminalOutput(), not(hasItem(containsString(pluginName))));
     }
 
     /**
@@ -420,7 +370,7 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     @AwaitsFix(bugUrl = "fails with jar hell failures - http://build-us-00.elastic.co/job/es_core_master_oracle_6/519/testReport/")
     public void testInstallPluginWithElasticsearchDownloadService() throws IOException {
         assumeTrue("download.elastic.co is accessible", isDownloadServiceWorking("download.elastic.co", 80, "/elasticsearch/ci-test.txt"));
-        singlePluginInstallAndRemove("elasticsearch/elasticsearch-transport-thrift/2.4.0", null);
+        singlePluginInstallAndRemove("elasticsearch/elasticsearch-transport-thrift/2.4.0", "elasticsearch-transport-thrift", null);
     }
 
     /**
@@ -435,7 +385,7 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     public void testInstallPluginWithMavenCentral() throws IOException {
         assumeTrue("search.maven.org is accessible", isDownloadServiceWorking("search.maven.org", 80, "/"));
         assumeTrue("repo1.maven.org is accessible", isDownloadServiceWorking("repo1.maven.org", 443, "/maven2/org/elasticsearch/elasticsearch-transport-thrift/2.4.0/elasticsearch-transport-thrift-2.4.0.pom"));
-        singlePluginInstallAndRemove("org.elasticsearch/elasticsearch-transport-thrift/2.4.0", null);
+        singlePluginInstallAndRemove("org.elasticsearch/elasticsearch-transport-thrift/2.4.0", "elasticsearch-transport-thrift", null);
     }
 
     /**
@@ -448,7 +398,7 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     @Network
     public void testInstallPluginWithGithub() throws IOException {
         assumeTrue("github.com is accessible", isDownloadServiceWorking("github.com", 443, "/"));
-        singlePluginInstallAndRemove("elasticsearch/kibana", null);
+        singlePluginInstallAndRemove("elasticsearch/kibana", "kibana", null);
     }
 
     private boolean isDownloadServiceWorking(String host, int port, String resource) {
@@ -469,48 +419,38 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
     @Test
     public void testRemovePlugin() throws Exception {
         // We want to remove plugin with plugin short name
-        singlePluginInstallAndRemove("plugintest", getPluginUrlForResource("plugin_without_folders.zip"));
+        singlePluginInstallAndRemove("plugintest", "plugintest", getPluginUrlForResource("plugin_without_folders.zip"));
 
         // We want to remove plugin with groupid/artifactid/version form
-        singlePluginInstallAndRemove("groupid/plugintest/1.0.0", getPluginUrlForResource("plugin_without_folders.zip"));
+        singlePluginInstallAndRemove("groupid/plugintest/1.0.0", "plugintest", getPluginUrlForResource("plugin_without_folders.zip"));
 
         // We want to remove plugin with groupid/artifactid form
-        singlePluginInstallAndRemove("groupid/plugintest", getPluginUrlForResource("plugin_without_folders.zip"));
+        singlePluginInstallAndRemove("groupid/plugintest", "plugintest", getPluginUrlForResource("plugin_without_folders.zip"));
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void testRemovePlugin_NullName_ThrowsException() throws IOException {
-        pluginManager(getPluginUrlForResource("plugin_single_folder.zip")).removePlugin(null);
+        int status = new PluginManagerCliParser(terminal).execute(args("remove "));
+        assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(USAGE.status()));
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void testRemovePluginWithURLForm() throws Exception {
-        PluginManager pluginManager = pluginManager(null);
-        pluginManager.removePlugin("file://whatever");
+        int status = new PluginManagerCliParser(terminal).execute(args("remove file://whatever"));
+        assertThat(terminal.getTerminalOutput(), hasItem(containsString("Illegal plugin name")));
+        assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(USAGE.status()));
     }
 
     @Test
-    public void testForbiddenPluginName_ThrowsException() throws IOException {
-        runTestWithForbiddenName(null);
-        runTestWithForbiddenName("");
-        runTestWithForbiddenName("elasticsearch");
-        runTestWithForbiddenName("elasticsearch.bat");
-        runTestWithForbiddenName("elasticsearch.in.sh");
-        runTestWithForbiddenName("plugin");
-        runTestWithForbiddenName("plugin.bat");
-        runTestWithForbiddenName("service.bat");
-        runTestWithForbiddenName("ELASTICSEARCH");
-        runTestWithForbiddenName("ELASTICSEARCH.IN.SH");
-    }
-
-    private void runTestWithForbiddenName(String name) throws IOException {
-        try {
-            pluginManager(null).removePlugin(name);
-            fail("this plugin name [" + name +
-                    "] should not be allowed");
-        } catch (IllegalArgumentException e) {
-            // We expect that error
-        }
+    public void testForbiddenPluginNames() throws IOException {
+        assertStatus("remove elasticsearch", USAGE);
+        assertStatus("remove elasticsearch.bat", USAGE);
+        assertStatus("remove elasticsearch.in.sh", USAGE);
+        assertStatus("remove plugin", USAGE);
+        assertStatus("remove plugin.bat", USAGE);
+        assertStatus("remove service.bat", USAGE);
+        assertStatus("remove ELASTICSEARCH", USAGE);
+        assertStatus("remove ELASTICSEARCH.IN.SH", USAGE);
     }
 
     @Test
@@ -535,6 +475,33 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
         }
     }
 
+    @Test
+    public void testHelpWorks() throws IOException {
+        assertStatusOk("--help");
+        assertHelp("/org/elasticsearch/plugins/plugin.help");
+
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("install -h");
+        assertHelp("/org/elasticsearch/plugins/plugin-install.help");
+        for (String plugin : PluginManager.OFFICIAL_PLUGINS) {
+            assertThat(terminal.getTerminalOutput(), hasItem(containsString(plugin)));
+        }
+
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("remove --help");
+        assertHelp("/org/elasticsearch/plugins/plugin-remove.help");
+
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("list -h");
+        assertHelp("/org/elasticsearch/plugins/plugin-list.help");
+    }
+
+    private void assertHelp(String classPath) throws IOException {
+        String expectedDocs = Streams.copyToStringFromClasspath(classPath);
+        String returnedDocs = Joiner.on("").join(terminal.getTerminalOutput());
+        assertThat(returnedDocs.trim(), is(expectedDocs.trim()));
+    }
+
     /**
      * Retrieve a URL string that represents the resource with the given {@code resourceName}.
      * @param resourceName The resource name relative to {@link PluginManagerTests}.
@@ -546,4 +513,28 @@ public class PluginManagerTests extends ElasticsearchIntegrationTest {
 
         return "file://" + uri.getPath();
     }
+
+    private Tuple<Settings, Environment> buildInitialSettings() throws IOException {
+        Settings settings = settingsBuilder()
+                .put("discovery.zen.ping.multicast.enabled", false)
+                .put("http.enabled", true)
+                .put("path.home", createTempDir()).build();
+        return InternalSettingsPreparer.prepareSettings(settings, false);
+    }
+
+    private void assertStatusOk(String command) {
+        assertStatus(command, CliTool.ExitStatus.OK);
+    }
+
+    private void assertStatus(String command, CliTool.ExitStatus exitStatus) {
+        int status = new PluginManagerCliParser(terminal).execute(args(command));
+        assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(exitStatus.status()));
+    }
+
+    private void assertThatPluginIsListed(String pluginName) {
+        terminal.getTerminalOutput().clear();
+        assertStatusOk("list");
+        String message = String.format(Locale.ROOT, "Terminal output was: %s", terminal.getTerminalOutput());
+        assertThat(message, terminal.getTerminalOutput(), hasItem(containsString(pluginName)));
+    }
 }

+ 13 - 21
docs/reference/modules/plugins.asciidoc

@@ -20,7 +20,7 @@ Installing plugins typically take the following form:
 
 [source,sh]
 -----------------------------------
-bin/plugin --install plugin_name
+bin/plugin install plugin_name
 -----------------------------------
 
 The plugin will be automatically downloaded in this case from `download.elastic.co` download service using the
@@ -30,7 +30,7 @@ For older version of elasticsearch (prior to 2.0.0) or community plugins, you wo
 
 [source,sh]
 -----------------------------------
-bin/plugin --install <org>/<user/component>/<version>
+bin/plugin install <org>/<user/component>/<version>
 -----------------------------------
 
 The plugins will be automatically downloaded in this case from `download.elastic.co` (for older plugins),
@@ -45,7 +45,7 @@ for example:
 
 [source,sh]
 -----------------------------------
-bin/plugin --url file:///path/to/plugin --install plugin-name
+bin/plugin install plugin-name --url file:///path/to/plugin
 -----------------------------------
 
 
@@ -70,8 +70,8 @@ running:
 
 [source,js]
 --------------------------------------------------
-bin/plugin --install mobz/elasticsearch-head
-bin/plugin --install lukas-vlcek/bigdesk
+bin/plugin install mobz/elasticsearch-head
+bin/plugin install lukas-vlcek/bigdesk
 --------------------------------------------------
 
 Will install both of those site plugins, with `elasticsearch-head`
@@ -108,7 +108,7 @@ Removing plugins typically take the following form:
 
 [source,sh]
 -----------------------------------
-plugin --remove <pluginname>
+plugin remove <pluginname>
 -----------------------------------
 
 [float]
@@ -126,8 +126,8 @@ Note that exit codes could be:
 
 [source,sh]
 -----------------------------------
-bin/plugin --install mobz/elasticsearch-head --verbose
-plugin --remove head --silent
+bin/plugin install mobz/elasticsearch-head --verbose
+plugin remove head --silent
 -----------------------------------
 
 [float]
@@ -140,33 +140,25 @@ different values:
 [source,sh]
 -----------------------------------
 # Wait for 30 seconds before failing
-bin/plugin --install mobz/elasticsearch-head --timeout 30s
+bin/plugin install mobz/elasticsearch-head --timeout 30s
 
 # Wait for 1 minute before failing
-bin/plugin --install mobz/elasticsearch-head --timeout 1m
+bin/plugin install mobz/elasticsearch-head --timeout 1m
 
 # Wait forever (default)
-bin/plugin --install mobz/elasticsearch-head --timeout 0
+bin/plugin install mobz/elasticsearch-head --timeout 0
 -----------------------------------
 
 [float]
 ==== Proxy settings
 
-To install a plugin via a proxy, you can pass the proxy details using the environment variables `proxyHost` and `proxyPort`.
-
-On Linux and Mac, here is an example of setting it:
 
-[source,sh]
------------------------------------
-bin/plugin -DproxyHost=host_name -DproxyPort=port_number --install mobz/elasticsearch-head
------------------------------------
-
-On Windows, here is an example of setting it:
+To install a plugin via a proxy, you can pass the proxy details using the environment variables `proxyHost` and `proxyPort`.
 
 [source,sh]
 -----------------------------------
 set JAVA_OPTS="-DproxyHost=host_name -DproxyPort=port_number"
-bin/plugin --install mobz/elasticsearch-head
+bin/plugin install mobz/elasticsearch-head
 -----------------------------------
 
 [float]

+ 1 - 1
plugins/analysis-icu/README.md

@@ -29,7 +29,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install analysis-icu \
+plugin install analysis-icu \
        --url file:target/releases/elasticsearch-analysis-icu-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/analysis-kuromoji/README.md

@@ -29,7 +29,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install analysis-kuromoji \
+plugin install analysis-kuromoji \
        --url file:target/releases/elasticsearch-analysis-kuromoji-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/analysis-phonetic/README.md

@@ -28,7 +28,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install analysis-phonetic \
+plugin install analysis-phonetic \
        --url file:target/releases/elasticsearch-analysis-phonetic-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/analysis-smartcn/README.md

@@ -28,7 +28,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install analysis-smartcn \
+plugin install analysis-smartcn \
        --url file:target/releases/elasticsearch-analysis-smartcn-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/analysis-stempel/README.md

@@ -27,7 +27,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install analysis-stempel \
+plugin install analysis-stempel \
        --url file:target/releases/elasticsearch-analysis-stempel-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/cloud-aws/README.md

@@ -28,7 +28,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install cloud-aws \ 
+plugin install cloud-aws \
        --url file:target/releases/elasticsearch-cloud-aws-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/cloud-azure/README.md

@@ -27,7 +27,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install cloud-azure \ 
+plugin install cloud-azure \
        --url file:target/releases/elasticsearch-cloud-azure-X.X.X-SNAPSHOT.zip
 ```
 

+ 2 - 2
plugins/cloud-gce/README.md

@@ -27,7 +27,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install cloud-gce \ 
+plugin install cloud-gce \
        --url file:target/releases/elasticsearch-cloud-gce-X.X.X-SNAPSHOT.zip
 ```
 
@@ -148,7 +148,7 @@ Install the plugin:
 
 ```sh
 # Use Plugin Manager to install it
-sudo /usr/share/elasticsearch/bin/plugin --install elasticsearch/elasticsearch-cloud-gce/2.2.0
+sudo /usr/share/elasticsearch/bin/plugin install elasticsearch/elasticsearch-cloud-gce/2.2.0
 
 # Configure it:
 sudo vi /etc/elasticsearch/elasticsearch.yml

+ 1 - 1
plugins/lang-javascript/README.md

@@ -27,7 +27,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install lang-javascript \
+plugin install lang-javascript \
        --url file:target/releases/elasticsearch-lang-javascript-X.X.X-SNAPSHOT.zip
 ```
 

+ 1 - 1
plugins/lang-python/README.md

@@ -27,7 +27,7 @@ To build a `SNAPSHOT` version, you need to build it with Maven:
 
 ```bash
 mvn clean install
-plugin --install lang-python \
+plugin install lang-python \
        --url file:target/releases/elasticsearch-lang-python-X.X.X-SNAPSHOT.zip
 ```