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="<name>=<soft>[:<hard>]"}Command line arguments should not be translated</comment>
3283
</data>
3284
<data name="WSLCCLI_InvalidUlimitError" xml:space="preserve">
3271
- <value>Invalid {} argument value: '{}'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)</value>
3285
+ <value>Invalid {} option value: '{}'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)</value>
3286
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="-1"}{Locked="<name>=<soft>[:<hard>]"}</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: <host path | named volume>:<container path>[: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