changeset 2542:80c3708bd5e4

Improve subcommand support Add Arguments.getSubcommand() method to allow plugin authors to more easily utilize subcommands in their Command implementations. Refactor existing Thermostat core plugins already using subcommands to take advantage of this new method. Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-November/021682.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-December/021803.html
author Andrew Azores <aazores@redhat.com>
date Thu, 17 Nov 2016 15:48:11 -0500
parents 9c6f95f61014
children 4254f69340ee
files common/core/src/main/java/com/redhat/thermostat/common/cli/Arguments.java common/core/src/main/java/com/redhat/thermostat/common/cli/InvalidSubcommandException.java common/core/src/main/java/com/redhat/thermostat/common/cli/LocaleResources.java common/core/src/main/java/com/redhat/thermostat/common/cli/SimpleArguments.java common/core/src/main/java/com/redhat/thermostat/common/cli/SubcommandExpectedException.java common/core/src/main/resources/com/redhat/thermostat/common/cli/locale/strings.properties integration-tests/itest-run/src/test/java/com/redhat/thermostat/itest/NotesCommandsTest.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandLineArguments.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandLineArgumentsParser.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommand.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommandTest.java setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/SetupCommand.java vm-byteman/client-cli/src/main/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommand.java vm-byteman/client-cli/src/main/java/com/redhat/thermostat/vm/byteman/client/cli/internal/LocaleResources.java vm-byteman/client-cli/src/main/resources/com/redhat/thermostat/vm/byteman/client/cli/internal/strings.properties vm-byteman/client-cli/src/test/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommandTest.java vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommand.java vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommandTest.java vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/LocaleResources.java vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommand.java vm-profiler/client-cli/src/main/resources/com/redhat/thermostat/vm/profiler/client/cli/internal/strings.properties vm-profiler/client-cli/src/test/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommandTest.java web/endpoint-plugin/web-service/src/test/java/com/redhat/thermostat/web/endpoint/internal/WebappLauncherCommandTest.java
diffstat 24 files changed, 291 insertions(+), 120 deletions(-) [+]
line wrap: on
line diff
--- a/common/core/src/main/java/com/redhat/thermostat/common/cli/Arguments.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/common/core/src/main/java/com/redhat/thermostat/common/cli/Arguments.java	Thu Nov 17 15:48:11 2016 -0500
@@ -40,11 +40,22 @@
 
 public interface Arguments {
     @Deprecated
-    static public final String HOST_ID_ARGUMENT = "hostId";
-    static public final String DB_URL_ARGUMENT = "dbUrl";
+    String HOST_ID_ARGUMENT = "hostId";
+    String DB_URL_ARGUMENT = "dbUrl";
 
     List<String> getNonOptionArguments();
     boolean hasArgument(String name);
     String getArgument(String name);
+
+    /**
+     * Get the selected subcommand
+     * Subcommands are taken to be the first of any non-option arguments passed to a Command.
+     * If any value is returned by this method, that value is guaranteed to be equal to one of the names given for
+     * a subcommand defined within the plugin's thermostat-plugin.xml.
+     * @return the selected subcommand
+     * @throws SubcommandExpectedException if no subcommand was supplied
+     * @throws InvalidSubcommandException if an unrecognized subcommand was supplied
+     */
+    String getSubcommand() throws SubcommandExpectedException, InvalidSubcommandException;
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/cli/InvalidSubcommandException.java	Thu Nov 17 15:48:11 2016 -0500
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.cli;
+
+import com.redhat.thermostat.shared.locale.Translate;
+
+public class InvalidSubcommandException extends CommandException {
+
+    private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
+
+    public InvalidSubcommandException(String subcommand) {
+        super(t.localize(LocaleResources.INVALID_SUBCOMMAND, subcommand));
+    }
+
+}
--- a/common/core/src/main/java/com/redhat/thermostat/common/cli/LocaleResources.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/common/core/src/main/java/com/redhat/thermostat/common/cli/LocaleResources.java	Thu Nov 17 15:48:11 2016 -0500
@@ -41,6 +41,8 @@
 public enum LocaleResources {
     MISSING_COMMAND_NAME,
     MISSING_REQUIRED_SERVICE,
+    SUBCOMMAND_EXPECTED,
+    INVALID_SUBCOMMAND,
     ;
 
     public static final String RESOURCE_BUNDLE =
--- a/common/core/src/main/java/com/redhat/thermostat/common/cli/SimpleArguments.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/common/core/src/main/java/com/redhat/thermostat/common/cli/SimpleArguments.java	Thu Nov 17 15:48:11 2016 -0500
@@ -47,6 +47,8 @@
 
     private List<String> nonOptionArguments = new ArrayList<>();
 
+    private List<String> subcommands = new ArrayList<>();
+
     @Override
     public boolean hasArgument(String name) {
         return arguments.containsKey(name);
@@ -70,5 +72,22 @@
     public void addNonOptionArgument(String arg) {
         nonOptionArguments.add(arg);
     }
+
+    public void addSubcommand(String subcommand) {
+        subcommands.add(subcommand);
+    }
+
+    @Override
+    public String getSubcommand() throws SubcommandExpectedException, InvalidSubcommandException {
+        List<String> nonOptionArgs = getNonOptionArguments();
+        if (nonOptionArgs.isEmpty()) {
+            throw new SubcommandExpectedException();
+        }
+        String subcommand = nonOptionArgs.get(0);
+        if (!subcommands.contains(subcommand)) {
+            throw new InvalidSubcommandException(subcommand);
+        }
+        return subcommand;
+    }
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/cli/SubcommandExpectedException.java	Thu Nov 17 15:48:11 2016 -0500
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.cli;
+
+import com.redhat.thermostat.shared.locale.Translate;
+
+public class SubcommandExpectedException extends CommandException {
+
+    private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
+
+    public SubcommandExpectedException() {
+        super(t.localize(LocaleResources.SUBCOMMAND_EXPECTED));
+    }
+
+}
--- a/common/core/src/main/resources/com/redhat/thermostat/common/cli/locale/strings.properties	Mon Dec 05 11:14:58 2016 -0500
+++ b/common/core/src/main/resources/com/redhat/thermostat/common/cli/locale/strings.properties	Thu Nov 17 15:48:11 2016 -0500
@@ -1,2 +1,4 @@
 MISSING_COMMAND_NAME=The implementation class {0} does not define an OSGi property for COMMAND_NAME, which is required.
-MISSING_REQUIRED_SERVICE=Required service {0} is unavailable
\ No newline at end of file
+MISSING_REQUIRED_SERVICE=Required service {0} is unavailable
+SUBCOMMAND_EXPECTED=A subcommand is expected
+INVALID_SUBCOMMAND=Invalid subcommand: {0}
\ No newline at end of file
--- a/integration-tests/itest-run/src/test/java/com/redhat/thermostat/itest/NotesCommandsTest.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/integration-tests/itest-run/src/test/java/com/redhat/thermostat/itest/NotesCommandsTest.java	Thu Nov 17 15:48:11 2016 -0500
@@ -222,7 +222,7 @@
 
         assertCommandIsFound(cmd);
         assertNoExceptions(cmd);
-        assertThat(cmd.getCurrentStandardErrContents(), containsString("The subcommand \"lsit\" is not recognized"));
+        assertThat(cmd.getCurrentStandardErrContents(), containsString("Invalid subcommand: lsit"));
     }
 
     @Test
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandLineArguments.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandLineArguments.java	Thu Nov 17 15:48:11 2016 -0500
@@ -36,8 +36,13 @@
 
 package com.redhat.thermostat.launcher.internal;
 
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
+import com.redhat.thermostat.common.cli.InvalidSubcommandException;
+import com.redhat.thermostat.common.cli.SubcommandExpectedException;
 import org.apache.commons.cli.CommandLine;
 
 import com.redhat.thermostat.common.cli.Arguments;
@@ -45,9 +50,11 @@
 class CommandLineArguments implements Arguments {
 
     private CommandLine cmdLine;
+    private Set<String> subcommands;
 
-    public CommandLineArguments(CommandLine commandLine) {
+    public CommandLineArguments(CommandLine commandLine, Collection<String> subcommands) {
         cmdLine = commandLine;
+        this.subcommands = new HashSet<>(subcommands);
     }
 
     @SuppressWarnings("unchecked")
@@ -66,5 +73,17 @@
         return cmdLine.getOptionValue(name);
     }
 
+    @Override
+    public String getSubcommand() throws SubcommandExpectedException, InvalidSubcommandException {
+        List<String> nonOptionArgs = getNonOptionArguments();
+        if (nonOptionArgs.isEmpty()) {
+            throw new SubcommandExpectedException();
+        }
+        String subcommand = nonOptionArgs.get(0);
+        if (!subcommands.contains(subcommand)) {
+            throw new InvalidSubcommandException(subcommand);
+        }
+        return subcommand;
+    }
 }
 
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandLineArgumentsParser.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandLineArgumentsParser.java	Thu Nov 17 15:48:11 2016 -0500
@@ -37,13 +37,14 @@
 package com.redhat.thermostat.launcher.internal;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.CommandLineParser;
 import org.apache.commons.cli.GnuParser;
-import org.apache.commons.cli.MissingArgumentException;
 import org.apache.commons.cli.MissingOptionException;
 import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
@@ -59,6 +60,7 @@
     private static Translate<LocaleResources> tr = LocaleResources.createLocalizer();
 
     private Options options = new Options();
+    private Set<String> subcommands = new HashSet<>();
 
     @SuppressWarnings("unchecked")
     void addOptions(Options options) {
@@ -67,12 +69,16 @@
         }
     }
 
+    void addSubcommands(Collection<String> subcommands) {
+        this.subcommands.addAll(subcommands);
+    }
+
     Arguments parse(String[] args) throws CommandLineArgumentParseException {
         try {
             CommandLineParser parser = new GnuParser();
             CommandLine commandLine;
             commandLine = parser.parse(options, args);
-            return new CommandLineArguments(commandLine);
+            return new CommandLineArguments(commandLine, subcommands);
         } catch (MissingOptionException moe) {
             LocalizedString msg = createMissingOptionsMessage(moe);
             throw new CommandLineArgumentParseException(msg, moe);
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Nov 17 15:48:11 2016 -0500
@@ -43,6 +43,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -367,8 +368,7 @@
             }
         }
         try {
-            Options options = mergeSubcommandOptionsWithParent(cmdInfo, cmdArgs);
-            Arguments args = parseCommandArguments(cmdArgs, options);
+            Arguments args = parseCommandArguments(cmdArgs, cmdInfo);
             setupLogLevel(args);
             CommandContext ctx = setupCommandContext(cmd, args);
             cmd.run(ctx);
@@ -388,6 +388,15 @@
     	out.println(message.getContents());
     }
 
+    private Arguments parseCommandArguments(String[] cmdArgs, CommandInfo commandInfo)
+            throws CommandLineArgumentParseException {
+        CommandLineArgumentsParser cliArgsParser = new CommandLineArgumentsParser();
+        cliArgsParser.addOptions(mergeSubcommandOptionsWithParent(commandInfo, cmdArgs));
+        cliArgsParser.addSubcommands(flattenSubcommandNames(commandInfo.getSubcommands()));
+
+        return cliArgsParser.parse(cmdArgs);
+    }
+
     // Note: this has the side-effect of adding subcommands' options to the parent command's Options.
     // An Options copy-constructor or Options.remove(Option) method could help us here. The problem with this side-effect
     // is that the subcommand options cannot be removed, only overridden again later, which prevents us from "resetting"
@@ -420,6 +429,9 @@
         return options;
     }
 
