CLI: Add global options, move --session to global options, add environment variable support (#40789)
David Bennett committed
Jun 16, 2026 at 21:28 UTC
0305adac641f449632a8a932b88ee50ac9377767
91 files changed
+1199
-205
localization/strings/en-US/Resources.resw
+3
@@ -2189,6 +2189,9 @@ Usage:
2189
<data name="WSLCCLI_AvailableArguments" xml:space="preserve">
2190
<value>The following arguments are available:</value>
2191
</data>
2192
+ <data name="WSLCCLI_NoColorArgDescription" xml:space="preserve">
2193
+ <value>Disable color output.</value>
2194
+ </data>
2195
<data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2196
<value>For more details on a specific command, pass it the help argument.</value>
2197
</data>
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -80,6 +80,7 @@ _(NetworkAlias, "network-alias", NO_ALIAS, Kind::Value, L
80
_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
81
/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
82
_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
83
+_(NoColor, "no-color", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoColorArgDescription()) \
84
_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \
85
_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \
86
_(ObjectId, "object-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ObjectIdArgDescription()) \
src/windows/wslc/arguments/ArgumentParser.cpp
+106
-31
@@ -17,10 +17,15 @@ Abstract:
17
using namespace wsl::shared;
18
19
namespace wsl::windows::wslc {
20
-ParseArgumentsStateMachine::ParseArgumentsStateMachine(Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments) :
21
- m_invocation(inv), m_executionArgs(execArgs), m_arguments(std::move(arguments)), m_invocationItr(m_invocation.begin())
20
+ParseArgumentsStateMachine::ParseArgumentsStateMachine(
21
+ Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments, bool optionsOnly, bool stopOnUnknown, const std::vector<Argument>& overridableDefaults) :
22
+ m_invocation(inv),
23
+ m_executionArgs(execArgs),
24
+ m_arguments(std::move(arguments)),
25
+ m_invocationItr(m_invocation.begin()),
26
+ m_optionsOnly(optionsOnly),
27
+ m_stopOnUnknown(stopOnUnknown)
28
{
23
- // Create sublists by Kind for easier processing in the state machine.
29
for (const auto& arg : m_arguments)
30
{
31
switch (arg.Kind())
@@ -41,11 +46,17 @@ ParseArgumentsStateMachine::ParseArgumentsStateMachine(Invocation& inv, ArgMap&
46
}
47
48
m_positionalSearchItr = m_positionalArgs.begin();
49
+
50
+ m_overridableDefaults.reserve(overridableDefaults.size());
51
+ for (const auto& arg : overridableDefaults)
52
+ {
53
+ m_overridableDefaults.push_back(arg.Type());
54
+ }
55
}
56
57
bool ParseArgumentsStateMachine::Step()
58
{
48
- if (m_invocationItr == m_invocation.end())
59
+ if (m_stopped || m_invocationItr == m_invocation.end())
60
{
61
return false;
62
}
@@ -88,35 +99,66 @@ bool ParseArgumentsStateMachine::HasNextPositional() const
99
return itr != m_positionalArgs.end();
100
}
101
91
-// Parse arguments as such:
92
-// 1. If argument starts with a single -, the alias is considered (can be 1-2 characters).
93
-// a. If the named argument alias (a or ab) needs a VALUE, it can be provided in these ways:
94
-// -a=VALUE or -ab=VALUE
95
-// -a VALUE or -ab VALUE
96
-// b. If the argument is a flag, additional characters after are treated as if they start
97
-// with a -, repeatedly until the end of the argument is reached. Fails if non-flags hit.
98
-// 2. If the argument starts with a double --, only the full name is considered.
99
-// a. If the named argument (arg) needs a VALUE, it can be provided in these ways:
100
-// --arg=VALUE
101
-// --arg VALUE
102
-// 3. If the argument does not start with any -, it is considered the next positional argument.
103
-// 4. Once a positional argument is encountered, all subsequent arguments are considered positional
104
-// 5. If the command only has 1 positional argument, all subsequent arguments are considered forwarded.
102
+ParseArgumentsStateMachine::State ParseArgumentsStateMachine::BackUpAndStop()
103
+{
104
+ --m_invocationItr;
105
+ m_stopped = true;
106
+ return {};
107
+}
108
+
109
+bool ParseArgumentsStateMachine::ConsumeOverrideIfPresent(ArgType type)
110
+{
111
+ auto it = std::find(m_overridableDefaults.begin(), m_overridableDefaults.end(), type);
112
+ if (it == m_overridableDefaults.end())
113
+ {
114
+ return false;
115
+ }
116
+
117
+ m_executionArgs.Remove(type);
118
+ m_overridableDefaults.erase(it);
119
+ return true;
120
+}
121
+
122
+void ParseArgumentsStateMachine::AddFlag(ArgType type)
123
+{
124
+ if (!ConsumeOverrideIfPresent(type) && m_executionArgs.Contains(type))
125
+ {
126
+ // Repeating the same flag on the CLI is a no-op, matching docker.
127
+ // TODO: revisit when --flag=value (explicit bool) lands so a mismatch
128
+ // between env-preload and CLI-explicit can warn or error.
129
+ return;
130
+ }
131
+
132
+ m_executionArgs.Add(type, true);
133
+}
134
+
135
+void ParseArgumentsStateMachine::AddValue(ArgType type, std::wstring value)
136
+{
137
+ ConsumeOverrideIfPresent(type);
138
+ m_executionArgs.Add(type, std::move(value));
139
+}
140
+
141
+// Parse rules:
142
+// 1. Token starting with a single '-' is an alias (1-2 chars):
143
+// a. Value: '-a=VALUE' / '-ab=VALUE' / '-a VALUE' / '-ab VALUE'
144
+// b. Flag: trailing chars are additional flags; fails if any is non-flag.
145
+// 2. Token starting with '--' is the full name: '--arg=VALUE' or '--arg VALUE'.
146
+// 3. Anything else is the next positional.
147
+// 4. Once a positional is seen, everything after stays positional.
148
+// 5. If only one positional is defined, everything after it is forwarded.
149
ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
150
{
107
- // Get the next argument from the invocation.
151
auto currArg = std::wstring_view{*m_invocationItr};
152
++m_invocationItr;
153
111
- // If current state has a type, then that means this must be a value for the previous argument.
154
+ // Pending value from the previous token.
155
if (m_state.Type())
156
{
114
- m_executionArgs.Add(m_state.Type().value(), std::wstring{currArg});
157
+ AddValue(m_state.Type().value(), std::wstring{currArg});
158
return {};
159
}
160
118
- // If this command has forwarded args present and we have found a positional argument,
119
- // the all remaining args are considered positional or forwarded.
161
+ // Anchored: remaining tokens are positional or forwarded.
162
if (!m_forwardArgs.empty() && m_anchorPositional.has_value())
163
{
164
return ProcessAnchoredPositionals(currArg);
@@ -125,6 +167,13 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
167
// Arg does not begin with '-' so it is neither an alias nor a named value, must be positional.
168
if (currArg.empty() || currArg[0] != WSLC_CLI_ARG_ID_CHAR)
169
{
170
+ if (m_optionsOnly)
171
+ {
172
+ // Options-only mode: stop cleanly at the first positional token without
173
+ // consuming it so the caller can resume parsing (e.g. subcommand resolution).
174
+ return BackUpAndStop();
175
+ }
176
+
177
return ProcessPositionalArgument(currArg);
178
}
179
@@ -138,7 +187,13 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
187
return ProcessPositionalArgument(currArg);
188
}
189
141
- // No positional argument remaining means this is an invalid argument.
190
+ // No positional argument remaining. In stopOnUnknown mode this token isn't ours;
191
+ // back up and let the next pass deal with it.
192
+ if (m_stopOnUnknown)
193
+ {
194
+ return BackUpAndStop();
195
+ }
196
+
197
return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
198
}
199
@@ -254,6 +309,13 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
309
const Argument* firstArg = findArgumentByAlias(currArg, 1, aliasLength);
310
if (!firstArg)
311
{
312
+ // Leading alias is unknown. In stopOnUnknown mode nothing has been added
313
+ // to m_executionArgs for this token yet, so it is safe to back up and stop.
314
+ if (m_stopOnUnknown)
315
+ {
316
+ return BackUpAndStop();
317
+ }
318
+
319
return ArgumentException(Localization::WSLCCLI_InvalidAliasError(currArg));
320
}
321
@@ -281,8 +343,10 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
343
return {};
344
}
345
284
- // Boolean flag - add it and process any adjoined flags
285
- m_executionArgs.Add(firstArg->Type(), true);
346
+ // Boolean flag - add it and process any adjoined flags. Once we have added a
347
+ // flag to m_executionArgs for this token, stopOnUnknown no longer applies for
348
+ // mid-chain unknowns; the token has already been claimed.
349
+ AddFlag(firstArg->Type());
350
351
// Process remaining adjoined flags
352
while (currentPos < currArg.length())
@@ -317,7 +381,7 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
381
return {};
382
}
383
320
- m_executionArgs.Add(nextArg->Type(), true);
384
+ AddFlag(nextArg->Type());
385
currentPos = nextPos;
386
}
387
@@ -331,7 +395,13 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume
395
396
if (currArg.length() == 2)
397
{
334
- // Missing argument name after double dash, this is an error.
398
+ // Bare '--': not a name we recognize. In stopOnUnknown mode hand it off
399
+ // to the next pass; otherwise it's a malformed token at this level.
400
+ if (m_stopOnUnknown)
401
+ {
402
+ return BackUpAndStop();
403
+ }
404
+
405
return ArgumentException(Localization::WSLCCLI_MissingArgumentNameError(currArg));
406
}
407
@@ -366,7 +436,7 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume
436
return ArgumentException(Localization::WSLCCLI_FlagContainAdjoinedError(currArg));
437
}
438
369
- m_executionArgs.Add(arg.Type(), true);
439
+ AddFlag(arg.Type());
440
return {};
441
}
442
@@ -382,7 +452,12 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume
452
}
453
}
454
385
- // We found no matching argument for this name, this is an invalid argument name.
455
+ // Unknown name. In stopOnUnknown mode hand it off to the next pass.
456
+ if (m_stopOnUnknown)
457
+ {
458
+ return BackUpAndStop();
459
+ }
460
+
461
return ArgumentException(Localization::WSLCCLI_InvalidNameError(currArg));
462
}
463
@@ -394,6 +469,6 @@ void ParseArgumentsStateMachine::ProcessAdjoinedValue(ArgType type, std::wstring
469
value = value.substr(1, value.length() - 2);
470
}
471
397
- m_executionArgs.Add(type, std::wstring{value});
472
+ AddValue(type, std::wstring{value});
473
}
474
} // namespace wsl::windows::wslc
src/windows/wslc/arguments/ArgumentParser.h
+62
-18
@@ -24,12 +24,28 @@ Abstract:
24
#include <type_traits>
25
26
namespace wsl::windows::wslc {
27
-// The argument parsing state machine.
28
-// It is broken out to enable completion to process arguments, ignore errors,
29
-// and determine the likely state of the word to be completed.
27
+// State machine is exposed so completion can run the parser, ignore errors,
28
+// and inspect the in-progress state of the word being completed.
29
struct ParseArgumentsStateMachine
30
{
32
- ParseArgumentsStateMachine(Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments);
31
+ // optionsOnly: stop (without consuming) at the first positional token.
32
+ // stopOnUnknown: stop (without consuming) at the first unknown option
33
+ // token instead of throwing.
34
+ // overridableDefaults: ArgTypes whose existing entries in execArgs are
35
+ // treated as preloaded defaults (e.g. from environment
36
+ // variables). The first CLI Add for one of these types
37
+ // clears the preexisting entry first, so a single-value
38
+ // arg can be overridden on the command line even though
39
+ // Limit() == 1. Subsequent Adds in the same parse run
40
+ // behave normally and still enforce Limit, so
41
+ // duplicates on the command line itself are caught.
42
+ ParseArgumentsStateMachine(
43
+ Invocation& inv,
44
+ ArgMap& execArgs,
45
+ std::vector<Argument> arguments,
46
+ bool optionsOnly = false,
47
+ bool stopOnUnknown = false,
48
+ const std::vector<Argument>& overridableDefaults = {});
49
50
ParseArgumentsStateMachine(const ParseArgumentsStateMachine&) = delete;
51
ParseArgumentsStateMachine& operator=(const ParseArgumentsStateMachine&) = delete;
@@ -37,16 +53,12 @@ struct ParseArgumentsStateMachine
53
ParseArgumentsStateMachine(ParseArgumentsStateMachine&&) = default;
54
ParseArgumentsStateMachine& operator=(ParseArgumentsStateMachine&&) = default;
55
40
- // Processes the next argument from the invocation.
41
- // Returns true if there was an argument to process;
42
- // returns false if there were none.
56
+ // Returns false when there is nothing left to process.
57
bool Step();
58
45
- // Throws if there was an error during the prior step.
59
void ThrowIfError() const;
60
48
- // The current state of the state machine.
49
- // An empty state indicates that the next argument can be anything.
61
+ // Empty state means the next argument can be anything.
62
struct State
63
{
64
State() = default;
@@ -57,19 +69,17 @@ struct ParseArgumentsStateMachine
69
{
70
}
71
60
- // If set, indicates that the next argument is a value for this type.
72
+ // If set, the next argument is a value for this type.
73
const std::optional<ArgType>& Type() const
74
{
75
return m_type;
76
}
77
66
- // The actual argument string associated with Type.
78
const std::wstring& Arg() const
79
{
80
return m_arg;
81
}
82
72
- // If set, indicates that the last argument produced an error.
83
const std::optional<ArgumentException>& Exception() const
84
{
85
return m_exception;
@@ -86,10 +96,9 @@ struct ParseArgumentsStateMachine
96
return m_state;
97
}
98
89
- // Gets the next positional argument, or nullptr if there is not one.
99
const Argument* NextPositional();
100
92
- // Returns true if there is a next positional argument available, without advancing the iterator.
101
+ // Non-advancing variant of NextPositional.
102
bool HasNextPositional() const;
103
104
const std::vector<Argument>& Arguments() const
@@ -97,6 +106,12 @@ struct ParseArgumentsStateMachine
106
return m_arguments;
107
}
108
109
+ // In optionsOnly / stopOnUnknown modes this points at the first unconsumed token.
110
+ Invocation::iterator Position() const
111
+ {
112
+ return m_invocationItr;
113
+ }
114
+
115
private:
116
State StepInternal();
117
State ProcessPositionalArgument(const std::wstring_view& currArg);
@@ -105,9 +120,25 @@ private:
120
State ProcessNamedArgument(const std::wstring_view& currArg);
121
void ProcessAdjoinedValue(ArgType type, std::wstring_view value);
122
108
- // Advances the given iterator past any positionals that have reached their limit.
123
void AdvanceToNextPositional(std::vector<Argument>::iterator& itr) const;
124
125
+ // Backs up one token and stops cleanly so Position() points at the unconsumed token.
126
+ State BackUpAndStop();
127
+
128
+ // Routes a flag add through the override/idempotency rules:
129
+ // - if type is in m_overridableDefaults, the preloaded value is replaced;
130
+ // - else if the flag is already set, the add is a no-op (CLI duplicates
131
+ // fold to a single entry, matching docker / kubectl / git style).
132
+ void AddFlag(ArgType type);
133
+
134
+ // Routes a value add through the override rule. CLI duplicates of value
135
+ // args still stack, so Validate() will catch exceeding Limit.
136
+ void AddValue(ArgType type, std::wstring value);
137
+
138
+ // If type is in m_overridableDefaults, removes any existing entry and
139
+ // consumes the override slot. Returns true if an override was consumed.
140
+ bool ConsumeOverrideIfPresent(ArgType type);
141
+
142
Invocation& m_invocation;
143
ArgMap& m_executionArgs;
144
std::vector<Argument> m_arguments;
@@ -115,14 +146,27 @@ private:
146
Invocation::iterator m_invocationItr;
147
std::vector<Argument>::iterator m_positionalSearchItr;
148
118
- // The anchor positional is the first positional argument processed.
149
+ // First positional processed; anchors handling of subsequent positionals/forwards.
150
std::optional<Argument> m_anchorPositional = std::nullopt;
151
121
- // Separate arguments by Kind
152
std::vector<Argument> m_standardArgs = {};
153
std::vector<Argument> m_positionalArgs = {};
154
std::vector<Argument> m_forwardArgs = {};
155
156
State m_state;
157
+
158
+ // When true, stop cleanly at the first positional token (do not consume it).
159
+ bool m_optionsOnly = false;
160
+
161
+ // When true, stop cleanly (do not consume) at the first unknown option token.
162
+ bool m_stopOnUnknown = false;
163
+
164
+ // Set when m_optionsOnly or m_stopOnUnknown stopped processing.
165
+ bool m_stopped = false;
166
+
167
+ // ArgTypes whose preloaded value should be replaced by the first CLI add.
168
+ // Empties as overrides are consumed so a single preload can only be
169
+ // overridden once per parse.
170
+ std::vector<ArgType> m_overridableDefaults;
171
};
172
} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerAttachCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ContainerAttachCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::ContainerId, true),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/ContainerCreateCommand.cpp
-1
@@ -56,7 +56,6 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
56
Argument::Create(ArgType::PublishAll),
57
Argument::Create(ArgType::Remove),
58
// Argument::Create(ArgType::Scheme),
59
- Argument::Create(ArgType::Session),
59
Argument::Create(ArgType::ShmSize),
60
Argument::Create(ArgType::StopSignal),
61
Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
src/windows/wslc/commands/ContainerExecCommand.cpp
-1
@@ -34,7 +34,6 @@ std::vector<Argument> ContainerExecCommand::GetArguments() const
34
Argument::Create(ArgType::Env, false, NO_LIMIT),
35
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
36
Argument::Create(ArgType::Interactive),
37
- Argument::Create(ArgType::Session),
37
Argument::Create(ArgType::TTY),
38
Argument::Create(ArgType::User),
39
Argument::Create(ArgType::WorkDir),
src/windows/wslc/commands/ContainerExportCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> ContainerExportCommand::GetArguments() const
29
return {
30
Argument::Create(ArgType::ContainerId, true),
31
Argument::Create(ArgType::Output, std::nullopt, std::nullopt, Localization::WSLCCLI_ContainerExportOutputArgDescription()),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/ContainerInspectCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ContainerInspectCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/ContainerKillCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ContainerKillCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
Argument::Create(ArgType::Signal),
32
};
33
}
src/windows/wslc/commands/ContainerListCommand.cpp
-1
@@ -35,7 +35,6 @@ std::vector<Argument> ContainerListCommand::GetArguments() const
35
Argument::Create(ArgType::Latest),
36
Argument::Create(ArgType::NoTrunc),
37
Argument::Create(ArgType::Quiet),
38
- Argument::Create(ArgType::Session),
38
};
39
}
40
src/windows/wslc/commands/ContainerLogsCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ContainerLogsCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::ContainerId, true),
31
- Argument::Create(ArgType::Session),
31
Argument::Create(ArgType::Follow),
32
Argument::Create(ArgType::Tail),
33
Argument::Create(ArgType::Timestamps),
src/windows/wslc/commands/ContainerPruneCommand.cpp
+1
-3
@@ -26,9 +26,7 @@ namespace wsl::windows::wslc {
26
// Container Prune Command
27
std::vector<Argument> ContainerPruneCommand::GetArguments() const
28
{
29
- return {
30
- Argument::Create(ArgType::Session),
31
- };
29
+ return {};
30
}
31
32
std::wstring ContainerPruneCommand::ShortDescription() const
src/windows/wslc/commands/ContainerRemoveCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> ContainerRemoveCommand::GetArguments() const
29
return {
30
Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
31
Argument::Create(ArgType::Force),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/ContainerRunCommand.cpp
-1
@@ -57,7 +57,6 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
57
// Argument::Create(ArgType::Pull),
58
Argument::Create(ArgType::Remove),
59
// Argument::Create(ArgType::Scheme),
60
- Argument::Create(ArgType::Session),
60
Argument::Create(ArgType::ShmSize),
61
Argument::Create(ArgType::StopSignal),
62
Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
src/windows/wslc/commands/ContainerStartCommand.cpp
-1
@@ -30,7 +30,6 @@ std::vector<Argument> ContainerStartCommand::GetArguments() const
30
Argument::Create(ArgType::ContainerId, true),
31
Argument::Create(ArgType::Attach),
32
Argument::Create(ArgType::Interactive), // NYI
33
- Argument::Create(ArgType::Session), // NYI
33
};
34
}
35
src/windows/wslc/commands/ContainerStatsCommand.cpp
-1
@@ -31,7 +31,6 @@ std::vector<Argument> ContainerStatsCommand::GetArguments() const
31
Argument::Create(ArgType::All),
32
Argument::Create(ArgType::Format),
33
Argument::Create(ArgType::NoTrunc),
34
- Argument::Create(ArgType::Session),
34
};
35
}
36
src/windows/wslc/commands/ContainerStopCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ContainerStopCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::ContainerId, std::nullopt, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
Argument::Create(ArgType::Signal),
32
Argument::Create(ArgType::Time),
33
};
src/windows/wslc/commands/ImageBuildCommand.cpp
-1
@@ -33,7 +33,6 @@ std::vector<Argument> ImageBuildCommand::GetArguments() const
33
Argument::Create(ArgType::BuildTarget),
34
Argument::Create(ArgType::File),
35
Argument::Create(ArgType::NoCache),
36
- Argument::Create(ArgType::Session),
36
Argument::Create(ArgType::Tag, false, NO_LIMIT),
37
Argument::Create(ArgType::Verbose),
38
};
src/windows/wslc/commands/ImageImportCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> ImageImportCommand::GetArguments() const
29
return {
30
Argument::Create(ArgType::ImportFile, true),
31
Argument::Create(ArgType::ImageId),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/ImageInspectCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> ImageInspectCommand::GetArguments() const
29
{
30
return {
31
Argument::Create(ArgType::ImageId, true, NO_LIMIT),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/ImageListCommand.cpp
-1
@@ -32,7 +32,6 @@ std::vector<Argument> ImageListCommand::GetArguments() const
32
Argument::Create(ArgType::Format),
33
Argument::Create(ArgType::NoTrunc),
34
Argument::Create(ArgType::Quiet),
35
- Argument::Create(ArgType::Session),
35
Argument::Create(ArgType::Verbose)};
36
}
37
src/windows/wslc/commands/ImageLoadCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ImageLoadCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::Input),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/ImagePruneCommand.cpp
-1
@@ -30,7 +30,6 @@ std::vector<Argument> ImagePruneCommand::GetArguments() const
30
return {
31
Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_ImagePruneAllArgDescription()),
32
Argument::Create(ArgType::Filter, false, NO_LIMIT),
33
- Argument::Create(ArgType::Session),
33
};
34
}
35
src/windows/wslc/commands/ImagePullCommand.cpp
-1
@@ -30,7 +30,6 @@ std::vector<Argument> ImagePullCommand::GetArguments() const
30
Argument::Create(ArgType::ImageId, true),
31
// Argument::Create(ArgType::Scheme),
32
// Argument::Create(ArgType::Progress),
33
- Argument::Create(ArgType::Session),
33
};
34
}
35
src/windows/wslc/commands/ImagePushCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> ImagePushCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::ImageId, true),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/ImageRemoveCommand.cpp
-1
@@ -31,7 +31,6 @@ std::vector<Argument> ImageRemoveCommand::GetArguments() const
31
Argument::Create(ArgType::ImageId, true, NO_LIMIT),
32
Argument::Create(ArgType::ImageForce),
33
Argument::Create(ArgType::NoPrune),
34
- Argument::Create(ArgType::Session),
34
};
35
}
36
src/windows/wslc/commands/ImageSaveCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> ImageSaveCommand::GetArguments() const
29
return {
30
Argument::Create(ArgType::ImageId, true, NO_LIMIT),
31
Argument::Create(ArgType::Output),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/ImageTagCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> ImageTagCommand::GetArguments() const
29
return {
30
Argument::Create(ArgType::Source, true),
31
Argument::Create(ArgType::Target, true),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/InspectCommand.cpp
-1
@@ -24,7 +24,6 @@ std::vector<Argument> InspectCommand::GetArguments() const
24
return {
25
Argument::Create(ArgType::ObjectId, true, NO_LIMIT),
26
Argument::Create(ArgType::Type),
27
- Argument::Create(ArgType::Session),
27
};
28
}
29
src/windows/wslc/commands/NetworkCreateCommand.cpp
-1
@@ -31,7 +31,6 @@ std::vector<Argument> NetworkCreateCommand::GetArguments() const
31
Argument::Create(ArgType::Driver, std::nullopt, std::nullopt, Localization::WSLCCLI_NetworkDriverOptionDescription()),
32
Argument::Create(ArgType::Options, false, NO_LIMIT),
33
Argument::Create(ArgType::Label, false, NO_LIMIT, Localization::WSLCCLI_NetworkLabelArgDescription()),
34
- Argument::Create(ArgType::Session),
34
};
35
}
36
src/windows/wslc/commands/NetworkInspectCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> NetworkInspectCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::NetworkName, true, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/NetworkListCommand.cpp
-1
@@ -30,7 +30,6 @@ std::vector<Argument> NetworkListCommand::GetArguments() const
30
return {
31
Argument::Create(ArgType::Format),
32
Argument::Create(ArgType::Quiet, false, std::nullopt, Localization::WSLCCLI_NetworkListQuietArgDesc()),
33
- Argument::Create(ArgType::Session),
33
};
34
}
35
src/windows/wslc/commands/NetworkRemoveCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> NetworkRemoveCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::NetworkName, true, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/RegistryCommand.cpp
-1
@@ -96,7 +96,6 @@ std::vector<Argument> RegistryLoginCommand::GetArguments() const
96
Argument::Create(ArgType::PasswordStdin),
97
Argument::Create(ArgType::Username),
98
Argument::Create(ArgType::Server),
99
- Argument::Create(ArgType::Session),
99
};
100
}
101
src/windows/wslc/commands/RootCommand.cpp
+18
@@ -73,6 +73,24 @@ std::vector<Argument> RootCommand::GetArguments() const
73
};
74
}
75
76
+// Global options apply to the overall invocation and may appear before any
77
+// subcommand (e.g. `wslc --session foo image list`). Define them here using
78
+// the Argument::Create factory backed by ArgumentDefinitions.h so help text,
79
+// aliases, validation, and parsing match subcommand arguments.
80
+std::vector<Argument> RootCommand::GetGlobalArguments() const
81
+{
82
+ return {
83
+ Argument::Create(ArgType::Session),
84
+ };
85
+}
86
+
87
+std::vector<Argument> RootCommand::GetEnvArguments() const
88
+{
89
+ return {
90
+ Argument::Create(ArgType::NoColor),
91
+ };
92
+}
93
+
94
std::wstring RootCommand::ShortDescription() const
95
{
96
return Localization::WSLCCLI_RootCommandDesc();
src/windows/wslc/commands/RootCommand.h
+2
@@ -25,6 +25,8 @@ struct RootCommand final : public Command
25
26
std::vector<std::unique_ptr<Command>> GetCommands() const override;
27
std::vector<Argument> GetArguments() const override;
28
+ std::vector<Argument> GetGlobalArguments() const override;
29
+ std::vector<Argument> GetEnvArguments() const override;
30
std::wstring ShortDescription() const override;
31
std::wstring LongDescription() const override;
32
src/windows/wslc/commands/SessionRunCommand.cpp
-1
@@ -27,7 +27,6 @@ std::vector<Argument> SessionRunCommand::GetArguments() const
27
return {
28
Argument::Create(ArgType::Command, true),
29
Argument::Create(ArgType::ForwardArgs, std::nullopt, std::nullopt, Localization::WSLCCLI_SessionRunForwardArgsDescription()),
30
- Argument::Create(ArgType::Session),
30
};
31
}
32
src/windows/wslc/commands/VolumeCreateCommand.cpp
-1
@@ -31,7 +31,6 @@ std::vector<Argument> VolumeCreateCommand::GetArguments() const
31
Argument::Create(ArgType::Driver),
32
Argument::Create(ArgType::Options, false, NO_LIMIT),
33
Argument::Create(ArgType::Label, false, NO_LIMIT),
34
- Argument::Create(ArgType::Session),
34
};
35
}
36
src/windows/wslc/commands/VolumeInspectCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> VolumeInspectCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/commands/VolumeListCommand.cpp
-1
@@ -30,7 +30,6 @@ std::vector<Argument> VolumeListCommand::GetArguments() const
30
return {
31
Argument::Create(ArgType::Format),
32
Argument::Create(ArgType::Quiet, false, std::nullopt, Localization::WSLCCLI_VolumeListQuietArgDesc()),
33
- Argument::Create(ArgType::Session),
33
};
34
}
35
src/windows/wslc/commands/VolumePruneCommand.cpp
-1
@@ -29,7 +29,6 @@ std::vector<Argument> VolumePruneCommand::GetArguments() const
29
return {
30
Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()),
31
Argument::Create(ArgType::Filter, false, NO_LIMIT),
32
- Argument::Create(ArgType::Session),
32
};
33
}
34
src/windows/wslc/commands/VolumeRemoveCommand.cpp
-1
@@ -28,7 +28,6 @@ std::vector<Argument> VolumeRemoveCommand::GetArguments() const
28
{
29
return {
30
Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
31
- Argument::Create(ArgType::Session),
31
};
32
}
33
src/windows/wslc/core/CLIExecutionContext.cpp
new
+24
@@ -0,0 +1,24 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+--*/
6
+#include "precomp.h"
7
+#include "CLIExecutionContext.h"
8
+
9
+namespace wsl::windows::wslc::execution {
10
+
11
+HANDLE CLIExecutionContext::CreateCancelEvent()
12
+{
13
+ WI_ASSERT(!CancelEvent);
14
+ CancelEvent.create(wil::EventOptions::ManualReset);
15
+ return CancelEvent.get();
16
+}
17
+
18
+// This method should be idempotent.
19
+void CLIExecutionContext::ApplyGlobalOptions()
20
+{
21
+ // TODO: Add per-global side effects here as features land.
22
+}
23
+
24
+} // namespace wsl::windows::wslc::execution
src/windows/wslc/core/CLIExecutionContext.h
+14
-13
@@ -17,8 +17,7 @@ Abstract:
17
#include <optional>
18
19
namespace wsl::windows::wslc::execution {
20
-// The context within which all commands execute.
21
-// Contains arguments via Args.
20
+
21
struct CLIExecutionContext : public wsl::windows::common::ExecutionContext
22
{
23
CLIExecutionContext() : wsl::windows::common::ExecutionContext(wsl::windows::common::Context::WslC)
@@ -30,25 +29,27 @@ struct CLIExecutionContext : public wsl::windows::common::ExecutionContext
29
CLIExecutionContext(CLIExecutionContext&&) = default;
30
CLIExecutionContext& operator=(CLIExecutionContext&&) = default;
31
32
+ // Per-subcommand arguments parsed by the resolved leaf Command.
33
argument::ArgMap Args;
34
35
+ // Global options parsed from tokens that appear before any subcommand
36
+ // (e.g. `wslc <global-option> image list`). Populated early in CoreMain.
37
+ argument::ArgMap GlobalArgs;
38
+
39
// Map of data stored in the context.
40
DataMap Data;
41
38
- // Process exit code set by tasks like Run/Exec. When set, CoreMain returns this
39
- // instead of the HRESULT, enabling `wslc run ... && echo success` patterns.
42
+ // Process exit code set by tasks like Run/Exec.
43
std::optional<int> ExitCode;
44
42
- // Event signaled when the user presses Ctrl-C. Starts null; long-running operations
43
- // that support cancellation create it via CreateCancelEvent() before passing it to
44
- // COM APIs that accept a CancelEvent handle.
45
+ // Event signaled when the user presses Ctrl-C.
46
wil::unique_event CancelEvent;
47
47
- HANDLE CreateCancelEvent()
48
- {
49
- WI_ASSERT(!CancelEvent);
50
- CancelEvent.create(wil::EventOptions::ManualReset);
51
- return CancelEvent.get();
52
- }
48
+ HANDLE CreateCancelEvent();
49
+
50
+ // Single chokepoint that turns parsed GlobalArgs into process-wide effects
51
+ // (debug logging, VT color, ...). Idempotent.
52
+ void ApplyGlobalOptions();
53
};
54
+
55
} // namespace wsl::windows::wslc::execution
src/windows/wslc/core/Command.cpp
+46
-13
@@ -311,16 +311,27 @@ std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const
311
// Argument map is based on the arguments that the command defines and are stored as
312
// an enum -> variant multimap. This is parsing and value storage only, not validation of
313
// the argument data.
314
-void Command::ParseArguments(Invocation& inv, ArgMap& execArgs) const
314
+void Command::ParseArguments(
315
+ Invocation& inv, ArgMap& target, std::vector<Argument> definedArgs, bool optionsOnly, bool stopOnUnknown, const std::vector<Argument>& overridableDefaults) const
316
{
316
- auto definedArgs = GetAllArguments();
317
+ if (definedArgs.empty())
318
+ {
319
+ return;
320
+ }
321
318
- ParseArgumentsStateMachine stateMachine{inv, execArgs, std::move(definedArgs)};
322
+ ParseArgumentsStateMachine stateMachine{inv, target, std::move(definedArgs), optionsOnly, stopOnUnknown, overridableDefaults};
323
324
while (stateMachine.Step())
325
{
326
stateMachine.ThrowIfError();
327
}
328
+ stateMachine.ThrowIfError();
329
+
330
+ // Both modes leave the iterator at the first unconsumed token; sync inv.
331
+ if (optionsOnly || stopOnUnknown)
332
+ {
333
+ inv.consumeUntil(stateMachine.Position());
334
+ }
335
}
336
337
// Validates the ArgMap produced by ParseArguments. ArgMap is assumed to have
@@ -328,34 +339,35 @@ void Command::ParseArguments(Invocation& inv, ArgMap& execArgs) const
339
// that the arguments provided meet the requirements of the command. This includes checking
340
// that all required arguments are present and no arguments exceed their count limits.
341
// Any defined validation for specific ArgTypes are also run.
331
-void Command::ValidateArguments(ArgMap& execArgs) const
342
+void Command::ValidateArguments(const ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const
343
{
333
- // If help is asked for, don't bother validating anything else.
334
- if (execArgs.Contains(ArgType::Help))
344
+ if (source.Contains(ArgType::Help))
345
{
346
return;
347
}
348
339
- auto allArgs = GetAllArguments();
340
- for (const auto& arg : allArgs)
349
+ for (const auto& arg : definedArgs)
350
{
342
- if (arg.Required() && !execArgs.Contains(arg.Type()))
351
+ if (arg.Required() && !source.Contains(arg.Type()))
352
{
353
throw CommandException(Localization::WSLCCLI_RequiredArgumentError(arg.Name()));
354
}
355
347
- if ((arg.Limit() > 0) && (arg.Limit() < execArgs.Count(arg.Type())))
356
+ if ((arg.Limit() > 0) && (arg.Limit() < source.Count(arg.Type())))
357
{
358
throw CommandException(Localization::WSLCCLI_TooManyArgumentsError(arg.Name()));
359
}
360
352
- if (execArgs.Contains(arg.Type()))
361
+ if (source.Contains(arg.Type()))
362
{
354
- arg.Validate(execArgs);
363
+ arg.Validate(source);
364
}
365
}
366
358
- ValidateArgumentsInternal(execArgs);
367
+ if (runInternalHook)
368
+ {
369
+ ValidateArgumentsInternal(source);
370
+ }
371
}
372
373
void Command::Execute(CLIExecutionContext& context) const
@@ -382,4 +394,25 @@ void Command::ValidateArgumentsInternal(const ArgMap&) const
394
{
395
// Commands may not need any extra validation; they'll override if they do.
396
}
397
+
398
+std::vector<Argument> Command::GetGlobalsAndEnvArguments() const
399
+{
400
+ auto merged = GetGlobalArguments();
401
+ auto envOnly = GetEnvArguments();
402
+
403
+ // Globals listed first, so the loop below treats them as the winners.
404
+ merged.reserve(merged.size() + envOnly.size());
405
+ for (auto& arg : envOnly)
406
+ {
407
+ const auto type = arg.Type();
408
+ const bool alreadyPresent =
409
+ std::any_of(merged.begin(), merged.end(), [type](const Argument& existing) { return existing.Type() == type; });
410
+ if (!alreadyPresent)
411
+ {
412
+ merged.emplace_back(std::move(arg));
413
+ }
414
+ }
415
+
416
+ return merged;
417
+}
418
} // namespace wsl::windows::wslc
src/windows/wslc/core/Command.h
+47
-3
@@ -79,6 +79,23 @@ struct Command
79
return args;
80
}
81
82
+ // Options accepted before any subcommand on the command line.
83
+ virtual std::vector<Argument> GetGlobalArguments() const
84
+ {
85
+ return {};
86
+ }
87
+
88
+ // Args eligible for environment binding.
89
+ virtual std::vector<Argument> GetEnvArguments() const
90
+ {
91
+ return {};
92
+ }
93
+
94
+ // Union of GetGlobalArguments() and GetEnvArguments(), deduped by ArgType
95
+ // (globals win on conflict). Use this anywhere the two sets are combined
96
+ // so duplicates are not parsed/validated twice.
97
+ std::vector<Argument> GetGlobalsAndEnvArguments() const;
98
+
99
virtual std::wstring ShortDescription() const = 0;
100
virtual std::wstring LongDescription() const = 0;
101
@@ -86,13 +103,40 @@ struct Command
103
void OutputHelp(const CommandException* exception = nullptr) const;
104
105
std::unique_ptr<Command> FindSubCommand(Invocation& inv) const;
89
- void ParseArguments(Invocation& inv, ArgMap& execArgs) const;
90
- void ValidateArguments(ArgMap& execArgs) const;
106
+
107
+ // optionsOnly: stop (without consuming) at the first positional token.
108
+ // stopOnUnknown: stop (without consuming) at the first unknown option
109
+ // token instead of throwing. Note: applies per-token; a
110
+ // bundled short chain (e.g. "-Dv") whose leading alias
111
+ // is recognized is treated as claimed, and an unknown
112
+ // alias later in the chain still throws.
113
+ // overridableDefaults: args whose preloaded entries in target are treated
114
+ // as defaults (e.g. env-applied) and may be replaced
115
+ // by the first CLI occurrence.
116
+ void ParseArguments(
117
+ Invocation& inv,
118
+ ArgMap& target,
119
+ std::vector<Argument> definedArgs,
120
+ bool optionsOnly = false,
121
+ bool stopOnUnknown = false,
122
+ const std::vector<Argument>& overridableDefaults = {}) const;
123
+
124
+ void ParseArguments(Invocation& inv, ArgMap& target) const
125
+ {
126
+ ParseArguments(inv, target, GetAllArguments());
127
+ }
128
+
129
+ void ValidateArguments(const ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const;
130
+
131
+ void ValidateArguments(const ArgMap& source) const
132
+ {
133
+ ValidateArguments(source, GetAllArguments(), true);
134
+ }
135
136
virtual void Execute(CLIExecutionContext& context) const;
137
138
protected:
95
- virtual void ValidateArgumentsInternal(const ArgMap& execArgs) const;
139
+ virtual void ValidateArgumentsInternal(const ArgMap& source) const;
140
virtual void ExecuteInternal(CLIExecutionContext& context) const = 0;
141
142
private:
src/windows/wslc/core/EnvironmentOptions.cpp
new
+79
@@ -0,0 +1,79 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ EnvironmentOptions.cpp
8
+
9
+--*/
10
+#include "precomp.h"
11
+#include "EnvironmentOptions.h"
12
+
13
+namespace wsl::windows::wslc {
14
+namespace {
15
+
16
+ // nullopt iff the variable is not defined; engaged (possibly empty) otherwise.
17
+ std::optional<std::wstring> ReadEnv(const wchar_t* name) noexcept
18
+ try
19
+ {
20
+ std::wstring value;
21
+ const HRESULT hr = wil::GetEnvironmentVariableW(name, value);
22
+ if (FAILED(hr))
23
+ {
24
+ return std::nullopt;
25
+ }
26
+ return value;
27
+ }
28
+ catch (...)
29
+ {
30
+ return std::nullopt;
31
+ }
32
+
33
+} // namespace
34
+
35
+void ApplyEnvironmentOptions(argument::ArgMap& target, const std::vector<Argument>& definedArgs) noexcept
36
+try
37
+{
38
+ for (const auto& arg : definedArgs)
39
+ {
40
+ // Lowest-precedence: skip args already set by the caller.
41
+ if (target.Contains(arg.Type()))
42
+ {
43
+ continue;
44
+ }
45
+
46
+ for (const auto& binding : c_envBindings)
47
+ {
48
+ if (binding.Type != arg.Type())
49
+ {
50
+ continue;
51
+ }
52
+
53
+ auto value = ReadEnv(binding.Name);
54
+ if (!value.has_value())
55
+ {
56
+ continue;
57
+ }
58
+
59
+ if (arg.Kind() == Kind::Flag)
60
+ {
61
+ target.Add(arg.Type(), true);
62
+ }
63
+ else if (arg.Kind() == Kind::Value)
64
+ {
65
+ target.Add(arg.Type(), std::move(*value));
66
+ }
67
+
68
+ break;
69
+ }
70
+ }
71
+}
72
+catch (...)
73
+{
74
+ // Must not throw: runs before NO_COLOR is applied, so a throw could
75
+ // surface as colored error output from the parser's error path.
76
+ LOG_CAUGHT_EXCEPTION();
77
+}
78
+
79
+} // namespace wsl::windows::wslc
src/windows/wslc/core/EnvironmentOptions.h
new
+40
@@ -0,0 +1,40 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ EnvironmentOptions.h
8
+
9
+Abstract:
10
+
11
+ Maps environment variables onto ArgTypes for early CLI configuration.
12
+
13
+--*/
14
+#pragma once
15
+#include "Argument.h"
16
+#include "ArgumentTypes.h"
17
+
18
+#include <vector>
19
+
20
+namespace wsl::windows::wslc {
21
+
22
+// Binding contract: presence of Name sets the option. The value is ignored
23
+// for Flag kinds and stored verbatim for Value kinds. To opt out, unset the
24
+// variable.
25
+struct EnvBinding
26
+{
27
+ const wchar_t* Name;
28
+ ArgType Type;
29
+};
30
+
31
+// Many-to-one allowed: multiple env var names may bind to one ArgType.
32
+constexpr EnvBinding c_envBindings[] = {
33
+ {L"NO_COLOR", ArgType::NoColor},
34
+};
35
+
36
+// Populates target for any ArgType in definedArgs not already set.
37
+// Never throws on user input or environment state.
38
+void ApplyEnvironmentOptions(argument::ArgMap& target, const std::vector<Argument>& definedArgs) noexcept;
39
+
40
+} // namespace wsl::windows::wslc
src/windows/wslc/core/Invocation.h
+8
@@ -88,10 +88,18 @@ struct Invocation
88
{
89
return {m_args.size(), m_args};
90
}
91
+ // Marks i as consumed: the next begin() returns i + 1.
92
void consume(const iterator& i)
93
{
94
m_currentFirstArg = i.index() + 1;
95
}
96
+ // Sets the start of the unconsumed range to i: the next begin() returns i.
97
+ // Use this when a parser stopped at an unconsumed token (e.g. options-only
98
+ // parsing that stopped on the first positional / subcommand token).
99
+ void consumeUntil(const iterator& i)
100
+ {
101
+ m_currentFirstArg = i.index();
102
+ }
103
104
private:
105
std::vector<std::wstring> m_args;
src/windows/wslc/core/Main.cpp
+34
-16
@@ -18,6 +18,7 @@ Abstract:
18
#include "wslutil.h"
19
#include "Errors.h"
20
#include "CLIExecutionContext.h"
21
+#include "EnvironmentOptions.h"
22
#include "Invocation.h"
23
#include "RootCommand.h"
24
@@ -32,7 +33,6 @@ try
33
EnableContextualizedErrors(false, true);
34
HRESULT result = S_OK;
35
35
- // Initialize runtime and COM.
36
wslutil::ConfigureCrt();
37
wslutil::InitializeWil();
38
@@ -43,14 +43,11 @@ try
43
auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
44
wslutil::CoInitializeSecurity();
45
46
- // The execution context must be declared after COM is initialized because it stores internal
47
- // COM references.
46
+ // Must be declared after COM init; it holds COM references.
47
CLIExecutionContext context;
48
50
- // Register a console control handler so Ctrl-C signals the cancel event.
51
- // This allows long-running operations (e.g. image build) to be cancelled.
52
- // The static pointer is required because SetConsoleCtrlHandler only accepts function pointers.
53
- // CancelEvent starts null; when a task creates it, the handler picks it up automatically.
49
+ // SetConsoleCtrlHandler only accepts plain function pointers, so route Ctrl-C
50
+ // through a static reference into the context.
51
static auto& s_cancelEvent = context.CancelEvent;
52
auto ctrlHandler = [](DWORD ctrlType) -> BOOL {
53
if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT)
@@ -72,6 +69,16 @@ try
69
70
std::unique_ptr<Command> command = std::make_unique<RootCommand>();
71
72
+ // Environment variable scanning.
73
+ // The env-bound argument set is the only state needed before NO_COLOR is
74
+ // applied; keep just this and the noexcept env apply outside the try so a
75
+ // throw can't reroute through the colored-help error path.
76
+ auto envDefs = command->GetGlobalsAndEnvArguments();
77
+ ApplyEnvironmentOptions(context.GlobalArgs, envDefs);
78
+ context.ApplyGlobalOptions();
79
+
80
+ // Past this point, environment variable options are in effect.
81
+
82
try
83
{
84
std::vector<std::wstring> args;
@@ -81,6 +88,25 @@ try
88
}
89
90
Invocation invocation{std::move(args)};
91
+
92
+ // Pass 1 — CLI globals. Consume only the global options we recognize at
93
+ // the front of the invocation; anything else (subcommands, unknown
94
+ // options, --help, --version, malformed tokens) is left in place for
95
+ // the regular pipeline to parse and report against the right command.
96
+ auto cliGlobals = command->GetGlobalArguments();
97
+ command->ParseArguments(
98
+ invocation,
99
+ context.GlobalArgs,
100
+ cliGlobals,
101
+ /*optionsOnly*/ true,
102
+ /*stopOnUnknown*/ true,
103
+ /*overridableDefaults*/ envDefs);
104
+ command->ValidateArguments(context.GlobalArgs, envDefs, /*runInternalHook*/ false);
105
+ context.ApplyGlobalOptions();
106
+
107
+ // Past this point, global options are in effect.
108
+
109
+ // Pass 2 - Subcommand and leaf command resolution.
110
std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
111
while (subCommand)
112
{
@@ -92,24 +118,17 @@ try
118
command->ValidateArguments(context.Args);
119
command->Execute(context);
120
}
95
- // Exceptions specific to parsing the arguments of a command
121
catch (const CommandException& ce)
122
{
98
- // A command exception means there was an input failure. Display the help
99
- // along with the error message to help the user correct their input.
123
+ // Input failure: show help alongside the error so the user can correct it.
124
command->OutputHelp(&ce);
125
return 1;
126
}
103
- // Any other type of error unrelated to the command parsing.
127
catch (...)
128
{
129
LOG_CAUGHT_EXCEPTION();
107
-
108
- // Using WSL shared utility to get the HRESULT from the caught exception.
109
- // CLIExecutionContext is a derived class of wsl::windows::common::ExecutionContext.
130
result = wil::ResultFromCaughtException();
131
112
- // If the user pressed Ctrl-C, acknowledge the cancellation and exit.
132
if (context.CancelEvent && context.CancelEvent.is_signaled())
133
{
134
fwprintf(stderr, L"\nCancelled.\n");
@@ -126,7 +145,6 @@ try
145
}
146
else
147
{
129
- // Fallback for errors without context
148
wslutil::PrintMessage(Localization::MessageErrorCode("", wslutil::ErrorCodeToString(result)), stderr);
149
}
150
}
src/windows/wslc/tasks/SessionTasks.cpp
+4
-4
@@ -40,10 +40,10 @@ void AttachToSession(CLIExecutionContext& context)
40
41
void CreateSession(CLIExecutionContext& context)
42
{
43
- if (context.Args.Contains(ArgType::Session))
43
+ if (context.GlobalArgs.Contains(ArgType::Session))
44
{
45
// User specified a session name — open only, don't create.
46
- const auto& sessionName = context.Args.Get<ArgType::Session>();
46
+ const auto& sessionName = context.GlobalArgs.Get<ArgType::Session>();
47
context.Data.Add<Data::Session>(SessionService::OpenSession(sessionName));
48
return;
49
}
@@ -90,9 +90,9 @@ void TerminateSession(CLIExecutionContext& context)
90
void RunInSession(CLIExecutionContext& context)
91
{
92
std::wstring sessionName;
93
- if (context.Args.Contains(ArgType::Session))
93
+ if (context.GlobalArgs.Contains(ArgType::Session))
94
{
95
- sessionName = context.Args.Get<ArgType::Session>();
95
+ sessionName = context.GlobalArgs.Get<ArgType::Session>();
96
}
97
98
std::vector<std::string> arguments;
test/windows/wslc/CommandLineTestCases.h
+14
-5
@@ -25,6 +25,15 @@ COMMAND_LINE_TEST_CASE(L"-?", L"root", true)
25
COMMAND_LINE_TEST_CASE(L"--version", L"root", true)
26
COMMAND_LINE_TEST_CASE(L"-v", L"root", true)
27
28
+// Global options (RootCommand::GetGlobalArguments). These must be accepted by
29
+// the root-level options-only pass before any subcommand is resolved. A
30
+// non-exhaustive sampling — the parser-level matrix lives in ParserTestCases.h.
31
+COMMAND_LINE_TEST_CASE(L"--session foo image list --verbose", L"list", true)
32
+// Cases that fail because the unknown/misplaced option falls through to a
33
+// command that doesn't accept it:
34
+COMMAND_LINE_TEST_CASE(L"--notaglobal system list", L"root", false) // Unknown option falls through to root, which rejects it
35
+COMMAND_LINE_TEST_CASE(L"container list --session foo", L"list", false) // --session is global; must come before the subcommand
36
+
37
// System command tests
38
COMMAND_LINE_TEST_CASE(L"system -?", L"system", true)
39
COMMAND_LINE_TEST_CASE(L"system session list", L"list", true)
@@ -37,11 +46,11 @@ COMMAND_LINE_TEST_CASE(L"system session shell", L"shell", true)
46
COMMAND_LINE_TEST_CASE(L"system session run ls", L"run", true)
47
COMMAND_LINE_TEST_CASE(L"system session run echo foo", L"run", true) // Command with trailing arguments
48
COMMAND_LINE_TEST_CASE(L"system session run ls -la", L"run", true) // Flags after the command are forwarded
40
-COMMAND_LINE_TEST_CASE(L"system session run --session session1 ls", L"run", true)
41
-COMMAND_LINE_TEST_CASE(L"system session run --session session1 echo foo", L"run", true)
49
+COMMAND_LINE_TEST_CASE(L"--session session1 system session run ls", L"run", true)
50
+COMMAND_LINE_TEST_CASE(L"--session session1 system session run echo foo", L"run", true)
51
COMMAND_LINE_TEST_CASE(L"system session run \"ls -la /tmp\"", L"run", true)
52
COMMAND_LINE_TEST_CASE(L"system session run", L"run", false) // Missing required command positional
44
-COMMAND_LINE_TEST_CASE(L"system session run --session session1", L"run", false) // Missing required command positional
53
+COMMAND_LINE_TEST_CASE(L"--session session1 system session run", L"run", false) // Missing required command positional
54
COMMAND_LINE_TEST_CASE(L"system session run --notanarg ls", L"run", false) // Invalid flag before command
55
COMMAND_LINE_TEST_CASE(L"system session terminate session1", L"terminate", true)
56
COMMAND_LINE_TEST_CASE(L"system session terminate", L"terminate", true)
@@ -60,13 +69,13 @@ COMMAND_LINE_TEST_CASE(L"list", L"list", true)
69
COMMAND_LINE_TEST_CASE(L"ls", L"list", true)
70
COMMAND_LINE_TEST_CASE(L"ps", L"list", true)
71
COMMAND_LINE_TEST_CASE(L"container list --no-trunc", L"list", true)
63
-COMMAND_LINE_TEST_CASE(L"container list --session foo", L"list", true)
72
+COMMAND_LINE_TEST_CASE(L"--session foo container list", L"list", true)
73
COMMAND_LINE_TEST_CASE(L"container list -qa", L"list", true)
74
COMMAND_LINE_TEST_CASE(L"container list --format json", L"list", true)
75
COMMAND_LINE_TEST_CASE(L"container list --format table", L"list", true)
76
COMMAND_LINE_TEST_CASE(L"container list --format badformat", L"list", false)
77
COMMAND_LINE_TEST_CASE(L"container prune", L"prune", true)
69
-COMMAND_LINE_TEST_CASE(L"container prune --session foo", L"prune", true)
78
+COMMAND_LINE_TEST_CASE(L"--session foo container prune", L"prune", true)
79
COMMAND_LINE_TEST_CASE(L"run ubuntu", L"run", true)
80
COMMAND_LINE_TEST_CASE(L"run --rm -it --entrypoint bash archlinux:latest -c \"echo 123\"", L"run", true)
81
COMMAND_LINE_TEST_CASE(L"run --rm --entrypoint /bin/bash debian:latest -c ls", L"run", true)
test/windows/wslc/ParserTestCases.h
+52
-2
@@ -24,6 +24,8 @@ enum class ArgumentSet
24
{
25
Run,
26
List,
27
+ // RootCommand globals; parsed in optionsOnly mode (stops at first positional).
28
+ Globals,
29
};
30
31
// ParserTestCase - represents a single test case
@@ -34,6 +36,14 @@ struct ParserTestCase
36
std::wstring commandLine;
37
};
38
39
+// True for argument sets that mirror the root-level "global options" parsing
40
+// pass in Main.cpp, which uses optionsOnly=true so the parser stops at the
41
+// first positional / subcommand token without consuming it.
42
+inline bool IsOptionsOnlySet(ArgumentSet argumentSet)
43
+{
44
+ return argumentSet == ArgumentSet::Globals;
45
+}
46
+
47
// Function to get the argument definitions for a given ArgumentSet
48
inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet argumentSet)
49
{
@@ -63,6 +73,18 @@ inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet
73
Argument::Create(ArgType::Verbose),
74
};
75
76
+ case ArgumentSet::Globals:
77
+ // Synthetic stand-in for what Main.cpp passes as cliGlobals to the
78
+ // first (optionsOnly) parse pass. Decoupled from RootCommand so the
79
+ // parser tests stay stable as the production global set evolves.
80
+ // Quiet (Flag with alias) and Session (Value, no alias) are convenient
81
+ // existing ArgTypes that together exercise both kinds in the global
82
+ // parsing path; the cases below treat them as "the global options".
83
+ return {
84
+ Argument::Create(ArgType::Quiet),
85
+ Argument::Create(ArgType::Session),
86
+ };
87
+
88
default:
89
return {};
90
}
@@ -86,7 +108,7 @@ WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 image1)") \
108
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 image1)") \
109
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 -p 443:443 image1)") \
110
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 -p=443:443 image1)") \
89
-WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --verbose --verbose image1)") \
111
+WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose --verbose image1)") \
112
\
113
/* Flag parse tests */ \
114
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -? image1)") \
@@ -143,5 +165,33 @@ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -i cont1 cont2)") \
165
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -vp cont1)") \
166
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 -v cont2 -12)") \
167
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=false cont2)") \
146
-WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)")
168
+WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)") \
169
+\
170
+/* Root-level globals: strict optionsOnly parsing. Stops cleanly at the first \
171
+ * non-option token; recognized globals before that are consumed. Production \
172
+ * uses an additional stopOnUnknown bool that is covered separately by \
173
+ * OptionsOnly_StopOnUnknown_LeavesTokenForCaller. The Globals set is a \
174
+ * synthetic mix of Quiet (Flag) and Session (Value) — see GetArgumentsForSet \
175
+ * — so these cases test the parser, not RootCommand's current global set. */ \
176
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc)") \
177
+/* Flag-kind global: long, short, and stops at positional/subcommand. */ \
178
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet)") \
179
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc -q)") \
180
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet image1)") \
181
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc -q image1)") \
182
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc image1)") \
183
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet system list)") \
184
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc system --verbose)") \
185
+/* Value-kind global: separated and adjoined value forms, then positional. */ \
186
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session foo)") \
187
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session=foo)") \
188
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session foo image1)") \
189
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session=foo image1)") \
190
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session foo system list)") \
191
+/* Mixed Flag + Value globals before the first positional. */ \
192
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet --session foo image1)") \
193
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session foo -q image1)") \
194
+/* Docker-style idempotency: duplicate global flags collapse to a single entry. */ \
195
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet --quiet)") \
196
+WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc -q -q system list)")
197
// clang-format on
test/windows/wslc/WSLCCLICommandUnitTests.cpp
+48
@@ -24,6 +24,7 @@ Abstract:
24
#include "SessionCommand.h"
25
#include "SystemCommand.h"
26
#include "VersionCommand.h"
27
+#include "EnvironmentOptions.h"
28
29
using namespace wsl::windows::wslc;
30
using namespace WSLCTestHelpers;
@@ -189,6 +190,53 @@ class WSLCCLICommandUnitTests
190
VERIFY_IS_TRUE(found, L"RootCommand should contain VersionCommand");
191
}
192
193
+ // RootCommand exposes Session as the sole CLI global option. The override
194
+ // is the entry point for future globals; the test pins the current shape.
195
+ TEST_METHOD(RootCommand_GlobalArguments_OnlySession)
196
+ {
197
+ auto root = RootCommand();
198
+ auto globals = root.GetGlobalArguments();
199
+
200
+ VERIFY_ARE_EQUAL(1u, globals.size());
201
+ VERIFY_ARE_EQUAL(ArgType::Session, globals[0].Type());
202
+ VERIFY_ARE_EQUAL(Kind::Value, globals[0].Kind());
203
+ }
204
+
205
+ // RootCommand exposes NoColor as the sole env-eligible global option.
206
+ TEST_METHOD(RootCommand_EnvArguments_OnlyNoColor)
207
+ {
208
+ auto root = RootCommand();
209
+ auto envArgs = root.GetEnvArguments();
210
+
211
+ VERIFY_ARE_EQUAL(1u, envArgs.size());
212
+ VERIFY_ARE_EQUAL(ArgType::NoColor, envArgs[0].Type());
213
+ VERIFY_ARE_EQUAL(Kind::Flag, envArgs[0].Kind());
214
+ }
215
+
216
+ // Every ArgType advertised by GetEnvArguments() must have at least one entry
217
+ // in c_envBindings; otherwise ApplyEnvironmentOptions() has nothing to apply
218
+ // and help output would lie about env support.
219
+ TEST_METHOD(RootCommand_EnvArguments_AllHaveBindings)
220
+ {
221
+ auto root = RootCommand();
222
+ auto envArgs = root.GetEnvArguments();
223
+
224
+ for (const auto& a : envArgs)
225
+ {
226
+ bool found = false;
227
+ for (const auto& b : c_envBindings)
228
+ {
229
+ if (b.Type == a.Type())
230
+ {
231
+ found = true;
232
+ break;
233
+ }
234
+ }
235
+
236
+ VERIFY_IS_TRUE(found, std::format(L"ArgType {} has no env binding", static_cast<size_t>(a.Type())).c_str());
237
+ }
238
+ }
239
+
240
// Walk every command in the root tree and verify no argument collisions.
241
TEST_METHOD(AllCommands_NoAmbiguousArgumentNamesOrAliases)
242
{
test/windows/wslc/WSLCCLIEnvironmentOptionsUnitTests.cpp
new
+160
@@ -0,0 +1,160 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ WSLCCLIEnvironmentOptionsUnitTests.cpp
8
+
9
+Abstract:
10
+
11
+ Unit tests for Environment Options.
12
+
13
+--*/
14
+
15
+#include "precomp.h"
16
+#include "windows/Common.h"
17
+#include "WSLCCLITestHelpers.h"
18
+
19
+#include "Argument.h"
20
+#include "ArgumentTypes.h"
21
+#include "EnvironmentOptions.h"
22
+
23
+using namespace wsl::windows::wslc;
24
+using namespace wsl::windows::wslc::argument;
25
+
26
+using namespace WSLCTestHelpers;
27
+using namespace WEX::Logging;
28
+using namespace WEX::Common;
29
+using namespace WEX::TestExecution;
30
+
31
+namespace WSLCCLIEnvironmentOptionsUnitTests {
32
+
33
+class WSLCCLIEnvironmentOptionsUnitTests
34
+{
35
+ WSLC_TEST_CLASS(WSLCCLIEnvironmentOptionsUnitTests)
36
+
37
+ // Tests touch process-wide env state. Capture pre-existing values in setup
38
+ // and restore them in cleanup so the suite is hermetic and doesn't clobber
39
+ // values the test host (or CI) may have set.
40
+ TEST_METHOD_SETUP(TestMethodSetup)
41
+ {
42
+ m_savedNoColor = CaptureEnv(L"NO_COLOR");
43
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", nullptr));
44
+ return true;
45
+ }
46
+
47
+ TEST_METHOD_CLEANUP(TestMethodCleanup)
48
+ {
49
+ RestoreEnv(L"NO_COLOR", m_savedNoColor);
50
+ return true;
51
+ }
52
+
53
+ TEST_METHOD(ApplyEnvironmentOptions_NoColorEmptyValue_SetsFlag)
54
+ {
55
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", L""));
56
+
57
+ ArgMap target;
58
+ ApplyEnvironmentOptions(target, NoColorDefs());
59
+
60
+ VERIFY_IS_TRUE(target.Contains(ArgType::NoColor));
61
+ VERIFY_IS_TRUE(target.Get<ArgType::NoColor>());
62
+ }
63
+
64
+ // NO_COLOR spec: "0" / "false" / "no" / "off" are not opt-outs.
65
+ TEST_METHOD(ApplyEnvironmentOptions_NoColorFalsyLikeValues_StillSetFlag)
66
+ {
67
+ for (const auto* value : {L"0", L"false", L"FALSE", L"no", L"off"})
68
+ {
69
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", value));
70
+
71
+ ArgMap target;
72
+ ApplyEnvironmentOptions(target, NoColorDefs());
73
+
74
+ LogComment(std::wstring(L"NO_COLOR=") + value);
75
+ VERIFY_IS_TRUE(target.Contains(ArgType::NoColor));
76
+ VERIFY_IS_TRUE(target.Get<ArgType::NoColor>());
77
+
78
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", nullptr));
79
+ }
80
+ }
81
+
82
+ TEST_METHOD(ApplyEnvironmentOptions_NoColorArbitraryValue_SetsFlag)
83
+ {
84
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", L"1"));
85
+
86
+ ArgMap target;
87
+ ApplyEnvironmentOptions(target, NoColorDefs());
88
+
89
+ VERIFY_IS_TRUE(target.Contains(ArgType::NoColor));
90
+ VERIFY_IS_TRUE(target.Get<ArgType::NoColor>());
91
+ }
92
+
93
+ TEST_METHOD(ApplyEnvironmentOptions_NoColorAbsent_DoesNotSetFlag)
94
+ {
95
+ ArgMap target;
96
+ ApplyEnvironmentOptions(target, NoColorDefs());
97
+
98
+ VERIFY_IS_FALSE(target.Contains(ArgType::NoColor));
99
+ }
100
+
101
+ // Env-derived defaults are lowest precedence and must not overwrite.
102
+ TEST_METHOD(ApplyEnvironmentOptions_TargetAlreadyContainsArg_LeavesItUntouched)
103
+ {
104
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", L""));
105
+
106
+ ArgMap target;
107
+ target.Add<ArgType::NoColor>(false);
108
+
109
+ ApplyEnvironmentOptions(target, NoColorDefs());
110
+
111
+ VERIFY_ARE_EQUAL(1U, target.Count(ArgType::NoColor));
112
+ VERIFY_IS_FALSE(target.Get<ArgType::NoColor>());
113
+ }
114
+
115
+ // Bindings outside definedArgs are ignored even if the env var is set.
116
+ // Verbose isn't bound to any env var and isn't NoColor, so it stays a
117
+ // valid "declared but unrelated" stand-in: declaring it alone must not
118
+ // cause NO_COLOR to leak into target.
119
+ TEST_METHOD(ApplyEnvironmentOptions_UndeclaredArg_IsIgnored)
120
+ {
121
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(L"NO_COLOR", L""));
122
+
123
+ std::vector<Argument> defs;
124
+ defs.push_back(Argument::Create(ArgType::Verbose));
125
+
126
+ ArgMap target;
127
+ ApplyEnvironmentOptions(target, defs);
128
+
129
+ VERIFY_IS_FALSE(target.Contains(ArgType::NoColor));
130
+ }
131
+
132
+private:
133
+ std::optional<std::wstring> m_savedNoColor;
134
+
135
+ static std::vector<Argument> NoColorDefs()
136
+ {
137
+ std::vector<Argument> defs;
138
+ defs.push_back(Argument::Create(ArgType::NoColor));
139
+ return defs;
140
+ }
141
+
142
+ // Snapshot a process env var. nullopt means the variable was not defined;
143
+ // an empty string means it was defined as "".
144
+ static std::optional<std::wstring> CaptureEnv(const wchar_t* name)
145
+ {
146
+ std::wstring value;
147
+ if (FAILED(wil::GetEnvironmentVariableW(name, value)))
148
+ {
149
+ return std::nullopt;
150
+ }
151
+ return value;
152
+ }
153
+
154
+ static void RestoreEnv(const wchar_t* name, const std::optional<std::wstring>& saved)
155
+ {
156
+ VERIFY_IS_TRUE(SetEnvironmentVariableW(name, saved.has_value() ? saved->c_str() : nullptr));
157
+ }
158
+};
159
+
160
+} // namespace WSLCCLIEnvironmentOptionsUnitTests
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+23
-3
@@ -452,6 +452,13 @@ class WSLCCLIExecutionUnitTests
452
// found and the provided command line parsed correctly according to the command's defined arguments,
453
// and the argument validation rules are correctly applied. The test cases are defined in
454
// CommandLineTestCases.h and cover various valid and invalid command lines.
455
+ //
456
+ // Mirrors CoreMain's pipeline:
457
+ // 1. Globals scan (optionsOnly + stopOnUnknown): consume recognized
458
+ // globals, leave everything else in place. Env apply is intentionally
459
+ // skipped so test behavior is not affected by the host environment.
460
+ // 2. Subcommand resolution.
461
+ // 3. Leaf command parse + validate.
462
TEST_METHOD(CommandLineParsing_AllCases)
463
{
464
std::vector<CommandLineTestCase> testCases = {
@@ -483,6 +490,21 @@ class WSLCCLIExecutionUnitTests
490
{
491
Invocation invocation{std::move(args)};
492
std::unique_ptr<Command> command = std::make_unique<RootCommand>();
493
+ const Command* const rootCommand = command.get();
494
+
495
+ // Pass 1: globals scan. Lenient on unknowns so non-global tokens
496
+ // (subcommands, root options, errors) flow to subsequent passes.
497
+ CLIExecutionContext context;
498
+ const auto cliGlobals = rootCommand->GetGlobalArguments();
499
+ rootCommand->ParseArguments(
500
+ invocation,
501
+ context.GlobalArgs,
502
+ cliGlobals,
503
+ /*optionsOnly*/ true,
504
+ /*stopOnUnknown*/ true);
505
+ rootCommand->ValidateArguments(context.GlobalArgs, cliGlobals, /*runInternalHook*/ false);
506
+
507
+ // Pass 2: walk down to the leaf subcommand.
508
std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
509
while (subCommand)
510
{
@@ -493,9 +515,7 @@ class WSLCCLIExecutionUnitTests
515
// Ensure we found the expected command
516
VERIFY_ARE_EQUAL(testCase.expectedCommand, command->Name());
517
496
- CLIExecutionContext context;
497
-
498
- // Parse and validate and compare to expected results.
518
+ // Pass 3: leaf parse + validate.
519
command->ParseArguments(invocation, context.Args);
520
command->ValidateArguments(context.Args);
521
}
test/windows/wslc/WSLCCLIParserUnitTests.cpp
+394
-9
@@ -48,7 +48,6 @@ class WSLCCLIParserUnitTests
48
49
TEST_METHOD(ParserTest_ParserCases)
50
{
51
- // Build test cases from x-macro
51
std::vector<ParserTestCase> testCases = {
52
#define WSLC_PARSER_TEST_CASE(argSetValue, expected, cmdLine) {ArgumentSet::argSetValue, expected, cmdLine},
53
WSLC_PARSER_TEST_CASES
@@ -58,29 +57,35 @@ class WSLCCLIParserUnitTests
57
for (const auto& testCase : testCases)
58
{
59
bool succeeded = false;
60
+ const bool optionsOnly = IsOptionsOnlySet(testCase.argumentSet);
61
62
try
63
{
64
Log::Comment(String().Format(L"Testing: %ls", testCase.commandLine.c_str()));
65
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(testCase.commandLine);
66
67
- // Get argument definitions from the helper function
67
std::vector<Argument> definedArgs = GetArgumentsForSet(testCase.argumentSet);
68
69
ArgMap args;
71
- ParseArgumentsStateMachine stateMachine{inv, args, std::move(definedArgs)};
70
+ ParseArgumentsStateMachine stateMachine{inv, args, std::move(definedArgs), optionsOnly};
71
while (stateMachine.Step())
72
{
73
stateMachine.ThrowIfError();
74
}
75
+ // Step() returns false on stop in optionsOnly mode without surfacing any
76
+ // pending error, so drain once more to convert "missing value at EOF"
77
+ // into a thrown ArgumentException.
78
+ stateMachine.ThrowIfError();
79
77
- // Validate count limits and required arguments, mirroring Command::ValidateArguments.
78
- // Skip all validation if --help is present, as Command::ValidateArguments does.
80
if (!args.Contains(ArgType::Help))
81
{
82
for (const auto& arg : GetArgumentsForSet(testCase.argumentSet))
83
{
83
- if (arg.Required() && !args.Contains(arg.Type()))
84
+ // Required-arg enforcement is a *whole-command-line* concern.
85
+ // In optionsOnly mode the parser only saw part of the input, so
86
+ // missing values would be reported by the second (subcommand)
87
+ // pass, not this one. Skip required checks for those sets.
88
+ if (!optionsOnly && arg.Required() && !args.Contains(arg.Type()))
89
{
90
throw ArgumentException(std::wstring(L"Required argument missing: ") + arg.Name());
91
}
@@ -115,18 +120,17 @@ class WSLCCLIParserUnitTests
120
121
if (testCase.commandLine.find(L"--rm") != std::wstring::npos)
122
{
118
- // Ensure '--rm' was parsed wherever it was found.
123
VERIFY_IS_TRUE(args.Contains(ArgType::Remove));
124
}
125
122
- if (testCase.commandLine.find(L"command") != std::wstring::npos)
126
+ if (testCase.commandLine.find(L"command") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
127
{
128
VERIFY_IS_TRUE(args.Contains(ArgType::Command));
129
auto command = args.Get<ArgType::Command>();
130
VERIFY_IS_TRUE(command.find(L"command") != std::wstring::npos);
131
}
132
129
- if (testCase.commandLine.find(L"forward") != std::wstring::npos)
133
+ if (testCase.commandLine.find(L"forward") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
134
{
135
VERIFY_IS_TRUE(args.Contains(ArgType::ForwardArgs));
136
auto forwardArgs = args.Get<ArgType::ForwardArgs>();
@@ -144,6 +148,25 @@ class WSLCCLIParserUnitTests
148
VERIFY_ARE_EQUAL(2, publishArgs.size()); // Should have both publish args
149
VERIFY_ARE_NOT_EQUAL(publishArgs[0], publishArgs[1]); // Both publish args should be different
150
}
151
+
152
+ // Globals-specific spot checks: the synthetic global flag
153
+ // (--quiet / -q) and value (--session, see GetArgumentsForSet)
154
+ // must have been consumed by the global parser when present,
155
+ // and the stop position must point at the first non-global
156
+ // token (or end()).
157
+ if (testCase.argumentSet == ArgumentSet::Globals)
158
+ {
159
+ if (testCase.commandLine.find(L"--quiet") != std::wstring::npos || testCase.commandLine.find(L"-q") != std::wstring::npos)
160
+ {
161
+ VERIFY_IS_TRUE(args.Contains(ArgType::Quiet));
162
+ }
163
+
164
+ if (testCase.commandLine.find(L"--session") != std::wstring::npos)
165
+ {
166
+ VERIFY_IS_TRUE(args.Contains(ArgType::Session));
167
+ VERIFY_ARE_EQUAL(std::wstring(L"foo"), args.Get<ArgType::Session>());
168
+ }
169
+ }
170
}
171
catch (ArgumentException& ex)
172
{
@@ -171,5 +194,367 @@ class WSLCCLIParserUnitTests
194
VERIFY_ARE_EQUAL(testCase.expectedResult, succeeded, String().Format(L"Command line: %ls", testCase.commandLine.c_str()));
195
}
196
}
197
+
198
+ // Options-only mode: parser consumes leading options and stops at the first
199
+ // positional without consuming it. inv must advance past consumed options.
200
+ TEST_METHOD(OptionsOnly_StopsAtFirstPositional)
201
+ {
202
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose image1 command");
203
+
204
+ std::vector<Argument> defs = {
205
+ Argument::Create(ArgType::Verbose),
206
+ Argument::Create(ArgType::NoColor),
207
+ };
208
+
209
+ ArgMap args;
210
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
211
+ while (sm.Step())
212
+ {
213
+ sm.ThrowIfError();
214
+ }
215
+
216
+ VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
217
+ VERIFY_IS_FALSE(args.Contains(ArgType::NoColor));
218
+
219
+ // Position points at the first positional ("image1") so the next pass
220
+ // can resume from there via Invocation::consumeUntil.
221
+ auto pos = sm.Position();
222
+ VERIFY_IS_TRUE(pos != inv.end());
223
+ VERIFY_ARE_EQUAL(std::wstring(L"image1"), *pos);
224
+ }
225
+
226
+ TEST_METHOD(OptionsOnly_NoOptionsPresent_StopsImmediately)
227
+ {
228
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc image1");
229
+
230
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
231
+
232
+ ArgMap args;
233
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
234
+ while (sm.Step())
235
+ {
236
+ sm.ThrowIfError();
237
+ }
238
+
239
+ VERIFY_IS_FALSE(args.Contains(ArgType::Verbose));
240
+ VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
241
+ }
242
+
243
+ TEST_METHOD(OptionsOnly_OnlyOptions_PositionEqualsEnd)
244
+ {
245
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose");
246
+
247
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
248
+
249
+ ArgMap args;
250
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
251
+ while (sm.Step())
252
+ {
253
+ sm.ThrowIfError();
254
+ }
255
+
256
+ VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
257
+ VERIFY_IS_TRUE(sm.Position() == inv.end());
258
+ }
259
+
260
+ TEST_METHOD(OptionsOnly_AdjoinedValue_IsConsumed)
261
+ {
262
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal=9 image1");
263
+
264
+ std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
265
+
266
+ ArgMap args;
267
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
268
+ while (sm.Step())
269
+ {
270
+ sm.ThrowIfError();
271
+ }
272
+
273
+ VERIFY_IS_TRUE(args.Contains(ArgType::Signal));
274
+ VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
275
+ VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
276
+ }
277
+
278
+ TEST_METHOD(OptionsOnly_SeparatedValue_IsConsumed)
279
+ {
280
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 image1");
281
+
282
+ std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
283
+
284
+ ArgMap args;
285
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
286
+ while (sm.Step())
287
+ {
288
+ sm.ThrowIfError();
289
+ }
290
+
291
+ VERIFY_IS_TRUE(args.Contains(ArgType::Signal));
292
+ VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
293
+ VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
294
+ }
295
+
296
+ TEST_METHOD(OptionsOnly_UnknownOption_Throws)
297
+ {
298
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --doesnotexist image1");
299
+
300
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
301
+
302
+ ArgMap args;
303
+ bool threw = false;
304
+ try
305
+ {
306
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
307
+ while (sm.Step())
308
+ {
309
+ sm.ThrowIfError();
310
+ }
311
+ }
312
+ catch (const ArgumentException&)
313
+ {
314
+ threw = true;
315
+ }
316
+
317
+ VERIFY_IS_TRUE(threw);
318
+ }
319
+
320
+ TEST_METHOD(OptionsOnly_ValueAtEndOfInput_Throws)
321
+ {
322
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal");
323
+
324
+ std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
325
+
326
+ ArgMap args;
327
+ bool threw = false;
328
+ try
329
+ {
330
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
331
+ while (sm.Step())
332
+ {
333
+ sm.ThrowIfError();
334
+ }
335
+ sm.ThrowIfError();
336
+ }
337
+ catch (const ArgumentException&)
338
+ {
339
+ threw = true;
340
+ }
341
+
342
+ VERIFY_IS_TRUE(threw);
343
+ }
344
+
345
+ // After options-only stops, Invocation::consumeUntil(Position()) hands the
346
+ // remaining tokens to a second parse pass — exactly what Main.cpp does.
347
+ TEST_METHOD(OptionsOnly_TwoPassParseAcrossPositional)
348
+ {
349
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose image1 --signal 9");
350
+
351
+ // Pass 1: options-only with just Verbose visible.
352
+ std::vector<Argument> globalDefs = {Argument::Create(ArgType::Verbose)};
353
+ ArgMap globals;
354
+ ParseArgumentsStateMachine sm1{inv, globals, std::move(globalDefs), /*optionsOnly*/ true};
355
+ while (sm1.Step())
356
+ {
357
+ sm1.ThrowIfError();
358
+ }
359
+ inv.consumeUntil(sm1.Position());
360
+
361
+ VERIFY_IS_TRUE(globals.Contains(ArgType::Verbose));
362
+ VERIFY_IS_FALSE(globals.Contains(ArgType::Signal));
363
+
364
+ // Pass 2: full mode with the subcommand's argument set.
365
+ std::vector<Argument> subDefs = {
366
+ Argument::Create(ArgType::ImageId, true),
367
+ Argument::Create(ArgType::Signal),
368
+ };
369
+ ArgMap subArgs;
370
+ ParseArgumentsStateMachine sm2{inv, subArgs, std::move(subDefs), /*optionsOnly*/ false};
371
+ while (sm2.Step())
372
+ {
373
+ sm2.ThrowIfError();
374
+ }
375
+
376
+ VERIFY_IS_TRUE(subArgs.Contains(ArgType::ImageId));
377
+ VERIFY_ARE_EQUAL(std::wstring(L"image1"), subArgs.Get<ArgType::ImageId>());
378
+ VERIFY_IS_TRUE(subArgs.Contains(ArgType::Signal));
379
+ VERIFY_ARE_EQUAL(std::wstring(L"9"), subArgs.Get<ArgType::Signal>());
380
+ }
381
+
382
+ // stopOnUnknown: unknown -alias / --name / lone '-' / bare '--' tokens
383
+ // back the iterator up and stop cleanly instead of throwing. Recognized
384
+ // options before the unknown one are still consumed.
385
+ TEST_METHOD(OptionsOnly_StopOnUnknown_LeavesTokenForCaller)
386
+ {
387
+ // Case 1: leading unknown --name. Nothing consumed, position == begin.
388
+ {
389
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --doesnotexist image1");
390
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
391
+
392
+ ArgMap args;
393
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
394
+ while (sm.Step())
395
+ {
396
+ sm.ThrowIfError();
397
+ }
398
+ sm.ThrowIfError();
399
+
400
+ VERIFY_IS_FALSE(args.Contains(ArgType::Verbose));
401
+ VERIFY_ARE_EQUAL(std::wstring(L"--doesnotexist"), *sm.Position());
402
+ }
403
+
404
+ // Case 2: leading unknown -alias. Same backing-up behavior.
405
+ {
406
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc -X image1");
407
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
408
+
409
+ ArgMap args;
410
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
411
+ while (sm.Step())
412
+ {
413
+ sm.ThrowIfError();
414
+ }
415
+ sm.ThrowIfError();
416
+
417
+ VERIFY_IS_FALSE(args.Contains(ArgType::Verbose));
418
+ VERIFY_ARE_EQUAL(std::wstring(L"-X"), *sm.Position());
419
+ }
420
+
421
+ // Case 3: recognized option consumed, then unknown stops the scan.
422
+ {
423
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose --doesnotexist image1");
424
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
425
+
426
+ ArgMap args;
427
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
428
+ while (sm.Step())
429
+ {
430
+ sm.ThrowIfError();
431
+ }
432
+ sm.ThrowIfError();
433
+
434
+ VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
435
+ VERIFY_ARE_EQUAL(std::wstring(L"--doesnotexist"), *sm.Position());
436
+ }
437
+
438
+ // Case 4: lone '-' with no positionals defined backs up instead of erroring.
439
+ {
440
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc -");
441
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
442
+
443
+ ArgMap args;
444
+ ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
445
+ while (sm.Step())
446
+ {
447
+ sm.ThrowIfError();
448
+ }
449
+ sm.ThrowIfError();
450
+
451
+ VERIFY_ARE_EQUAL(std::wstring(L"-"), *sm.Position());
452
+ }
453
+ }
454
+
455
+ // Preloaded env-style default can be replaced by a single CLI occurrence
456
+ // even though the arg's Limit is 1.
457
+ TEST_METHOD(OverridableDefaults_CliValueReplacesPreload)
458
+ {
459
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9");
460
+
461
+ std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
462
+
463
+ ArgMap args;
464
+ args.Add(ArgType::Signal, std::wstring(L"15")); // pretend env preloaded SIGTERM
465
+
466
+ ParseArgumentsStateMachine sm{inv, args, defs, /*optionsOnly*/ false, /*stopOnUnknown*/ false, /*overridableDefaults*/ defs};
467
+ while (sm.Step())
468
+ {
469
+ sm.ThrowIfError();
470
+ }
471
+ sm.ThrowIfError();
472
+
473
+ VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
474
+ VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
475
+ }
476
+
477
+ // Overridable-default consumption is one-shot: a CLI duplicate after the
478
+ // override still stacks and would trip Limit during Validate().
479
+ TEST_METHOD(OverridableDefaults_OverrideIsConsumedOncePerType)
480
+ {
481
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 --signal 1");
482
+
483
+ std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
484
+
485
+ ArgMap args;
486
+ args.Add(ArgType::Signal, std::wstring(L"15"));
487
+
488
+ ParseArgumentsStateMachine sm{inv, args, defs, /*optionsOnly*/ false, /*stopOnUnknown*/ false, /*overridableDefaults*/ defs};
489
+ while (sm.Step())
490
+ {
491
+ sm.ThrowIfError();
492
+ }
493
+ sm.ThrowIfError();
494
+
495
+ // First CLI value replaced the env preload; second CLI value stacked.
496
+ VERIFY_ARE_EQUAL(2u, args.Count(ArgType::Signal));
497
+ }
498
+
499
+ // Preloaded flag default plus CLI mention of the same flag stays a single
500
+ // entry (current flag value is always 'true').
501
+ TEST_METHOD(OverridableDefaults_FlagPreloadCoexistsWithCli)
502
+ {
503
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose");
504
+
505
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
506
+
507
+ ArgMap args;
508
+ args.Add(ArgType::Verbose, true); // pretend env preloaded it
509
+
510
+ ParseArgumentsStateMachine sm{inv, args, defs, /*optionsOnly*/ false, /*stopOnUnknown*/ false, /*overridableDefaults*/ defs};
511
+ while (sm.Step())
512
+ {
513
+ sm.ThrowIfError();
514
+ }
515
+ sm.ThrowIfError();
516
+
517
+ VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
518
+ VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
519
+ }
520
+
521
+ // Duplicate flag on the CLI (no env preload) folds to one entry: docker-style.
522
+ TEST_METHOD(DuplicateFlagOnCli_IsIdempotent)
523
+ {
524
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose --verbose");
525
+
526
+ std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
527
+
528
+ ArgMap args;
529
+ ParseArgumentsStateMachine sm{inv, args, defs};
530
+ while (sm.Step())
531
+ {
532
+ sm.ThrowIfError();
533
+ }
534
+ sm.ThrowIfError();
535
+
536
+ VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
537
+ VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
538
+ }
539
+
540
+ // Duplicate value on the CLI (no override) still stacks so Validate can
541
+ // catch the Limit violation.
542
+ TEST_METHOD(DuplicateValueOnCli_StillStacks)
543
+ {
544
+ auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 --signal 1");
545
+
546
+ std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
547
+
548
+ ArgMap args;
549
+ ParseArgumentsStateMachine sm{inv, args, defs};
550
+ while (sm.Step())
551
+ {
552
+ sm.ThrowIfError();
553
+ }
554
+ sm.ThrowIfError();
555
+
556
+ VERIFY_ARE_EQUAL(2u, args.Count(ArgType::Signal));
557
+ }
558
};
559
+
560
} // namespace WSLCCLIParserUnitTests
test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp
+1
-2
@@ -155,8 +155,7 @@ private:
155
std::wstring GetAvailableOptions() const
156
{
157
std::wstringstream options;
158
- options << L"The following options are available:\r\n" //
159
- << L" --session Specify the session to use\r\n" //
158
+ options << L"The following options are available:\r\n" //
159
<< L" -?,--help Shows help about the selected command\r\n"
160
<< L"\r\n";
161
return options.str();
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
-1
@@ -1188,7 +1188,6 @@ private:
1188
<< L" -p,--publish Publish a port from a container to host\r\n"
1189
<< L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1190
<< L" --rm Remove the container after it stops\r\n"
1191
- << L" --session Specify the session to use\r\n"
1191
<< L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1192
<< L" --stop-signal Signal to stop the container\r\n"
1193
<< L" --tmpfs Mount tmpfs to the container at the given path\r\n"
test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
-1
@@ -484,7 +484,6 @@ private:
484
<< L" -e,--env Key=Value pairs for environment variables\r\n"
485
<< L" --env-file File containing key=value pairs of env variables\r\n"
486
<< L" -i,--interactive Attach to stdin and keep it open\r\n"
487
- << L" --session Specify the session to use\r\n"
487
<< L" -t,--tty Open a TTY with the container process.\r\n"
488
<< L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
489
<< L" -w,--workdir Working directory inside the container\r\n"
test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp
-1
@@ -139,7 +139,6 @@ private:
139
std::wstringstream options;
140
options << L"The following options are available:\r\n" //
141
<< L" -o,--output Write to a file, instead of STDOUT\r\n" //
142
- << L" --session Specify the session to use\r\n" //
142
<< L" -?,--help Shows help about the selected command\r\n" //
143
<< L"\r\n";
144
return options.str();
test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp
-1
@@ -151,7 +151,6 @@ private:
151
{
152
std::wstringstream options;
153
options << L"The following options are available:\r\n" //
154
- << L" --session Specify the session to use\r\n" //
154
<< L" -?,--help Shows help about the selected command\r\n" //
155
<< L"\r\n";
156
return options.str();
test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp
-1
@@ -178,7 +178,6 @@ private:
178
{
179
std::wstringstream options;
180
options << L"The following options are available:\r\n"
181
- << L" --session Specify the session to use\r\n"
181
<< L" -s,--signal Signal to send\r\n"
182
<< L" -?,--help Shows help about the selected command\r\n"
183
<< L"\r\n";
test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp
-1
@@ -527,7 +527,6 @@ private:
527
<< L" -l,--latest " << Localization::WSLCCLI_LatestArgDescription() << L"\r\n"
528
<< L" --no-trunc Do not truncate output\r\n"
529
<< L" -q,--quiet Outputs the container IDs only\r\n"
530
- << L" --session Specify the session to use\r\n"
530
<< L" -?,--help Shows help about the selected command\r\n"
531
<< L"\r\n";
532
return options.str();
test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
-1
@@ -148,7 +148,6 @@ private:
148
{
149
std::wstringstream options;
150
options << L"The following options are available:\r\n"
151
- << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
151
<< L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
152
<< L"\r\n";
153
return options.str();
test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
-1
@@ -200,7 +200,6 @@ private:
200
std::wstringstream options;
201
options << L"The following options are available:\r\n" //
202
<< L" -f,--force Delete containers even if they are running\r\n"
203
- << L" --session Specify the session to use\r\n"
203
<< L" -?,--help Shows help about the selected command\r\n"
204
<< L"\r\n";
205
return options.str();
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
-1
@@ -1030,7 +1030,6 @@ private:
1030
<< L" -p,--publish Publish a port from a container to host\r\n"
1031
<< L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1032
<< L" --rm Remove the container after it stops\r\n"
1033
- << L" --session Specify the session to use\r\n"
1033
<< L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1034
<< L" --stop-signal Signal to stop the container\r\n"
1035
<< L" --tmpfs Mount tmpfs to the container at the given path\r\n"
test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp
-1
@@ -293,7 +293,6 @@ private:
293
{
294
std::wstringstream options;
295
options << L"The following options are available:\r\n"
296
- << L" --session Specify the session to use\r\n"
296
<< L" -s,--signal Signal to send\r\n"
297
<< L" -t,--time Time in seconds to wait before executing (default 5)\r\n"
298
<< L" -?,--help Shows help about the selected command\r\n"
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+9
-9
@@ -116,7 +116,7 @@ class WSLCE2EGlobalTests
116
117
// Try to explicitly target the admin session from non-elevated process
118
auto adminName = GetExpectedDefaultSessionName(true);
119
- result = RunWslc(std::format(L"container list --session {}", adminName), ElevationType::NonElevated);
119
+ result = RunWslc(std::format(L"--session {} container list", adminName), ElevationType::NonElevated);
120
121
// Should fail with access denied.
122
result.Verify({.Stderr = L"The requested operation requires elevation. \r\nError code: ERROR_ELEVATION_REQUIRED\r\n", .ExitCode = 1});
@@ -130,7 +130,7 @@ class WSLCE2EGlobalTests
130
131
// Elevated user should be able to explicitly target the non-admin session
132
auto nonAdminName = GetExpectedDefaultSessionName(false);
133
- result = RunWslc(std::format(L"container list --session {}", nonAdminName), ElevationType::Elevated);
133
+ result = RunWslc(std::format(L"--session {} container list", nonAdminName), ElevationType::Elevated);
134
135
// This should work - elevated users can access non-elevated sessions
136
result.Verify({.Stderr = L"", .ExitCode = 0});
@@ -144,11 +144,11 @@ class WSLCE2EGlobalTests
144
// Ensure elevated cannot create the non-elevated session.
145
auto nonAdminName = GetExpectedDefaultSessionName(false);
146
auto adminName = GetExpectedDefaultSessionName(true);
147
- auto result = RunWslc(std::format(L"container list --session {}", nonAdminName), ElevationType::Elevated);
147
+ auto result = RunWslc(std::format(L"--session {} container list", nonAdminName), ElevationType::Elevated);
148
result.Verify({.Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
149
150
// Ensure non-elevated cannot create the elevated session.
151
- result = RunWslc(std::format(L"container list --session {}", adminName), ElevationType::NonElevated);
151
+ result = RunWslc(std::format(L"--session {} container list", adminName), ElevationType::NonElevated);
152
result.Verify({.Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
153
}
154
@@ -378,7 +378,7 @@ class WSLCE2EGlobalTests
378
EnsureImageIsLoaded(DebianTestImage(), session.Name());
379
380
// Verify targeting a non-existent session fails.
381
- auto result = RunWslc(L"container list --session INVALID_SESSION_NAME");
381
+ auto result = RunWslc(L"--session INVALID_SESSION_NAME container list");
382
result.Verify({.Stdout = L"", .Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
383
384
// Verify session list
@@ -391,12 +391,12 @@ class WSLCE2EGlobalTests
391
VERIFY_ARE_NOT_EQUAL(findResult, std::wstring::npos);
392
393
// Run container list in the test session, which should succeed if the session is valid.
394
- result = RunWslc(std::format(L"container list --session {}", session.Name()));
394
+ result = RunWslc(std::format(L"--session {} container list", session.Name()));
395
result.Verify({.Stderr = L"", .ExitCode = 0});
396
397
// Add a container to the new session.
398
result = RunWslc(
399
- std::format(L"container create --session {} --name {} {}", session.Name(), L"test-cont", DebianTestImage().NameAndTag()));
399
+ std::format(L"--session {} container create --name {} {}", session.Name(), L"test-cont", DebianTestImage().NameAndTag()));
400
result.Dump(); // Dump so it is easier to find any potential issues with the pull in the test output.
401
result.Verify({.ExitCode = 0});
402
@@ -500,12 +500,12 @@ class WSLCE2EGlobalTests
500
}
501
502
{
503
- auto result = RunWslc(std::format(L"system session run --session {} echo OK", GetExpectedDefaultSessionName(true)));
503
+ auto result = RunWslc(std::format(L"--session {} system session run echo OK", GetExpectedDefaultSessionName(true)));
504
result.Verify({.Stdout = L"OK\n", .Stderr = L"", .ExitCode = 0});
505
}
506
507
{
508
- auto result = RunWslc(L"system session run --session not-found echo OK");
508
+ auto result = RunWslc(L"--session not-found system session run echo OK");
509
result.Verify({.Stdout = L"", .Stderr = L"Session not found: 'not-found'\r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
510
}
511
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+3
-3
@@ -145,7 +145,7 @@ void VerifyContainerIsListed(const std::wstring& containerNameOrId, const std::w
145
std::wstring command = L"container list --no-trunc --all";
146
if (!sessionName.empty())
147
{
148
- command = std::format(L"container list --no-trunc --all --session {}", sessionName);
148
+ command = std::format(L"--session {} container list --no-trunc --all", sessionName);
149
}
150
151
auto result = RunWslc(command);
@@ -379,7 +379,7 @@ void EnsureImageIsLoaded(const TestImage& image, const std::wstring& sessionName
379
std::wstring listCommand = L"image list -q";
380
if (!sessionName.empty())
381
{
382
- listCommand = std::format(L"image list -q --session \"{}\"", sessionName);
382
+ listCommand = std::format(L"--session \"{}\" image list -q", sessionName);
383
}
384
385
auto result = RunWslc(listCommand);
@@ -398,7 +398,7 @@ void EnsureImageIsLoaded(const TestImage& image, const std::wstring& sessionName
398
std::wstring loadCommand = std::format(L"image load --input \"{}\"", image.Path.wstring());
399
if (!sessionName.empty())
400
{
401
- loadCommand = std::format(L"image load --input \"{}\" --session \"{}\"", image.Path.wstring(), sessionName);
401
+ loadCommand = std::format(L"--session \"{}\" image load --input \"{}\"", sessionName, image.Path.wstring());
402
}
403
404
auto loadResult = RunWslc(loadCommand);
test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp
-1
@@ -193,7 +193,6 @@ private:
193
options << L"The following options are available:\r\n" //
194
<< L" -f,--force Delete images even if they are being used\r\n" //
195
<< L" --no-prune Do not delete untagged parents\r\n" //
196
- << L" --session Specify the session to use\r\n" //
196
<< L" -?,--help Shows help about the selected command\r\n" //
197
<< L"\r\n";
198
return options.str();
test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
+2
-3
@@ -137,9 +137,8 @@ private:
137
std::wstring GetAvailableOptions() const
138
{
139
std::wstringstream options;
140
- options << L"The following options are available:\r\n" //
141
- << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" //
142
- << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" //
140
+ options << L"The following options are available:\r\n" //
141
+ << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" //
142
<< L"\r\n";
143
return options.str();
144
}
test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
-1
@@ -105,7 +105,6 @@ private:
105
{
106
std::wstringstream options;
107
options << L"The following options are available:\r\n" //
108
- << L" --session Specify the session to use\r\n" //
108
<< L" -?,--help Shows help about the selected command\r\n" //
109
<< L"\r\n";
110
return options.str();
test/windows/wslc/e2e/WSLCE2EImageListTests.cpp
-1
@@ -315,7 +315,6 @@ private:
315
<< L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
316
<< L" --no-trunc Do not truncate output\r\n"
317
<< L" -q,--quiet Outputs the container IDs only\r\n"
318
- << L" --session Specify the session to use\r\n"
318
<< L" --verbose Output verbose details\r\n"
319
<< L" -?,--help Shows help about the selected command\r\n"
320
<< L"\r\n";
test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
-1
@@ -214,7 +214,6 @@ private:
214
options << L"The following options are available:\r\n"
215
<< L" -a,--all " << Localization::WSLCCLI_ImagePruneAllArgDescription() << L"\r\n"
216
<< L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
217
- << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
217
<< L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
218
<< L"\r\n";
219
return options.str();
test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
-1
@@ -212,7 +212,6 @@ private:
212
std::wstringstream options;
213
options << L"The following options are available:\r\n" //
214
<< L" -o,--output Path for the saved image\r\n" //
215
- << L" --session Specify the session to use\r\n" //
215
<< L" -?,--help Shows help about the selected command\r\n" //
216
<< L"\r\n";
217
return options.str();
test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp
-1
@@ -203,7 +203,6 @@ private:
203
{
204
std::wstringstream options;
205
options << L"The following options are available:\r\n" //
206
- << L" --session Specify the session to use\r\n" //
206
<< L" -?,--help Shows help about the selected command\r\n" //
207
<< L"\r\n";
208
return options.str();
test/windows/wslc/e2e/WSLCE2EInspectTests.cpp
-1
@@ -346,7 +346,6 @@ private:
346
std::wstringstream options;
347
options << L"The following options are available:\r\n" //
348
<< L" -t,--type Type of the object to inspect\r\n" //
349
- << L" --session Specify the session to use\r\n" //
349
<< L" -?,--help Shows help about the selected command\r\n" //
350
<< L"\r\n";
351
return options.str();
test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp
-1
@@ -152,7 +152,6 @@ private:
152
<< L" -d,--driver Specify network driver name (default: bridge)\r\n" //
153
<< L" -o,--opt Set driver specific options\r\n" //
154
<< L" -l,--label Network metadata setting\r\n" //
155
- << L" --session Specify the session to use\r\n" //
155
<< L" -?,--help Shows help about the selected command\r\n" //
156
<< L"\r\n";
157
return options.str();
test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp
-1
@@ -148,7 +148,6 @@ private:
148
{
149
std::wstringstream options;
150
options << L"The following options are available:\r\n" //
151
- << L" --session Specify the session to use\r\n" //
151
<< L" -?,--help Shows help about the selected command\r\n" //
152
<< L"\r\n";
153
return options.str();
test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp
-1
@@ -125,7 +125,6 @@ private:
125
options << L"The following options are available:\r\n" //
126
<< L" --format Output formatting (json or table) (Default: table)\r\n" //
127
<< L" -q,--quiet Outputs the network names only\r\n" //
128
- << L" --session Specify the session to use\r\n" //
128
<< L" -?,--help Shows help about the selected command\r\n" //
129
<< L"\r\n";
130
return options.str();
test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp
-1
@@ -143,7 +143,6 @@ private:
143
{
144
std::wstringstream options;
145
options << L"The following options are available:\r\n" //
146
- << L" --session Specify the session to use\r\n" //
146
<< L" -?,--help Shows help about the selected command\r\n" //
147
<< L"\r\n";
148
return options.str();
test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp
-1
@@ -172,7 +172,6 @@ private:
172
{
173
std::wstringstream options;
174
options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
175
- << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
175
<< L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
176
<< L"\r\n";
177
return options.str();
test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp
-1
@@ -274,7 +274,6 @@ private:
274
<< L" -p,--password " << Localization::WSLCCLI_LoginPasswordArgDescription() << L"\r\n"
275
<< L" --password-stdin " << Localization::WSLCCLI_LoginPasswordStdinArgDescription() << L"\r\n"
276
<< L" -u,--username " << Localization::WSLCCLI_LoginUsernameArgDescription() << L"\r\n"
277
- << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
277
<< L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
278
<< L"\r\n";
279
return options.str();
test/windows/wslc/e2e/WSLCE2ETlsRegistryTests.cpp
+4
-4
@@ -227,9 +227,9 @@ class WSLCE2ETlsRegistryTests
227
auto [registry, address] = StartLocalRegistry(session.Session(), "", "", c_registryPort, certDir.wstring());
228
VERIFY_ARE_EQUAL(std::format("{}:{}", c_registryIp, c_registryPort), address);
229
230
- RunWslcAndVerify(std::format(L"image tag {} {} --session {}", image.NameAndTag(), registryImage, session.Name()), {.ExitCode = 0});
230
+ RunWslcAndVerify(std::format(L"--session {} image tag {} {}", session.Name(), image.NameAndTag(), registryImage), {.ExitCode = 0});
231
232
- auto result = RunWslc(std::format(L"push {} --session {}", registryImage, session.Name()));
232
+ auto result = RunWslc(std::format(L"--session {} push {}", session.Name(), registryImage));
233
VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0), L"Push should fail while the CA is not trusted");
234
VERIFY_IS_TRUE(result.Stderr.has_value());
235
VERIFY_IS_TRUE(
@@ -251,9 +251,9 @@ class WSLCE2ETlsRegistryTests
251
auto [registry, address] = StartLocalRegistry(session.Session(), "", "", c_registryPort, certDir.wstring());
252
VERIFY_ARE_EQUAL(std::format("{}:{}", c_registryIp, c_registryPort), address);
253
254
- RunWslcAndVerify(std::format(L"image tag {} {} --session {}", image.NameAndTag(), registryImage, session.Name()), {.ExitCode = 0});
254
+ RunWslcAndVerify(std::format(L"--session {} image tag {} {}", session.Name(), image.NameAndTag(), registryImage), {.ExitCode = 0});
255
256
- auto result = RunWslc(std::format(L"push {} --session {}", registryImage, session.Name()));
256
+ auto result = RunWslc(std::format(L"--session {} push {}", session.Name(), registryImage));
257
VERIFY_ARE_EQUAL(0u, result.ExitCode.value_or(1), L"Push should succeed once the CA is trusted");
258
}
259
}
test/windows/wslc/e2e/WSLCE2EVolumeCreateTests.cpp
-1
@@ -149,7 +149,6 @@ private:
149
<< L" -d,--driver Specify volume driver name, e.g. 'guest' or 'vhd' (default: guest)\r\n" //
150
<< L" -o,--opt Set driver specific options\r\n" //
151
<< L" -l,--label Set metadata on an object\r\n" //
152
- << L" --session Specify the session to use\r\n" //
152
<< L" -?,--help Shows help about the selected command\r\n" //
153
<< L"\r\n";
154
return options.str();
test/windows/wslc/e2e/WSLCE2EVolumeInspectTests.cpp
-1
@@ -156,7 +156,6 @@ private:
156
{
157
std::wstringstream options;
158
options << L"The following options are available:\r\n" //
159
- << L" --session Specify the session to use\r\n" //
159
<< L" -?,--help Shows help about the selected command\r\n" //
160
<< L"\r\n";
161
return options.str();
test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp
-1
@@ -127,7 +127,6 @@ private:
127
options << L"The following options are available:\r\n" //
128
<< L" --format Output formatting (json or table) (Default: table)\r\n"
129
<< L" -q,--quiet Outputs the volume names only\r\n" //
130
- << L" --session Specify the session to use\r\n" //
130
<< L" -?,--help Shows help about the selected command\r\n" //
131
<< L"\r\n";
132
return options.str();
test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp
-1
@@ -264,7 +264,6 @@ private:
264
options << L"The following options are available:\r\n"
265
<< L" -a,--all " << Localization::WSLCCLI_VolumePruneAllArgDescription() << L"\r\n"
266
<< L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
267
- << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
267
<< L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
268
<< L"\r\n";
269
return options.str();
test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp
-1
@@ -177,7 +177,6 @@ private:
177
{
178
std::wstringstream options;
179
options << L"The following options are available:\r\n" //
180
- << L" --session Specify the session to use\r\n" //
180
<< L" -?,--help Shows help about the selected command\r\n" //
181
<< L"\r\n";
182
return options.str();