CLI: Help polish & add short, relevant help for usage/argument errors (#41316)

David Bennett committed Aug 14, 2026 at 15:28 UTC 0ec2988fcbd4a780e59f5903d4af5000378ac126
19 files changed +465 -140
localization/strings/en-US/Resources.resw
+37 -23
@@ -2204,11 +2204,15 @@ Usage:
2204 <value>Required argument not provided: '{}'</value>
2205 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2206 </data>
2207 + <data name="WSLCCLI_RequiredArgumentOptionError" xml:space="preserve">
2208 + <value>Required option not provided: '{}'</value>
2209 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2210 + </data>
2211 <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2212 <value>Shows help about the selected command</value>
2213 </data>
2214 <data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
2211 - <value>Multiple mutually exclusive arguments provided: '{}'</value>
2215 + <value>Multiple mutually exclusive options provided: '{}'</value>
2216 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2217 </data>
2218 <data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
@@ -2244,7 +2248,11 @@ Usage:
2248 <value>Disable color output.</value>
2249 </data>
2250 <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2247 - <value>For more details on a specific command, pass it the help argument.</value>
2251 + <value>For more details on a specific command, pass it the help option.</value>
2252 + </data>
2253 + <data name="WSLCCLI_RunHelpForMoreInformation" xml:space="preserve">
2254 + <value>Run '{} --help' for more information.</value>
2255 + <comment>{FixedPlaceholder="{}"}{Locked="--help'"}Command line arguments, file names and string inserts should not be translated</comment>
2256 </data>
2257 <data name="WSLCCLI_HeadingCommands" xml:space="preserve">
2258 <value>Commands:</value>
@@ -2261,8 +2269,14 @@ Usage:
2269 <data name="WSLCCLI_HeadingArguments" xml:space="preserve">
2270 <value>Arguments:</value>
2271 </data>
2272 + <data name="WSLCCLI_HeadingRelatedOptions" xml:space="preserve">
2273 + <value>Related options:</value>
2274 + </data>
2275 + <data name="WSLCCLI_HeadingRelatedArguments" xml:space="preserve">
2276 + <value>Related arguments:</value>
2277 + </data>
2278 <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2265 - <value>Argument name was not recognized for the current command: '{}'</value>
2279 + <value>Option name was not recognized for the current command: '{}'</value>
2280 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2281 </data>
2282 <data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
@@ -2284,35 +2298,35 @@ Usage:
2298 <value>Container ID</value>
2299 </data>
2300 <data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
2287 - <value>Missing argument value: '{}'</value>
2301 + <value>Missing option value: '{}'</value>
2302 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2303 </data>
2304 <data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
2291 - <value>Argument alias was not recognized for the current command: '{}'</value>
2305 + <value>Option alias was not recognized for the current command: '{}'</value>
2306 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2307 </data>
2308 <data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
2295 - <value>Invalid argument specifier: '{}'</value>
2309 + <value>Invalid option specifier: '{}'</value>
2310 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2311 </data>
2312 <data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
2299 - <value>Adjoined flag alias not found: '{}'</value>
2313 + <value>Adjoined option alias not found: '{}'</value>
2314 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2315 </data>
2316 <data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
2303 - <value>Adjoined alias is not a flag: '{}'</value>
2317 + <value>Adjoined option alias is not a flag: '{}'</value>
2318 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2319 </data>
2320 <data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
2307 - <value>Invalid argument specifier: '{}'</value>
2321 + <value>Invalid option specifier: '{}'</value>
2322 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2323 </data>
2324 <data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
2311 - <value>Flag argument cannot contain adjoined value: '{}'</value>
2325 + <value>Option cannot contain an adjoined value: '{}'</value>
2326 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2327 </data>
2328 <data name="WSLCCLI_FlagInvalidBooleanError" xml:space="preserve">
2315 - <value>Invalid boolean value for flag argument: '{}'. Expected true, false, 1, or 0.</value>
2329 + <value>Invalid boolean value for option: '{}'. Expected true, false, 1, 0, t, or f.</value>
2330 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2331 </data>
2332 <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
@@ -2320,7 +2334,7 @@ Usage:
2334 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2335 </data>
2336 <data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
2323 - <value>Missing argument name at: '{}'</value>
2337 + <value>Missing option name at: '{}'</value>
2338 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2339 </data>
2340 <data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
@@ -2332,7 +2346,7 @@ Usage:
2346 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2347 </data>
2348 <data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
2335 - <value>Alias arguments with a value must be last in the alias chain: '{}'</value>
2349 + <value>Alias options with a value must be last in the alias chain: '{}'</value>
2350 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2351 </data>
2352 <data name="WSLCCLI_CopyrightHeader" xml:space="preserve">
@@ -2475,7 +2489,7 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2489 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2490 </data>
2491 <data name="MessageWslcAliasAmbiguousWithMultipleNetworks" xml:space="preserve">
2478 - <value>Network aliases cannot be specified when multiple networks are requested. Use a single --network argument.</value>
2492 + <value>Network aliases cannot be specified when multiple networks are requested. Use a single --network option.</value>
2493 <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2494 </data>
2495 <data name = "MessageWslcFailedToMountVolume" xml:space = "preserve" >
@@ -2961,7 +2975,7 @@ On first run, creates the file with all settings commented out at their defaults
2975 <comment>{Locked="0.5"}{Locked="1"}{Locked="2.5"}Command line argument example values should not be translated</comment>
2976 </data>
2977 <data name="WSLCCLI_InvalidCpusError" xml:space="preserve">
2964 - <value>Invalid {} argument value: '{}'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)</value>
2978 + <value>Invalid {} option value: '{}'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)</value>
2979 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="0.5"}{Locked="1"}{Locked="2"}</comment>
2980 </data>
2981 <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
@@ -2989,7 +3003,7 @@ On first run, creates the file with all settings commented out at their defaults
3003 <value>Filter output based on conditions provided</value>
3004 </data>
3005 <data name="WSLCCLI_InvalidFilterError" xml:space="preserve">
2992 - <value>Invalid argument "{}" for '-f, --filter' flag: bad format of filter (expected name=value)</value>
3006 + <value>Invalid value "{}" for the '-f, --filter' option: bad format of filter (expected name=value)</value>
3007 <comment>{FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated</comment> </data>
3008 <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
3009 <value>Follow log output</value>
@@ -3268,7 +3282,7 @@ On first run, creates the file with all settings commented out at their defaults
3282 <comment>{Locked="-1"}{Locked="&lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;]"}Command line arguments should not be translated</comment>
3283 </data>
3284 <data name="WSLCCLI_InvalidUlimitError" xml:space="preserve">
3271 - <value>Invalid {} argument value: '{}'. Expected &lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;] (use -1 for unlimited)</value>
3285 + <value>Invalid {} option value: '{}'. Expected &lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;] (use -1 for unlimited)</value>
3286 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="-1"}{Locked="&lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;]"}</comment>
3287 </data>
3288 <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
@@ -3305,11 +3319,11 @@ On first run, creates the file with all settings commented out at their defaults
3319 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3320 </data>
3321 <data name="WSLCCLI_InvalidIntegerArgumentError" xml:space="preserve">
3308 - <value>Invalid {} argument value: {}</value>
3322 + <value>Invalid {} option value: {}</value>
3323 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3324 </data>
3325 <data name="WSLCCLI_InvalidTimestampArgumentError" xml:space="preserve">
3312 - <value>Invalid {} argument value: '{}'. Expected a Unix epoch timestamp or an RFC3339 date-time (e.g. 2024-01-15T10:30:00Z)</value>
3326 + <value>Invalid {} option value: '{}'. Expected a Unix epoch timestamp or an RFC3339 date-time (e.g. 2024-01-15T10:30:00Z)</value>
3327 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="2024-01-15T10:30:00Z"}{Locked="RFC3339"}</comment>
3328 </data>
3329 <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
@@ -3317,11 +3331,11 @@ On first run, creates the file with all settings commented out at their defaults
3331 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3332 </data>
3333 <data name="WSLCCLI_InvalidMemorySizeError" xml:space="preserve">
3320 - <value>Invalid {} argument value: '{}'. Expected a memory size (e.g. 256M, 1G)</value>
3334 + <value>Invalid {} option value: '{}'. Expected a memory size (e.g. 256M, 1G)</value>
3335 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="256M"}{Locked="1G"}</comment>
3336 </data>
3337 <data name="WSLCCLI_InvalidDurationError" xml:space="preserve">
3324 - <value>Invalid {} argument value: '{}'. Expected a duration (e.g. 30s, 1m30s)</value>
3338 + <value>Invalid {} option value: '{}'. Expected a duration (e.g. 30s, 1m30s)</value>
3339 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="30s"}{Locked="1m30s"}</comment>
3340 </data>
3341 <data name="WSLCCLI_NoHealthcheckConflictError" xml:space="preserve">
@@ -3498,10 +3512,10 @@ On first run, creates the file with all settings commented out at their defaults
3512 <value>Outputs the volume names only</value>
3513 </data>
3514 <data name="WSLCCLI_ImageSaveStdoutIsTerminalError" xml:space="preserve">
3501 - <value>Cannot write image to terminal. Use the -o flag or redirect stdout.</value>
3515 + <value>Cannot write image to terminal. Use the -o option or redirect stdout.</value>
3516 </data>
3517 <data name="WSLCCLI_ContainerExportStdoutIsTerminalError" xml:space="preserve">
3504 - <value>Cannot export container to terminal. Use the -o flag or redirect stdout.</value>
3518 + <value>Cannot export container to terminal. Use the -o option or redirect stdout.</value>
3519 </data>
3520 <data name="WSLCCLI_VolumeFormatUsage" xml:space="preserve">
3521 <value>Expected format: &lt;host path | named volume&gt;:&lt;container path&gt;[:mode]</value>
src/windows/wslc/arguments/Argument.h
+4
@@ -88,6 +88,10 @@ struct Argument
88 {
89 return m_type;
90 }
91 + bool IsOption() const
92 + {
93 + return m_type == argument::Kind::Flag || m_type == argument::Kind::Value;
94 + }
95 Limit Limit() const
96 {
97 return m_limit;
src/windows/wslc/arguments/ArgumentParser.cpp
+8 -4
@@ -75,7 +75,9 @@ void ParseArgumentsStateMachine::ThrowIfError() const
75 // If the next argument was to be a value, but none was provided, convert it to an exception.
76 else if (m_state.Type() && m_invocationItr == m_invocation.end())
77 {
78 - throw ArgumentException(Localization::WSLCCLI_MissingArgumentError(m_state.Arg()));
78 + const auto* argument = FindArgument(m_state.Type().value());
79 + const auto message = Localization::WSLCCLI_MissingArgumentError(m_state.Arg());
80 + throw argument != nullptr ? ArgumentException(message, *argument) : ArgumentException(message);
81 }
82 }
83
@@ -158,7 +160,9 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ApplyFlagValue(Arg
160 const auto boolVal = string::ParseBool(std::wstring(unquoted).c_str(), /*AllowExtendedForms*/ true);
161 if (!boolVal.has_value())
162 {
161 - return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
163 + const auto* argument = FindArgument(type);
164 + const auto message = Localization::WSLCCLI_FlagInvalidBooleanError(currArg);
165 + return argument != nullptr ? ArgumentException(message, *argument) : ArgumentException(message);
166 }
167
168 SetFlag(type, boolVal.value());
@@ -394,7 +398,7 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
398 if (currArg[currentPos] != WSLC_CLI_ARG_SPLIT_CHAR)
399 {
400 // There are more characters but it's not '=' - this is invalid
397 - return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg));
401 + return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg), *firstArg);
402 }
403
404 // Value is adjoined after '='
@@ -436,7 +440,7 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
440 if (currArg[nextPos] != WSLC_CLI_ARG_SPLIT_CHAR)
441 {
442 // There are more characters but it's not '=' - this is invalid
439 - return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg));
443 + return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg), *nextArg);
444 }
445
446 // Value is adjoined after '='
src/windows/wslc/arguments/ArgumentValidation.cpp
+12 -2
@@ -147,7 +147,17 @@ void Argument::Validate(ArgMap& execArgs) const
147 if (execArgs.Contains(ArgType::HealthCmd) || execArgs.Contains(ArgType::HealthInterval) || execArgs.Contains(ArgType::HealthTimeout) ||
148 execArgs.Contains(ArgType::HealthStartPeriod) || execArgs.Contains(ArgType::HealthRetries))
149 {
150 - throw ArgumentException(Localization::WSLCCLI_NoHealthcheckConflictError());
150 + std::vector<Argument> conflictingArguments{*this};
151 + for (const auto type :
152 + {ArgType::HealthCmd, ArgType::HealthInterval, ArgType::HealthTimeout, ArgType::HealthStartPeriod, ArgType::HealthRetries})
153 + {
154 + if (execArgs.Contains(type))
155 + {
156 + conflictingArguments.emplace_back(Argument::Create(type));
157 + }
158 + }
159 +
160 + throw ArgumentException(Localization::WSLCCLI_NoHealthcheckConflictError(), std::move(conflictingArguments));
161 }
162 break;
163
@@ -240,7 +250,7 @@ void Argument::Validate(ArgMap& execArgs) const
250 auto parsed = validation::ParseNetworkArgument(value, name);
251 if (IsEqual(parsed.Name, "host", true))
252 {
243 - throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
253 + throw ExecutionException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
254 }
255
256 return parsed;
src/windows/wslc/commands/ContainerListCommand.cpp
+3 -1
@@ -62,7 +62,9 @@ void ContainerListCommand::ValidateArgumentsInternal(ArgMap& execArgs) const
62 {
63 if (execArgs.Contains(ArgType::Last) && execArgs.GetValue<ArgType::Latest>())
64 {
65 - throw CommandException(Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest"));
65 + throw ArgumentException(
66 + Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest"),
67 + GetArgumentsForHelp({ArgType::Last, ArgType::Latest}));
68 }
69 }
70 } // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/commands/RegistryCommand.cpp
+4 -2
@@ -78,12 +78,14 @@ void RegistryLoginCommand::ValidateArgumentsInternal(ArgMap& execArgs) const
78 {
79 if (execArgs.Contains(ArgType::Password) && execArgs.GetValue<ArgType::PasswordStdin>())
80 {
81 - throw CommandException(Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive());
81 + throw ArgumentException(
82 + Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive(), GetArgumentsForHelp({ArgType::Password, ArgType::PasswordStdin}));
83 }
84
85 if (execArgs.GetValue<ArgType::PasswordStdin>() && !execArgs.Contains(ArgType::Username))
86 {
86 - throw CommandException(Localization::WSLCCLI_LoginPasswordStdinRequiresUsername());
87 + throw ArgumentException(
88 + Localization::WSLCCLI_LoginPasswordStdinRequiresUsername(), GetArgumentsForHelp({ArgType::PasswordStdin, ArgType::Username}));
89 }
90 }
91
src/windows/wslc/commands/SessionEnterCommand.cpp
+1 -1
@@ -45,7 +45,7 @@ void SessionEnterCommand::ExecuteInternal(CLIExecutionContext& context) const
45 {
46 if (context.GlobalArgs.Contains(ArgType::Session))
47 {
48 - throw CommandException(Localization::MessageWslcSessionOptionNotSupported());
48 + throw ExecutionException(Localization::MessageWslcSessionOptionNotSupported());
49 }
50
51 context << EnterSession;
src/windows/wslc/core/Command.cpp
+185 -58
@@ -18,6 +18,8 @@ Abstract:
18 #include "RootCommand.h"
19 #include "TableOutput.h"
20
21 +#include <algorithm>
22 +
23 using namespace wsl::shared;
24 using namespace wsl::windows::common::wslutil;
25 using namespace wsl::windows::common::vt;
@@ -43,11 +45,14 @@ Command::Command(std::wstring_view name, std::vector<std::wstring_view>&& aliase
45 }
46 }
47
46 -void Command::OutputHelp(Terminal& terminal, const CommandException* exception) const
48 +void Command::OutputHelp(Terminal& terminal, HelpOutput output, const CommandException* exception, std::span<const Argument> relevantArguments) const
49 {
50 constexpr size_t c_helpRowIndent = 2;
51 constexpr size_t c_helpColumnPadding = 2;
50 - const auto helpLevel = exception ? Terminal::Level::Info : Terminal::Level::Output;
52 + const bool fullHelp = output == HelpOutput::Full;
53 + const bool commandHelp = output == HelpOutput::Command;
54 + const bool argumentHelp = output == HelpOutput::Argument;
55 + const auto helpLevel = fullHelp ? Terminal::Level::Output : Terminal::Level::Info;
56
57 // Emphasis sequences for help output.
58 static const auto& HelpHeadingEmphasis = Format::Bright;
@@ -56,8 +61,10 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
61 static const auto& HelpMetaEmphasis = Format::Dim;
62 static const auto& HelpPlaceholderEmphasis = Format::Fg::BrightCyan;
63
59 - // Copyright header (dimmed)
60 - terminal.Write(helpLevel, L"{}{}{}\n\n", HelpMetaEmphasis, Localization::WSLCCLI_CopyrightHeader(), Format::Default);
64 + if (fullHelp)
65 + {
66 + terminal.Write(helpLevel, L"{}{}{}\n\n", HelpMetaEmphasis, Localization::WSLCCLI_CopyrightHeader(), Format::Default);
67 + }
68
69 // Error if given
70 if (exception)
@@ -65,8 +72,10 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
72 terminal.Error(L"{}\n\n", exception->Message());
73 }
74
68 - // Description
69 - terminal.Write(helpLevel, L"{}\n\n", LongDescription());
75 + if (fullHelp)
76 + {
77 + terminal.Write(helpLevel, L"{}\n\n", LongDescription());
78 + }
79
80 // Build command chain from full name (replace ParentSplitChar with spaces, strip root).
81 std::wstring commandChain = FullName();
@@ -90,6 +99,15 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
99 auto commandAliases = Aliases();
100 auto commands = GetCommands();
101 auto arguments = GetAllArguments();
102 + std::vector<Argument> helpArguments;
103 + if (fullHelp)
104 + {
105 + helpArguments = arguments;
106 + }
107 + else if (argumentHelp)
108 + {
109 + helpArguments.assign(relevantArguments.begin(), relevantArguments.end());
110 + }
111
112 std::vector<Argument> standardArgs;
113 std::vector<Argument> positionalArgs;
@@ -115,7 +133,30 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
133 const bool hasOptions = !standardArgs.empty();
134 const bool hasForwardArgs = !forwardArgs.empty();
135
118 - // Global options from the root command, shown on every command's help.
136 + std::vector<Argument> helpStandardArgs;
137 + std::vector<Argument> helpPositionalArgs;
138 + std::vector<Argument> helpForwardArgs;
139 + for (const auto& arg : helpArguments)
140 + {
141 + switch (arg.Kind())
142 + {
143 + case Kind::Flag:
144 + case Kind::Value:
145 + helpStandardArgs.emplace_back(arg);
146 + break;
147 + case Kind::Positional:
148 + helpPositionalArgs.emplace_back(arg);
149 + break;
150 + case Kind::Forward:
151 + helpForwardArgs.emplace_back(arg);
152 + break;
153 + }
154 + }
155 +
156 + const bool hasHelpArguments = !helpPositionalArgs.empty();
157 + const bool hasHelpOptions = !helpStandardArgs.empty();
158 + const bool hasHelpForwardArgs = !helpForwardArgs.empty();
159 +
160 auto globalArgs = RootCommand().GetGlobalArguments();
161
162 // Build usage line with Write calls for each segment.
@@ -208,7 +249,7 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
249 terminal.Write(helpLevel, L"\n\n");
250 }
251
211 - if (!commandAliases.empty())
252 + if (fullHelp && !commandAliases.empty())
253 {
254 terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingAliases(), Format::Default);
255
@@ -241,7 +282,47 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
282 return table;
283 };
284
244 - if (!commands.empty())
285 + // Col0: short alias (e.g. "-f")
286 + // Col1: long name (e.g. "--force")
287 + // Col2: description (word-wraps at computed column width)
288 + const auto MakeOptionsTable = [&terminal, helpLevel]() -> TableOutput<3> {
289 + TableOutput<3> table{terminal, {L"", L"", L""}, {}, 50, c_helpColumnPadding, helpLevel};
290 + table.SetShowHeader(false);
291 + table.SetRowIndent(c_helpRowIndent);
292 + table.SetColumnConfig(
293 + 2,
294 + ColumnWidthConfig{
295 + .MinWidth = ColumnWidthConfig::NoLimit,
296 + .MaxWidth = ColumnWidthConfig::NoLimit,
297 + .Overflow = ColumnOverflow::Wrap,
298 + });
299 + return table;
300 + };
301 +
302 + const auto AddArgumentRows = [](auto& table, const std::vector<Argument>& args) {
303 + for (const auto& arg : args)
304 + {
305 + FormattedCell aliasCell{L""};
306 + std::wstring name = arg.Name();
307 + if (arg.Kind() == Kind::Flag || arg.Kind() == Kind::Value)
308 + {
309 + if (!arg.Alias().empty())
310 + {
311 + aliasCell = FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Alias(), HelpArgumentEmphasis);
312 + }
313 +
314 + name = std::wstring{WSLC_CLI_ARG_ID_CHAR} + std::wstring{WSLC_CLI_ARG_ID_CHAR} + name;
315 + }
316 +
317 + table.WriteRow({
318 + std::move(aliasCell),
319 + FormattedCell(std::move(name), HelpArgumentEmphasis),
320 + FormattedCell(arg.Description()),
321 + });
322 + }
323 + };
324 +
325 + if ((fullHelp || commandHelp) && !commands.empty())
326 {
327 terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingCommands(), Format::Default);
328
@@ -255,10 +336,28 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
336 }
337 table.Complete();
338
258 - terminal.Write(helpLevel, L"\n{} [{}]\n", Localization::WSLCCLI_HelpForDetails(), WSLC_CLI_HELP_ARG_STRING);
339 + if (fullHelp)
340 + {
341 + terminal.Write(helpLevel, L"\n{} [{}]\n", Localization::WSLCCLI_HelpForDetails(), WSLC_CLI_HELP_ARG_STRING);
342 + }
343 }
344
261 - if (!arguments.empty())
345 + if (argumentHelp && !helpArguments.empty())
346 + {
347 + const bool onlyRelatedOptions = std::ranges::all_of(helpArguments, &Argument::IsOption);
348 +
349 + terminal.Write(
350 + helpLevel,
351 + L"{}{}{}\n",
352 + HelpHeadingEmphasis,
353 + onlyRelatedOptions ? Localization::WSLCCLI_HeadingRelatedOptions() : Localization::WSLCCLI_HeadingRelatedArguments(),
354 + Format::Default);
355 +
356 + auto table = MakeOptionsTable();
357 + AddArgumentRows(table, helpArguments);
358 + table.Complete();
359 + }
360 + else if (fullHelp && !helpArguments.empty())
361 {
362 if (!commands.empty())
363 {
@@ -266,13 +365,13 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
365 }
366
367 // Arguments table: positional and forward args, name (emphasized) | description
269 - if (hasArguments || hasForwardArgs)
368 + if (hasHelpArguments || hasHelpForwardArgs)
369 {
370 terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingArguments(), Format::Default);
371
372 auto table = MakeHelpTable();
373
275 - for (const auto& arg : positionalArgs)
374 + for (const auto& arg : helpPositionalArgs)
375 {
376 table.WriteRow({
377 FormattedCell(arg.Name(), HelpArgumentEmphasis),
@@ -280,7 +379,7 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
379 });
380 }
381
283 - for (const auto& arg : forwardArgs)
382 + for (const auto& arg : helpForwardArgs)
383 {
384 table.WriteRow({
385 FormattedCell(arg.Name(), HelpArgumentEmphasis),
@@ -292,73 +391,56 @@ void Command::OutputHelp(Terminal& terminal, const CommandException* exception)
391 }
392 }
393
295 - // Col0: short alias (e.g. "-f")
296 - // Col1: long name (e.g. "--force")
297 - // Col2: description (word-wraps at computed column width)
298 - const auto MakeOptionsTable = [&terminal, helpLevel]() -> TableOutput<3> {
299 - TableOutput<3> table{terminal, {L"", L"", L""}, {}, 50, c_helpColumnPadding, helpLevel};
300 - table.SetShowHeader(false);
301 - table.SetRowIndent(c_helpRowIndent);
302 - table.SetColumnConfig(
303 - 2,
304 - ColumnWidthConfig{
305 - .MinWidth = ColumnWidthConfig::NoLimit,
306 - .MaxWidth = ColumnWidthConfig::NoLimit,
307 - .Overflow = ColumnOverflow::Wrap,
308 - });
309 - return table;
310 - };
311 -
394 // Options table: alias (emphasized) | long name (emphasized) | description
395 // Global options are appended to the same table so column widths are shared.
314 - if (hasOptions || !globalArgs.empty())
396 + if (fullHelp && (hasHelpOptions || !globalArgs.empty()))
397 {
316 - if (hasArguments || hasForwardArgs)
398 + if (hasHelpArguments || hasHelpForwardArgs)
399 {
400 terminal.Write(helpLevel, L"\n");
401 }
320 - else if (!commands.empty() && arguments.empty())
402 + else if (fullHelp && !commands.empty() && helpArguments.empty())
403 {
404 terminal.Write(helpLevel, L"\n");
405 }
406
407 auto table = MakeOptionsTable();
408
327 - const auto AddOptionRows = [&table](const std::vector<Argument>& args) {
328 - for (const auto& arg : args)
329 - {
330 - FormattedCell aliasCell{L""};
331 - if (!arg.Alias().empty())
332 - {
333 - aliasCell = FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Alias(), HelpArgumentEmphasis);
334 - }
335 -
336 - table.WriteRow({
337 - std::move(aliasCell),
338 - FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Name(), HelpArgumentEmphasis),
339 - FormattedCell(arg.Description()),
340 - });
341 - }
342 - };
343 -
344 - if (hasOptions)
409 + if (hasHelpOptions)
410 {
411 table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingOptions(), HelpHeadingEmphasis));
347 - AddOptionRows(standardArgs);
412 + AddArgumentRows(table, helpStandardArgs);
413 }
414
350 - if (!globalArgs.empty())
415 + if (fullHelp && !globalArgs.empty())
416 {
352 - if (hasOptions)
417 + if (hasHelpOptions)
418 {
419 table.WriteLine();
420 }
421 table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingGlobalOptions(), HelpHeadingEmphasis));
357 - AddOptionRows(globalArgs);
422 + AddArgumentRows(table, globalArgs);
423 }
424
425 table.Complete();
426 }
427 +
428 + if (!fullHelp)
429 + {
430 + if ((commandHelp && !commands.empty()) || (argumentHelp && !helpArguments.empty()))
431 + {
432 + terminal.Write(helpLevel, L"\n");
433 + }
434 +
435 + std::wstring helpCommand = s_ExecutableName;
436 + if (!commandChain.empty())
437 + {
438 + helpCommand += L' ';
439 + helpCommand += commandChain;
440 + }
441 +
442 + terminal.Write(helpLevel, L"{}\n", Localization::WSLCCLI_RunHelpForMoreInformation(helpCommand));
443 + }
444 }
445
446 std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const
@@ -441,12 +523,38 @@ void Command::ValidateArguments(ArgMap& source, const std::vector<Argument>& def
523 {
524 if (arg.Required() && !source.Contains(arg.Type()))
525 {
444 - throw CommandException(Localization::WSLCCLI_RequiredArgumentError(arg.Name()));
526 + const auto name = arg.IsOption() ? std::wstring(2, WSLC_CLI_ARG_ID_CHAR) + arg.Name() : arg.Name();
527 + throw ArgumentException(
528 + arg.IsOption() ? Localization::WSLCCLI_RequiredArgumentOptionError(name)
529 + : Localization::WSLCCLI_RequiredArgumentError(arg.Name()),
530 + arg);
531 }
532
533 if (source.Contains(arg.Type()))
534 {
449 - arg.Validate(source);
535 + try
536 + {
537 + arg.Validate(source);
538 + }
539 + catch (const ArgumentException& exception)
540 + {
541 + std::vector<Argument> configuredArguments;
542 + if (exception.Arguments().empty())
543 + {
544 + configuredArguments.emplace_back(arg);
545 + }
546 + else
547 + {
548 + configuredArguments.reserve(exception.Arguments().size());
549 + for (const auto& exceptionArgument : exception.Arguments())
550 + {
551 + const auto configuredArgument = std::ranges::find(definedArgs, exceptionArgument.Type(), &Argument::Type);
552 + configuredArguments.emplace_back(configuredArgument != definedArgs.end() ? *configuredArgument : exceptionArgument);
553 + }
554 + }
555 +
556 + throw ArgumentException(exception.Message(), std::move(configuredArguments));
557 + }
558 }
559 }
560
@@ -481,6 +589,25 @@ void Command::ValidateArgumentsInternal(ArgMap&) const
589 // Commands may not need any extra validation; they'll override if they do.
590 }
591
592 +std::vector<Argument> Command::GetArgumentsForHelp(std::initializer_list<ArgType> types) const
593 +{
594 + auto arguments = GetAllArguments();
595 + auto globalArguments = RootCommand().GetGlobalArguments();
596 + arguments.insert(arguments.end(), globalArguments.begin(), globalArguments.end());
597 +
598 + std::vector<Argument> result;
599 + result.reserve(types.size());
600 +
601 + for (const auto type : types)
602 + {
603 + const auto argument = std::ranges::find(arguments, type, &Argument::Type);
604 + THROW_HR_IF_MSG(E_INVALIDARG, argument == arguments.end(), "Argument type %zu is not configured for command", static_cast<size_t>(type));
605 + result.emplace_back(*argument);
606 + }
607 +
608 + return result;
609 +}
610 +
611 std::vector<Argument> Command::GetGlobalsAndEnvArguments() const
612 {
613 auto merged = GetGlobalArguments();
src/windows/wslc/core/Command.h
+16 -1
@@ -20,8 +20,10 @@ Abstract:
20 #include "ArgumentParser.h"
21 #include "Terminal.h"
22
23 +#include <initializer_list>
24 #include <memory>
25 #include <optional>
26 +#include <span>
27 #include <string>
28 #include <string_view>
29 #include <vector>
@@ -31,6 +33,13 @@ using namespace wsl::windows::wslc::argument;
33
34 namespace wsl::windows::wslc {
35
36 +enum class HelpOutput
37 +{
38 + Full,
39 + Command,
40 + Argument,
41 +};
42 +
43 // The executable name shown in usage/help output, set from argv[0] at startup.
44 extern std::wstring s_ExecutableName;
45
@@ -101,7 +110,11 @@ struct Command
110 virtual std::wstring ShortDescription() const = 0;
111 virtual std::wstring LongDescription() const = 0;
112
104 - void OutputHelp(Terminal& terminal, const CommandException* exception = nullptr) const;
113 + void OutputHelp(
114 + Terminal& terminal,
115 + HelpOutput output = HelpOutput::Full,
116 + const CommandException* exception = nullptr,
117 + std::span<const Argument> relevantArguments = {}) const;
118
119 std::unique_ptr<Command> FindSubCommand(Invocation& inv) const;
120
@@ -147,6 +160,8 @@ protected:
160 virtual void ValidateArgumentsInternal(ArgMap& source) const;
161 virtual void ExecuteInternal(CLIExecutionContext& context) const = 0;
162
163 + std::vector<Argument> GetArgumentsForHelp(std::initializer_list<ArgType> types) const;
164 +
165 private:
166 std::wstring_view m_name;
167 std::vector<std::wstring_view> m_aliases;
src/windows/wslc/core/Exceptions.h
+42 -3
@@ -13,11 +13,17 @@ Abstract:
13 --*/
14 #pragma once
15
16 +#include "Argument.h"
17 +
18 +#include <string>
19 +#include <string_view>
20 +#include <utility>
21 +#include <vector>
22 +
23 namespace wsl::windows::wslc {
17 -// Base exception for all command-related errors
18 -struct CommandException
24 +struct CLIException
25 {
20 - CommandException(std::wstring_view message) : m_message(message)
26 + CLIException(std::wstring_view message) : m_message(message)
27 {
28 }
29
@@ -30,11 +36,44 @@ protected:
36 std::wstring m_message;
37 };
38
39 +// Specific exception for command parsing errors
40 +struct CommandException : CLIException
41 +{
42 + CommandException(std::wstring_view message) : CLIException(message)
43 + {
44 + }
45 +};
46 +
47 // Specific exception for argument parsing errors
48 struct ArgumentException : CommandException
49 {
50 ArgumentException(std::wstring_view message) : CommandException(message)
51 {
52 }
53 +
54 + ArgumentException(std::wstring_view message, Argument argument) : CommandException(message), m_arguments{std::move(argument)}
55 + {
56 + }
57 +
58 + ArgumentException(std::wstring_view message, std::vector<Argument> arguments) :
59 + CommandException(message), m_arguments(std::move(arguments))
60 + {
61 + }
62 +
63 + const std::vector<Argument>& Arguments() const
64 + {
65 + return m_arguments;
66 + }
67 +
68 +private:
69 + std::vector<Argument> m_arguments;
70 +};
71 +
72 +// Specific exception for failures after command and argument validation
73 +struct ExecutionException : CLIException
74 +{
75 + ExecutionException(std::wstring_view message) : CLIException(message)
76 + {
77 + }
78 };
79 } // namespace wsl::windows::wslc
src/windows/wslc/core/Main.cpp
+11 -2
@@ -134,10 +134,19 @@ try
134 command->ValidateArguments(context.Args);
135 command->Execute(context);
136 }
137 + catch (const ArgumentException& ae)
138 + {
139 + command->OutputHelp(context.Terminal, HelpOutput::Argument, &ae, ae.Arguments());
140 + return 1;
141 + }
142 catch (const CommandException& ce)
143 {
139 - // Input failure: show help alongside the error so the user can correct it.
140 - command->OutputHelp(context.Terminal, &ce);
144 + command->OutputHelp(context.Terminal, HelpOutput::Command, &ce);
145 + return 1;
146 + }
147 + catch (const ExecutionException& ee)
148 + {
149 + context.Terminal.Error(L"{}\n", ee.Message());
150 return 1;
151 }
152 catch (...)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+13 -2
@@ -55,6 +55,17 @@ class WSLCCLIArgumentUnitTests
55 return true;
56 }
57
58 + TEST_METHOD(ArgumentException_OptionalArgumentHelp)
59 + {
60 + const ArgumentException withoutArgument{L"error"};
61 + VERIFY_IS_TRUE(withoutArgument.Arguments().empty());
62 +
63 + const auto argument = Argument::Create(ArgType::Verbose);
64 + const ArgumentException withArgument{L"error", argument};
65 + VERIFY_ARE_EQUAL(1u, withArgument.Arguments().size());
66 + VERIFY_ARE_EQUAL(ArgType::Verbose, withArgument.Arguments().front().Type());
67 + }
68 +
69 // Test: Verify Argument::Create() successfully creates arguments for all ArgType enum values
70 TEST_METHOD(ArgumentCreate_AllArguments)
71 {
@@ -625,7 +636,7 @@ class WSLCCLIArgumentUnitTests
636 // the failure the up-front pass raises for the same value.
637 ArgMap invalid;
638 invalid.Add(ArgType::Network, std::wstring(L"host"));
628 - VERIFY_THROWS(invalid.GetAllValues<ArgType::Network>(), ArgumentException);
639 + VERIFY_THROWS(invalid.GetAllValues<ArgType::Network>(), ExecutionException);
640
641 // Valid up-front, then an unsupported value added before the first read: the map-action
642 // callback clears the validated record, so the read re-validates on demand and throws.
@@ -633,7 +644,7 @@ class WSLCCLIArgumentUnitTests
644 added.Add(ArgType::Network, std::wstring(L"bridge"));
645 Argument::Create(ArgType::Network).Validate(added);
646 added.Add(ArgType::Network, std::wstring(L"host"));
636 - VERIFY_THROWS(added.GetAllValues<ArgType::Network>(), ArgumentException);
647 + VERIFY_THROWS(added.GetAllValues<ArgType::Network>(), ExecutionException);
648 }
649
650 TEST_METHOD(ArgumentValidate_ReadMakesArgumentImmutable)
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+17
@@ -54,6 +54,23 @@ class WSLCCLIExecutionUnitTests
54 return true;
55 }
56
57 + TEST_METHOD(ValidateArguments_RequiredOptionUsesLongName)
58 + {
59 + RootCommand command;
60 + ArgMap args;
61 + const std::vector<Argument> definitions{Argument::Create(ArgType::Password, true)};
62 +
63 + try
64 + {
65 + command.ValidateArguments(args, definitions, false);
66 + VERIFY_FAIL(L"Expected ArgumentException");
67 + }
68 + catch (const ArgumentException& exception)
69 + {
70 + VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_RequiredArgumentOptionError(L"--password"), exception.Message());
71 + }
72 + }
73 +
74 TEST_METHOD(GlobalEnvironmentOptions_NoColorIsAppliedAndFrozen)
75 {
76 {
test/windows/wslc/WSLCCLIParserUnitTests.cpp
+6 -3
@@ -305,9 +305,10 @@ class WSLCCLIParserUnitTests
305 sm.ThrowIfError();
306 }
307 }
308 - catch (const ArgumentException&)
308 + catch (const ArgumentException& exception)
309 {
310 threw = true;
311 + VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_InvalidNameError(L"--doesnotexist"), exception.Message());
312 }
313
314 VERIFY_IS_TRUE(threw);
@@ -330,9 +331,10 @@ class WSLCCLIParserUnitTests
331 }
332 sm.ThrowIfError();
333 }
333 - catch (const ArgumentException&)
334 + catch (const ArgumentException& exception)
335 {
336 threw = true;
337 + VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_MissingArgumentError(L"--signal"), exception.Message());
338 }
339
340 VERIFY_IS_TRUE(threw);
@@ -637,9 +639,10 @@ class WSLCCLIParserUnitTests
639 }
640 sm.ThrowIfError();
641 }
640 - catch (const ArgumentException&)
642 + catch (const ArgumentException& exception)
643 {
644 threw = true;
645 + VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_FlagInvalidBooleanError(L"--verbose=maybe"), exception.Message());
646 }
647
648 VERIFY_IS_TRUE(threw);
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+17 -18
@@ -870,7 +870,8 @@ class WSLCE2EContainerCreateTests
870 auto result =
871 RunWslc(std::format(L"container create --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
872 result.Verify({.Stdout = L"", .ExitCode = 1});
873 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-timeout argument value: abc"));
873 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
874 + wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"stop-timeout", L"abc")));
875 VerifyContainerIsNotListed(WslcContainerName);
876 }
877
@@ -900,7 +901,7 @@ class WSLCE2EContainerCreateTests
901 RunWslc(std::format(L"container create --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
902 result.Verify({.Stdout = L"", .ExitCode = 1});
903 VERIFY_IS_TRUE(result.StderrContainsSubstring(
903 - L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
904 + wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"invalid")));
905 VerifyContainerIsNotListed(WslcContainerName);
906 }
907
@@ -908,8 +909,8 @@ class WSLCE2EContainerCreateTests
909 auto result =
910 RunWslc(std::format(L"container create --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
911 result.Verify({.Stdout = L"", .ExitCode = 1});
911 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
912 - L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)"));
912 + VERIFY_IS_TRUE(
913 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"128X")));
914 VerifyContainerIsNotListed(WslcContainerName);
915 }
916 }
@@ -979,7 +980,8 @@ class WSLCE2EContainerCreateTests
980 auto result = RunWslc(std::format(
981 L"container create --health-interval notaduration --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
982 result.Verify({.Stdout = L"", .ExitCode = 1});
982 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid health-interval argument value"));
983 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
984 + wsl::shared::Localization::WSLCCLI_InvalidDurationError(L"health-interval", L"notaduration")));
985 VerifyContainerIsNotListed(WslcContainerName);
986 }
987
@@ -987,7 +989,8 @@ class WSLCE2EContainerCreateTests
989 auto result =
990 RunWslc(std::format(L"container create --health-retries abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
991 result.Verify({.Stdout = L"", .ExitCode = 1});
990 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid health-retries argument value"));
992 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
993 + wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"health-retries", L"abc")));
994 VerifyContainerIsNotListed(WslcContainerName);
995 }
996
@@ -1050,8 +1053,7 @@ class WSLCE2EContainerCreateTests
1053 {
1054 auto result =
1055 RunWslc(std::format(L"container create --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
1053 - result.Verify({.Stdout = L"", .ExitCode = 1});
1054 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
1056 + result.Verify({.Stdout = L"", .Stderr = wsl::shared::Localization::WSLCCLI_NetworkHostModeNotSupportedError() + L"\r\n", .ExitCode = 1});
1057 VerifyContainerIsNotListed(WslcContainerName);
1058 }
1059
@@ -1059,8 +1061,7 @@ class WSLCE2EContainerCreateTests
1061 {
1062 auto result = RunWslc(std::format(
1063 L"container create --name {} --network bridge --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
1062 - result.Verify({.Stdout = L"", .ExitCode = 1});
1063 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
1064 + result.Verify({.Stdout = L"", .Stderr = wsl::shared::Localization::WSLCCLI_NetworkHostModeNotSupportedError() + L"\r\n", .ExitCode = 1});
1065 VerifyContainerIsNotListed(WslcContainerName);
1066 }
1067
@@ -1173,9 +1174,8 @@ class WSLCE2EContainerCreateTests
1174 WslcContainerName,
1175 DebianImage.NameAndTag()));
1176 result.Verify({.Stdout = L"", .ExitCode = 1});
1176 - VERIFY_IS_TRUE(
1177 - result.StderrContainsSubstring(L"Network aliases cannot be specified when multiple networks are requested. Use a "
1178 - L"single --network argument.\r\nError code: E_INVALIDARG"));
1177 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1178 + wsl::shared::Localization::MessageWslcAliasAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1179 VerifyContainerIsNotListed(WslcContainerName);
1180 }
1181
@@ -1202,8 +1202,7 @@ class WSLCE2EContainerCreateTests
1202 {
1203 auto result = RunWslc(std::format(L"container create --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1204 result.Verify({.Stdout = L"", .ExitCode = 1});
1205 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
1206 - L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)"));
1205 + VERIFY_IS_TRUE(result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidCpusError(L"cpus", L"0")));
1206 EnsureContainerDoesNotExist(WslcContainerName);
1207 }
1208
@@ -1224,7 +1223,7 @@ class WSLCE2EContainerCreateTests
1223 RunWslc(std::format(L"container create --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1224 result.Verify({.Stdout = L"", .ExitCode = 1});
1225 VERIFY_IS_TRUE(
1227 - result.StderrContainsSubstring(L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
1226 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"memory", L"invalid")));
1227 EnsureContainerDoesNotExist(WslcContainerName);
1228 }
1229
@@ -1256,8 +1255,8 @@ class WSLCE2EContainerCreateTests
1255 {
1256 auto result = RunWslc(std::format(L"container create --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1257 result.Verify({.Stdout = L"", .ExitCode = 1});
1259 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
1260 - L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)"));
1258 + VERIFY_IS_TRUE(
1259 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidUlimitError(L"ulimit", L"nofile")));
1260 EnsureContainerDoesNotExist(WslcContainerName);
1261 }
1262
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+14 -15
@@ -914,8 +914,7 @@ class WSLCE2EContainerRunTests
914 {
915 auto result =
916 RunWslc(std::format(L"container run --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
917 - result.Verify({.Stdout = L"", .ExitCode = 1});
918 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
917 + result.Verify({.Stdout = L"", .Stderr = wsl::shared::Localization::WSLCCLI_NetworkHostModeNotSupportedError() + L"\r\n", .ExitCode = 1});
918 VerifyContainerIsNotListed(WslcContainerName);
919 }
920
@@ -1028,9 +1027,8 @@ class WSLCE2EContainerRunTests
1027 WslcContainerName,
1028 DebianImage.NameAndTag()));
1029 result.Verify({.Stdout = L"", .ExitCode = 1});
1031 - VERIFY_IS_TRUE(
1032 - result.StderrContainsSubstring(L"Network aliases cannot be specified when multiple networks are requested. Use a "
1033 - L"single --network argument.\r\nError code: E_INVALIDARG"));
1030 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1031 + wsl::shared::Localization::MessageWslcAliasAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1032 }
1033
1034 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_EmptyValue_Rejected)
@@ -1162,7 +1160,8 @@ class WSLCE2EContainerRunTests
1160 auto result =
1161 RunWslc(std::format(L"container run --rm --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1162 result.Verify({.Stdout = L"", .ExitCode = 1});
1165 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-timeout argument value: abc"));
1163 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1164 + wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"stop-timeout", L"abc")));
1165 EnsureContainerDoesNotExist(WslcContainerName);
1166 }
1167
@@ -1196,7 +1195,7 @@ class WSLCE2EContainerRunTests
1195 RunWslc(std::format(L"container run --rm --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1196 result.Verify({.Stdout = L"", .ExitCode = 1});
1197 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1199 - L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
1198 + wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"invalid")));
1199 EnsureContainerDoesNotExist(WslcContainerName);
1200 }
1201
@@ -1204,8 +1203,8 @@ class WSLCE2EContainerRunTests
1203 auto result =
1204 RunWslc(std::format(L"container run --rm --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1205 result.Verify({.Stdout = L"", .ExitCode = 1});
1207 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
1208 - L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)"));
1206 + VERIFY_IS_TRUE(
1207 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"128X")));
1208 EnsureContainerDoesNotExist(WslcContainerName);
1209 }
1210 }
@@ -1253,7 +1252,8 @@ class WSLCE2EContainerRunTests
1252 auto result = RunWslc(
1253 std::format(L"container run --rm --health-timeout invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1254 result.Verify({.Stdout = L"", .ExitCode = 1});
1256 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid health-timeout argument value"));
1255 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1256 + wsl::shared::Localization::WSLCCLI_InvalidDurationError(L"health-timeout", L"invalid")));
1257 EnsureContainerDoesNotExist(WslcContainerName);
1258 }
1259
@@ -1355,8 +1355,7 @@ class WSLCE2EContainerRunTests
1355 {
1356 auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1357 result.Verify({.Stdout = L"", .ExitCode = 1});
1358 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
1359 - L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)"));
1358 + VERIFY_IS_TRUE(result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidCpusError(L"cpus", L"0")));
1359 EnsureContainerDoesNotExist(WslcContainerName);
1360 }
1361
@@ -1366,7 +1365,7 @@ class WSLCE2EContainerRunTests
1365 RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1366 result.Verify({.Stdout = L"", .ExitCode = 1});
1367 VERIFY_IS_TRUE(
1369 - result.StderrContainsSubstring(L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
1368 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"memory", L"invalid")));
1369 EnsureContainerDoesNotExist(WslcContainerName);
1370 }
1371
@@ -1375,8 +1374,8 @@ class WSLCE2EContainerRunTests
1374 auto result =
1375 RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1376 result.Verify({.Stdout = L"", .ExitCode = 1});
1378 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
1379 - L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)"));
1377 + VERIFY_IS_TRUE(
1378 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidUlimitError(L"ulimit", L"nofile")));
1379 EnsureContainerDoesNotExist(WslcContainerName);
1380 }
1381
test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp
+6 -3
@@ -218,7 +218,8 @@ class WSLCE2EContainerStopTests
218 // Invalid integer
219 result = RunWslc(std::format(L"container stop {} -t abc", containerId));
220 result.Verify({.Stdout = L"", .ExitCode = 1});
221 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: abc"));
221 + VERIFY_IS_TRUE(
222 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"time", L"abc")));
223
224 // Should still be running after failed stop
225 VerifyContainerIsListed(containerId, L"running");
@@ -228,7 +229,8 @@ class WSLCE2EContainerStopTests
229 // Another invalid integer shape
230 result = RunWslc(std::format(L"container stop {} -t 1.5", containerId));
231 result.Verify({.Stdout = L"", .ExitCode = 1});
231 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: 1.5"));
232 + VERIFY_IS_TRUE(
233 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"time", L"1.5")));
234
235 // Should still be running after failed stop
236 VerifyContainerIsListed(containerId, L"running");
@@ -238,7 +240,8 @@ class WSLCE2EContainerStopTests
240 // Invalid integer prefixed
241 result = RunWslc(std::format(L"container stop {} -t 9abc", containerId));
242 result.Verify({.Stdout = L"", .ExitCode = 1});
241 - VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: 9abc"));
243 + VERIFY_IS_TRUE(
244 + result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"time", L"9abc")));
245
246 // Should still be running after failed stop
247 VerifyContainerIsListed(containerId, L"running");
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+59 -2
@@ -26,6 +26,8 @@ using namespace wsl::shared;
26
27 namespace {
28
29 + const std::wstring c_copyrightPrefix = L"Copyright (c) Microsoft Corporation.";
30 +
31 // Returns the expected default session name for the current user (e.g. "wslc-cli-admin-benhill").
32 std::wstring GetExpectedDefaultSessionName(bool elevated)
33 {
@@ -75,16 +77,71 @@ class WSLCE2EGlobalTests
77 auto result = RunWslc(L"--help");
78 result.Verify({.Stderr = L"", .ExitCode = 0});
79 VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Usage: wslc"));
80 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(c_copyrightPrefix));
81 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_RootCommandLongDesc()));
82 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_HeadingOptions()));
83 + VERIFY_IS_FALSE(result.StdoutContainsSubstring(Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc")));
84 }
85
80 - WSLC_TEST_METHOD(WSLCE2E_Help_ErrorRoutesToStderr)
86 + WSLC_TEST_METHOD(WSLCE2E_Help_CommandErrorRoutesToStderr)
87 {
82 - // Help on error must land on stderr; stdout must remain empty.
88 auto result = RunWslc(L"INVALID_CMD");
89 VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
90 VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
91 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
92 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Usage: wslc"));
93 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingCommands()));
94 + VERIFY_IS_FALSE(result.StderrContainsSubstring(c_copyrightPrefix));
95 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_RootCommandLongDesc()));
96 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingOptions()));
97 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc")));
98 + }
99 +
100 + WSLC_TEST_METHOD(WSLCE2E_Help_ArgumentErrorRoutesToStderr)
101 + {
102 + auto result = RunWslc(L"container create");
103 + VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
104 + VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
105 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
106 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Usage: wslc container create"));
107 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedArguments()));
108 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_ImageIdArgDescription()));
109 + VERIFY_IS_FALSE(result.StderrContainsSubstring(c_copyrightPrefix));
110 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_ContainerCreateLongDesc()));
111 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingOptions()));
112 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_NameArgDescription()));
113 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
114 + Localization::WSLCCLI_ImageIdArgDescription() + L"\r\n\r\n" +
115 + Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc container create")));
116 + }
117 +
118 + WSLC_TEST_METHOD(WSLCE2E_Help_ArgumentConflictShowsRelevantArguments)
119 + {
120 + auto result = RunWslc(L"container list --last 1 --latest");
121 + VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
122 + VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
123 + VERIFY_IS_TRUE(
124 + result.StderrContainsSubstring(Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest")));
125 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_LastArgDescription()));
126 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_LatestArgDescription()));
127 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedOptions()));
128 + VERIFY_IS_FALSE(result.StderrContainsSubstring(c_copyrightPrefix));
129 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_AllArgDescription()));
130 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
131 + Localization::WSLCCLI_LatestArgDescription() + L"\r\n\r\n" + Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc container list")));
132 + }
133 +
134 + WSLC_TEST_METHOD(WSLCE2E_Help_OptionErrorUsesOptionTerminology)
135 + {
136 + auto result = RunWslc(L"list -a=falseee");
137 + VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
138 + VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
139 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_FlagInvalidBooleanError(L"-a=falseee")));
140 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedOptions()));
141 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_AllArgDescription()));
142 + VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedArguments()));
143 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
144 + Localization::WSLCCLI_AllArgDescription() + L"\r\n\r\n" + Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc list")));
145 }
146
147 WSLC_TEST_METHOD(WSLCE2E_Help_NoColorWhenRedirected)
test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp
+10
@@ -120,5 +120,15 @@ class WSLCE2ESessionEnterTests
120 .ExitCode = 1,
121 });
122 }
123 +
124 + WSLC_TEST_METHOD(WSLCE2E_Help_ExecutionErrorDoesNotShowHelp)
125 + {
126 + const auto result = RunWslc(L"--session unsupported system session enter ignored");
127 + result.Verify({
128 + .Stdout = L"",
129 + .Stderr = wsl::shared::Localization::MessageWslcSessionOptionNotSupported() + L"\r\n",
130 + .ExitCode = 1,
131 + });
132 + }
133 };
134 } // namespace WSLCE2ETests