+    // Here we have to take a little bit of a guess about the selected subcommand, if any. We are in the process of
+    // setting up all of the required information to hand over to the CommandLineArgumentsParser, which is what returns
+    // the CommandLineArguments instance which really does know for sure which subcommand has been selected
     private PluginConfiguration.Subcommand getSelectedSubcommand(CommandInfo cmdInfo, String[] cmdArgs) {
         for (PluginConfiguration.Subcommand subcommand : cmdInfo.getSubcommands()) {
             for (String arg : cmdArgs) {
@@ -431,6 +443,14 @@
         return null;
     }
 
+    private List<String> flattenSubcommandNames(List<PluginConfiguration.Subcommand> subcommands) {
+        List<String> result = new ArrayList<>(subcommands.size());
+        for (PluginConfiguration.Subcommand subcommand : subcommands) {
+            result.add(subcommand.getName());
+        }
+        return result;
+    }
+
     private void setupLogLevel(Arguments args) {
         if (args.hasArgument(CommonOptions.LOG_LEVEL_ARG)) {
             String levelOption = args.getArgument(CommonOptions.LOG_LEVEL_ARG);
@@ -447,14 +467,6 @@
         }
     }
 
-    private Arguments parseCommandArguments(String[] cmdArgs, Options options)
-            throws CommandLineArgumentParseException {
-        CommandLineArgumentsParser cliArgsParser = new CommandLineArgumentsParser();
-        cliArgsParser.addOptions(options);
-        Arguments args = cliArgsParser.parse(cmdArgs);
-        return args;
-    }
-
     @SuppressWarnings("rawtypes")
     private CommandContext setupCommandContext(Command cmd, Arguments args) throws CommandException {
 
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommand.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommand.java	Thu Nov 17 15:48:11 2016 -0500
@@ -37,29 +37,23 @@
 package com.redhat.thermostat.notes.client.cli.internal;
 
 import com.redhat.thermostat.common.cli.AbstractCommand;
-import com.redhat.thermostat.common.cli.Arguments;
 import com.redhat.thermostat.common.cli.CliCommandOption;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.CompleterService;
 import com.redhat.thermostat.common.cli.CompletionFinderTabCompleter;
 import com.redhat.thermostat.common.cli.TabCompleter;
-import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
-import com.redhat.thermostat.shared.locale.Translate;
 
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-public class NotesControlCommand extends AbstractCommand implements CompleterService {
+class NotesControlCommand extends AbstractCommand implements CompleterService {
 
-    public static final String COMMAND_NAME = "notes";
+    static final String COMMAND_NAME = "notes";
     static final CliCommandOption NOTE_ID_OPTION = new CliCommandOption("n", "noteId", true, "Note ID", false);
 
-    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
-
     private NoteIdsFinder noteIdsFinder;
 
     private AddNoteSubcommand addNoteCommand;
@@ -67,9 +61,9 @@
     private UpdateNoteSubcommand updateNoteCommand;
     private ListNotesSubcommand listNotesCommand;
 
-    public NotesControlCommand(NoteIdsFinder noteIdsFinder, AddNoteSubcommand addNoteCommand,
-                               DeleteNoteSubcommand deleteNoteCommand, UpdateNoteSubcommand updateNoteCommand,
-                               ListNotesSubcommand listNotesCommand) {
+    NotesControlCommand(NoteIdsFinder noteIdsFinder, AddNoteSubcommand addNoteCommand,
+                        DeleteNoteSubcommand deleteNoteCommand, UpdateNoteSubcommand updateNoteCommand,
+                        ListNotesSubcommand listNotesCommand) {
         this.noteIdsFinder = noteIdsFinder;
         this.addNoteCommand = addNoteCommand;
         this.deleteNoteCommand = deleteNoteCommand;
@@ -100,13 +94,7 @@
 
     @Override
     public void run(CommandContext ctx) throws CommandException {
-        Arguments args = ctx.getArguments();
-        List<String> nonOptionArgs = args.getNonOptionArguments();
-        if (nonOptionArgs.isEmpty()) {
-            throw new CommandException(translator.localize(LocaleResources.SUBCOMMAND_EXPECTED));
-        }
-
-        String subcommand = nonOptionArgs.get(0);
+        String subcommand = ctx.getArguments().getSubcommand();
         switch (subcommand) {
             case AddNoteSubcommand.SUBCOMMAND_NAME:
                 addNoteCommand.run(ctx);
@@ -120,8 +108,6 @@
             case ListNotesSubcommand.SUBCOMMAND_NAME:
                 listNotesCommand.run(ctx);
                 break;
-            default:
-                throw new CommandException(translator.localize(LocaleResources.UNKNOWN_SUBCOMMAND, subcommand));
         }
     }
 
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommandTest.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommandTest.java	Thu Nov 17 15:48:11 2016 -0500
@@ -40,9 +40,12 @@
 import com.redhat.thermostat.common.cli.CliCommandOption;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.InvalidSubcommandException;
+import com.redhat.thermostat.common.cli.SubcommandExpectedException;
 import com.redhat.thermostat.common.cli.TabCompleter;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Matchers;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -128,28 +131,28 @@
         CommandContext ctx = mock(CommandContext.class);
         Arguments args = mock(Arguments.class);
         when(ctx.getArguments()).thenReturn(args);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(subcommandName));
+        when(args.getSubcommand()).thenReturn(subcommandName);
 
         notesControlCommand.run(ctx);
         verify(subcommand).run(ctx);
     }
 
-    @Test(expected = CommandException.class)
+    @Test(expected = InvalidSubcommandException.class)
     public void testUnknownSubcommand() throws CommandException {
         CommandContext ctx = mock(CommandContext.class);
         Arguments args = mock(Arguments.class);
         when(ctx.getArguments()).thenReturn(args);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList("fake-subcommand"));
+        when(args.getSubcommand()).thenThrow(InvalidSubcommandException.class);
 
         notesControlCommand.run(ctx);
     }
 
-    @Test(expected = CommandException.class)
+    @Test(expected = SubcommandExpectedException.class)
     public void testNoSubcommand() throws CommandException {
         CommandContext ctx = mock(CommandContext.class);
         Arguments args = mock(Arguments.class);
         when(ctx.getArguments()).thenReturn(args);
-        when(args.getNonOptionArguments()).thenReturn(Collections.<String>emptyList());
+        when(args.getSubcommand()).thenThrow(SubcommandExpectedException.class);
 
         notesControlCommand.run(ctx);
     }
--- a/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/SetupCommand.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/setup/command/src/main/java/com/redhat/thermostat/setup/command/internal/SetupCommand.java	Thu Nov 17 15:48:11 2016 -0500
@@ -50,12 +50,15 @@
 import java.util.logging.Logger;
 
 import com.redhat.thermostat.common.ExitStatus;
+import com.redhat.thermostat.common.NotImplementedException;
 import com.redhat.thermostat.common.cli.AbstractCommand;
 import com.redhat.thermostat.common.cli.Arguments;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.Console;
 import com.redhat.thermostat.common.cli.DependencyServices;
+import com.redhat.thermostat.common.cli.InvalidSubcommandException;
+import com.redhat.thermostat.common.cli.SubcommandExpectedException;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.internal.utils.laf.ThemeManager;
 import com.redhat.thermostat.launcher.Launcher;
@@ -319,6 +322,16 @@
             }
             return additionalOptions.get(name);
         }
+
+        @Override
+        public String getSubcommand() throws SubcommandExpectedException, InvalidSubcommandException {
+            // This is only expected to be called by actual Command implementations, not during setup interception.
+            // MergedSetupArguments is discarded and the original arguments String array passed through to the
+            // Launcher, which will recreate a new Arguments object with the correct implementation and pass that
+            // to the Command implementation.
+            // See http://icedtea.classpath.org/pipermail/thermostat/2016-December/021803.html
+            throw new NotImplementedException();
+        }
         
         @Override
         public String toString() {
--- a/vm-byteman/client-cli/src/main/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommand.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-byteman/client-cli/src/main/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommand.java	Thu Nov 17 15:48:11 2016 -0500
@@ -111,7 +111,7 @@
 
     static final CliCommandOption RULES_OPTION = new CliCommandOption("r", "rules", true,
             "a file with Byteman rules to load into a VM", false);
-    
+
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
     static final String INJECT_RULE_ACTION = "load";
     static final String UNLOAD_RULE_ACTION = "unload";
@@ -124,7 +124,7 @@
     private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");
     private static final String[] DIVIDER = {"-", "-", "-", "-"};
 
-    
+
     private final DependencyServices depServices = new DependencyServices();
 
     private BorderedTableRenderer table;
@@ -160,14 +160,9 @@
 
         InetSocketAddress target = agentInfo.getRequestQueueAddress();
 
-        List<String> nonOptionargs = ctx.getArguments().getNonOptionArguments();
-        if (nonOptionargs.size() != 1) {
-            throw new CommandException(translator.localize(LocaleResources.COMMAND_EXPECTED));
-        }
         VmBytemanDAO bytemanDao = depServices.getRequiredService(VmBytemanDAO.class);
 
-        String command = nonOptionargs.get(0);
-
+        String command = ctx.getArguments().getSubcommand();
         switch (command) {
         case INJECT_RULE_ACTION:
             injectRules(target, vmInfo, ctx, bytemanDao);
@@ -186,8 +181,6 @@
             }
             showMetrics(ctx, vmId, agentId, bytemanDao, nameQuery);
             break;
-        default:
-            throw new CommandException(translator.localize(LocaleResources.UNKNOWN_COMMAND, command));
         }
     }
     
--- a/vm-byteman/client-cli/src/main/java/com/redhat/thermostat/vm/byteman/client/cli/internal/LocaleResources.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-byteman/client-cli/src/main/java/com/redhat/thermostat/vm/byteman/client/cli/internal/LocaleResources.java	Thu Nov 17 15:48:11 2016 -0500
@@ -43,8 +43,6 @@
     VM_SERVICE_UNAVAILABLE,
     AGENT_NOT_FOUND,
     AGENT_DEAD,
-    COMMAND_EXPECTED,
-    UNKNOWN_COMMAND,
     NO_RULE_OPTION,
     RULE_FILE_NOT_FOUND,
     ERROR_READING_RULE_FILE,
--- a/vm-byteman/client-cli/src/main/resources/com/redhat/thermostat/vm/byteman/client/cli/internal/strings.properties	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-byteman/client-cli/src/main/resources/com/redhat/thermostat/vm/byteman/client/cli/internal/strings.properties	Thu Nov 17 15:48:11 2016 -0500
@@ -1,6 +1,4 @@
 VM_SERVICE_UNAVAILABLE = VM information is not available
-COMMAND_EXPECTED = A valid subcommand is expected.
-UNKNOWN_COMMAND = Unknown command: {0}
 AGENT_NOT_FOUND = Agent with id {0} not found.
 AGENT_DEAD = Agent with id {0} is not alive.
 NO_RULE_OPTION = No rule option specified.
--- a/vm-byteman/client-cli/src/test/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommandTest.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-byteman/client-cli/src/test/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommandTest.java	Thu Nov 17 15:48:11 2016 -0500
@@ -46,6 +46,7 @@
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.matchers.JUnitMatchers.containsString;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
@@ -65,6 +66,8 @@
 import java.util.TimeZone;
 import java.util.concurrent.CountDownLatch;
 
+import com.redhat.thermostat.common.cli.InvalidSubcommandException;
+import com.redhat.thermostat.common.cli.SubcommandExpectedException;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -95,6 +98,8 @@
 import com.redhat.thermostat.vm.byteman.common.command.BytemanRequest;
 import com.redhat.thermostat.vm.byteman.common.command.BytemanRequest.RequestAction;
 import com.redhat.thermostat.vm.byteman.common.command.BytemanRequestResponseListener;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 public class BytemanControlCommandTest {
 
@@ -154,15 +159,15 @@
     @Test
     public void testUnknownAction() {
         String unknownAction = "some-action-that-doesn't-exist";
-        Arguments args = getBasicArgsWithAction(unknownAction);
-        CommandContext ctx = ctxFactory.createContext(args);
         try {
+            Arguments args = getBasicArgsWithAction(unknownAction);
+            CommandContext ctx = ctxFactory.createContext(args);
             command.run(ctx);
             fail("Expected failure due to unknown action");
         } catch (CommandException e) {
             String msg = e.getMessage();
-            assertTrue(msg.contains(unknownAction));
-            assertTrue(msg.startsWith("Unknown command:"));
+            assertThat(msg, containsString(unknownAction));
+            assertTrue(msg.startsWith("Invalid subcommand:"));
         }
     }
     
@@ -480,10 +485,21 @@
         assertEquals(stdOutExpected, stdOut);
     }
 
-    private Arguments getBasicArgsWithAction(String action) {
-        Arguments args = mock(Arguments.class);
+    private Arguments getBasicArgsWithAction(final String action) throws SubcommandExpectedException, InvalidSubcommandException {
+        final Arguments args = mock(Arguments.class);
         when(args.getArgument("vmId")).thenReturn(SOME_VM_ID);
-        when(args.getNonOptionArguments()).thenReturn(Arrays.asList(action));
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(action));
+        when(args.getSubcommand()).thenAnswer(new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocationOnMock) throws Throwable {
+                List<String> options = Arrays.asList(BytemanControlCommand.SHOW_ACTION, BytemanControlCommand.STATUS_ACTION,
+                        BytemanControlCommand.UNLOAD_RULE_ACTION, BytemanControlCommand.INJECT_RULE_ACTION);
+                if (!options.contains(action)) {
+                    throw new InvalidSubcommandException(action);
+                }
+                return action;
+            }
+        });
         return args;
     }
 }
