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();