--- a/vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommand.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommand.java	Thu Nov 17 15:48:11 2016 -0500
@@ -98,11 +98,7 @@
     @Override
     public void run(CommandContext ctx) throws CommandException {
         Arguments args = ctx.getArguments();
-
-        if (args.getNonOptionArguments().size() != 1) {
-            throw new CommandException(t.localize(LocaleResources.EXPECTED_ONE_NONOPTION_ARG));
-        }
-        String subcommand = args.getNonOptionArguments().get(0);
+        String subcommand = args.getSubcommand();
 
         ApplicationService applicationService = dependencyServices.getRequiredService(ApplicationService.class);
         Clock clock = dependencyServices.getRequiredService(Clock.class);
@@ -144,8 +140,6 @@
             case SHOW_SUBCOMMAND:
                 printNotifications(ctx, jmxNotificationDAO, vmRef, parseSinceOption(args));
                 break;
-            default:
-                throw new CommandException(t.localize(LocaleResources.UNRECOGNIZED_SUBCOMMAND, subcommand));
         }
     }
 
--- a/vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommandTest.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommandTest.java	Thu Nov 17 15:48:11 2016 -0500
@@ -44,6 +44,8 @@
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.Console;
+import com.redhat.thermostat.common.cli.InvalidSubcommandException;
+import com.redhat.thermostat.common.cli.SubcommandExpectedException;
 import com.redhat.thermostat.common.command.Request;
 import com.redhat.thermostat.common.command.RequestResponseListener;
 import com.redhat.thermostat.common.command.Response;
@@ -245,61 +247,61 @@
 
     @Test
     public void testStatusWhenMonitoringIsEnabled() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_ENABLED));
     }
 
     @Test
     public void testStatusWhenMonitoringIsDisabled() throws CommandException {
         jmxNotificationStatus.setEnabled(false);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_DISABLED));
     }
 
     @Test
     public void testStatusWhenMonitoringStatusIsUnknown() throws CommandException {
         when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(null);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_DISABLED));
     }
 
     @Test
     public void testDisableWhenMonitoringIsEnabled() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(DISABLE_SUBCOMMAND));
+        addSubcommandToArgs(DISABLE_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_DISABLED));
     }
 
     @Test
     public void testDisableWhenMonitoringIsDisabled() throws CommandException {
         jmxNotificationStatus.setEnabled(false);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(DISABLE_SUBCOMMAND));
+        addSubcommandToArgs(DISABLE_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_NOT_ENABLED));
     }
 
     @Test
     public void testDisableWhenMonitoringStatusIsUnknown() throws CommandException {
         when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(null);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(DISABLE_SUBCOMMAND));
+        addSubcommandToArgs(DISABLE_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_NOT_ENABLED));
     }
 
     @Test
     public void testEnableWhenMonitoringIsEnabled() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        addSubcommandToArgs(ENABLE_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_ALREADY_ENABLED));
     }
 
     @Test
     public void testEnableWhenMonitoringIsDisabled() throws CommandException {
         jmxNotificationStatus.setEnabled(false);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        addSubcommandToArgs(ENABLE_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_ENABLED));
     }
 
     @Test
     public void testEnableWhenMonitoringStatusIsUnknown() throws CommandException {
         when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(null);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        addSubcommandToArgs(ENABLE_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_ENABLED));
     }
 
@@ -307,7 +309,7 @@
     public void testEnableWithFollowOption() throws CommandException, IOException {
         jmxNotificationStatus.setEnabled(false);
         jmxNotification.setTimeStamp(FUTURE_TIMESTAMP);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        addSubcommandToArgs(ENABLE_SUBCOMMAND);
         when(args.hasArgument(FOLLOW_OPTION)).thenReturn(true);
 
         doFollowTestWithKeyboardInterrupt();
@@ -324,7 +326,7 @@
     @Test
     public void testEnableWithFollowOptionWhenAlreadyEnabled() throws CommandException, IOException {
         jmxNotification.setTimeStamp(FUTURE_TIMESTAMP);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        addSubcommandToArgs(ENABLE_SUBCOMMAND);
         when(args.hasArgument(FOLLOW_OPTION)).thenReturn(true);
 
         doFollowTestWithKeyboardInterrupt();
@@ -342,7 +344,7 @@
     public void testEnableWithFollowOptionWithExternalInterrupt() throws CommandException, IOException {
         jmxNotificationStatus.setEnabled(false);
         jmxNotification.setTimeStamp(FUTURE_TIMESTAMP);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        addSubcommandToArgs(ENABLE_SUBCOMMAND);
         when(args.hasArgument(FOLLOW_OPTION)).thenReturn(true);
 
         doFollowTestWithExternalMonitoringInterrupt();
@@ -362,7 +364,7 @@
     public void testFollowWhenDisabled() throws CommandException {
         jmxNotificationStatus.setEnabled(false);
         jmxNotification.setTimeStamp(FUTURE_TIMESTAMP);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(FOLLOW_SUBCOMMAND));
+        addSubcommandToArgs(FOLLOW_SUBCOMMAND);
 
         assertThat(runCommandForOutput(), is("JMX notification monitoring is not enabled for this JVM - notifications cannot be followed"));
     }
@@ -370,7 +372,7 @@
     @Test
     public void testFollowWhenEnabled() throws CommandException, IOException {
         jmxNotification.setTimeStamp(FUTURE_TIMESTAMP);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(FOLLOW_SUBCOMMAND));
+        addSubcommandToArgs(FOLLOW_SUBCOMMAND);
 
         doFollowTestWithKeyboardInterrupt();
 
@@ -384,7 +386,7 @@
     @Test
     public void testFollowWithExternalInterrupt() throws CommandException, IOException {
         jmxNotification.setTimeStamp(FUTURE_TIMESTAMP);
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(FOLLOW_SUBCOMMAND));
+        addSubcommandToArgs(FOLLOW_SUBCOMMAND);
 
         doFollowTestWithExternalMonitoringInterrupt();
 
@@ -473,13 +475,13 @@
 
     @Test
     public void testShow() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(SHOW_SUBCOMMAND));
+        addSubcommandToArgs(SHOW_SUBCOMMAND);
         assertThat(runCommandForOutput(), is(NOTIFICATION_OUTPUT));
     }
 
     @Test
     public void testShowSinceWithTimestampNewerThanData() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(SHOW_SUBCOMMAND));
+        addSubcommandToArgs(SHOW_SUBCOMMAND);
         when(args.hasArgument(SINCE_OPTION)).thenReturn(true);
         when(args.getArgument(SINCE_OPTION)).thenReturn("500");
         cmd.run(ctx);
@@ -488,7 +490,7 @@
 
     @Test
     public void testShowSinceWithTimestampOlderThanData() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(SHOW_SUBCOMMAND));
+        addSubcommandToArgs(SHOW_SUBCOMMAND);
         when(args.hasArgument(SINCE_OPTION)).thenReturn(true);
         when(args.getArgument(SINCE_OPTION)).thenReturn("1");
         assertThat(runCommandForOutput(), is(NOTIFICATION_OUTPUT));
@@ -502,9 +504,8 @@
 
     @Test(expected = CommandException.class)
     public void testRequiresRequestQueue() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         cmd.dependenciesUnavailable();
-//        cmd.bindRequestQueue(requestQueue);
         cmd.bindHostInfoDao(hostInfoDAO);
         cmd.bindAgentInfoDao(agentInfoDAO);
         cmd.bindVmInfoDao(vmInfoDAO);
@@ -514,10 +515,9 @@
 
     @Test(expected = CommandException.class)
     public void testRequiresHostInfoDao() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         cmd.dependenciesUnavailable();
         cmd.bindRequestQueue(requestQueue);
-//        cmd.bindHostInfoDao(hostInfoDAO);
         cmd.bindAgentInfoDao(agentInfoDAO);
         cmd.bindVmInfoDao(vmInfoDAO);
         cmd.bindJmxNotificationDao(jmxNotificationDAO);
@@ -526,11 +526,10 @@
 
     @Test(expected = CommandException.class)
     public void testRequiresAgentInfoDao() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         cmd.dependenciesUnavailable();
         cmd.bindRequestQueue(requestQueue);
         cmd.bindHostInfoDao(hostInfoDAO);
-//        cmd.bindAgentInfoDao(agentInfoDAO);
         cmd.bindVmInfoDao(vmInfoDAO);
         cmd.bindJmxNotificationDao(jmxNotificationDAO);
         cmd.run(ctx);
@@ -538,26 +537,28 @@
 
     @Test(expected = CommandException.class)
     public void testRequiresVmInfoDao() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         cmd.dependenciesUnavailable();
         cmd.bindRequestQueue(requestQueue);
         cmd.bindHostInfoDao(hostInfoDAO);
         cmd.bindAgentInfoDao(agentInfoDAO);
-//        cmd.bindVmInfoDao(vmInfoDAO);
         cmd.bindJmxNotificationDao(jmxNotificationDAO);
         cmd.run(ctx);
     }
 
     @Test(expected = CommandException.class)
     public void testRequiresJmxNotificationDao() throws CommandException {
-        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        addSubcommandToArgs(STATUS_SUBCOMMAND);
         cmd.dependenciesUnavailable();
         cmd.bindRequestQueue(requestQueue);
         cmd.bindHostInfoDao(hostInfoDAO);
         cmd.bindAgentInfoDao(agentInfoDAO);
         cmd.bindVmInfoDao(vmInfoDAO);
-//        cmd.bindJmxNotificationDao(jmxNotificationDAO);
         cmd.run(ctx);
     }
 
+    private void addSubcommandToArgs(String subcommand) throws SubcommandExpectedException, InvalidSubcommandException {
+        when(args.getSubcommand()).thenReturn(subcommand);
+    }
+
 }
--- a/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/LocaleResources.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/LocaleResources.java	Thu Nov 17 15:48:11 2016 -0500
@@ -40,8 +40,6 @@
 
 public enum LocaleResources {
 
-    COMMAND_EXPECTED,
-    UNKNOWN_COMMAND,
     INTERRUPTED_WAITING_FOR_RESPONSE,
     AGENT_NOT_FOUND,
 
--- a/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommand.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommand.java	Thu Nov 17 15:48:11 2016 -0500
@@ -73,10 +73,10 @@
 
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
-    private static final String START_ARGUMENT = "start";
-    private static final String STOP_ARGUMENT = "stop";
-    private static final String STATUS_ARGUMENT = "status";
-    private static final String SHOW_ARGUMENT = "show";
+    static final String START_ARGUMENT = "start";
+    static final String STOP_ARGUMENT = "stop";
+    static final String STATUS_ARGUMENT = "status";
+    static final String SHOW_ARGUMENT = "show";
 
     private final DependencyServices myServices = new DependencyServices();
 
@@ -100,13 +100,7 @@
 
         InetSocketAddress target = agentInfo.getRequestQueueAddress();
 
-        List<String> arguments = ctx.getArguments().getNonOptionArguments();
-        if (arguments.size() != 1) {
-            throw new CommandException(translator.localize(LocaleResources.COMMAND_EXPECTED));
-        }
-
-        String command = arguments.get(0);
-
+        String command = ctx.getArguments().getSubcommand();
         switch (command) {
         case START_ARGUMENT:
             sendStartProfilingRequest(ctx.getConsole(), requestQueue, target, vmId.get());
@@ -120,8 +114,6 @@
         case SHOW_ARGUMENT:
             showProfilingResults(ctx.getConsole(), agentId, vmId);
             break;
-        default:
-            throw new CommandException(translator.localize(LocaleResources.UNKNOWN_COMMAND, command));
         }
     }
 
--- a/vm-profiler/client-cli/src/main/resources/com/redhat/thermostat/vm/profiler/client/cli/internal/strings.properties	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-profiler/client-cli/src/main/resources/com/redhat/thermostat/vm/profiler/client/cli/internal/strings.properties	Thu Nov 17 15:48:11 2016 -0500
@@ -1,5 +1,3 @@
-COMMAND_EXPECTED = A valid subcommand is expected.
-UNKNOWN_COMMAND = Unknown command: {0}
 INTERRUPTED_WAITING_FOR_RESPONSE = Interrupted while waiting for a response from agent
 AGENT_NOT_FOUND = error: agent {0} not found
 
--- a/vm-profiler/client-cli/src/test/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommandTest.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/vm-profiler/client-cli/src/test/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommandTest.java	Thu Nov 17 15:48:11 2016 -0500
@@ -47,14 +47,12 @@
 import org.junit.Test;
 
 import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.client.cli.internal.LocaleResources;
 import com.redhat.thermostat.client.command.RequestQueue;
 import com.redhat.thermostat.common.cli.Arguments;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.SimpleArguments;
 import com.redhat.thermostat.common.internal.test.TestCommandContextFactory;
-import com.redhat.thermostat.shared.locale.Translate;
 import com.redhat.thermostat.storage.core.AgentId;
 import com.redhat.thermostat.storage.core.VmId;
 import com.redhat.thermostat.storage.dao.AgentInfoDAO;
@@ -66,8 +64,6 @@
 
 public class ProfileVmCommandTest {
 
-    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
-
     private static final String AGENT_ID = "some-agent";
     private static final String VM_ID = "some-vm";
     private static final long SOME_TIMESTAMP = 99;
@@ -123,6 +119,7 @@
         SimpleArguments args = new SimpleArguments();
         args.addArgument(VmArgument.ARGUMENT_NAME, VM_ID);
         args.addNonOptionArgument("status");
+        addSubcommandsToArguments(args);
         CommandContext ctx = cmdCtxFactory.createContext(args);
 
         cmd.run(ctx);
@@ -141,6 +138,7 @@
         SimpleArguments args = new SimpleArguments();
         args.addArgument(VmArgument.ARGUMENT_NAME, VM_ID);
         args.addNonOptionArgument("status");
+        addSubcommandsToArguments(args);
         CommandContext ctx = cmdCtxFactory.createContext(args);
 
         cmd.run(ctx);
@@ -156,6 +154,7 @@
         SimpleArguments args = new SimpleArguments();
         args.addArgument(VmArgument.ARGUMENT_NAME, VM_ID);
         args.addNonOptionArgument("show");
+        addSubcommandsToArguments(args);
         CommandContext ctx = cmdCtxFactory.createContext(args);
 
         cmd.run(ctx);
@@ -175,6 +174,7 @@
         SimpleArguments args = new SimpleArguments();
         args.addArgument(VmArgument.ARGUMENT_NAME, VM_ID);
         args.addNonOptionArgument("show");
+        addSubcommandsToArguments(args);
         CommandContext ctx = cmdCtxFactory.createContext(args);
 
         cmd.run(ctx);
@@ -183,4 +183,11 @@
                      "75.000000 3         int bar(int)\n" +
                      "25.000000 1         void foo()\n", cmdCtxFactory.getOutput());
     }
+
+    private void addSubcommandsToArguments(SimpleArguments args) {
+        args.addSubcommand(ProfileVmCommand.START_ARGUMENT);
+        args.addSubcommand(ProfileVmCommand.STOP_ARGUMENT);
+        args.addSubcommand(ProfileVmCommand.STATUS_ARGUMENT);
+        args.addSubcommand(ProfileVmCommand.SHOW_ARGUMENT);
+    }
 }
--- a/web/endpoint-plugin/web-service/src/test/java/com/redhat/thermostat/web/endpoint/internal/WebappLauncherCommandTest.java	Mon Dec 05 11:14:58 2016 -0500
+++ b/web/endpoint-plugin/web-service/src/test/java/com/redhat/thermostat/web/endpoint/internal/WebappLauncherCommandTest.java	Thu Nov 17 15:48:11 2016 -0500
@@ -67,6 +67,7 @@
 
 import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.common.cli.Console;
+import com.redhat.thermostat.testutils.NotImplementedException;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -237,7 +238,11 @@
             public String getArgument(String name) {
                 return null;
             }
-            
+
+            @Override
+            public String getSubcommand() {
+                throw new NotImplementedException();
+            }
         });
         setupLogger(matchString);
         assertFalse(handler.gotLogMessage);