@samitouri / QOSAMI-WSL / commits / 00d10afa

CLI: Align WSLC argument parser with Docker flag and value semantics (#41160)

David Bennett committed Jul 24, 2026 at 16:14 UTC 00d10afa8cdd50a2b67c9c550cb56cb7fccc6238
44 files changed +558 -206
localization/strings/en-US/Resources.resw
-4
@@ -2159,10 +2159,6 @@ Usage:
2159 <value>Required argument not provided: '{}'</value>
2160 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2161 </data>
2162 - <data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
2163 - <value>Argument provided more times than allowed: '{}'</value>
2164 - <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2165 - </data>
2162 <data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
2163 <value>Shows help about the selected command</value>
2164 </data>
src/shared/inc/stringshared.h
+9 -3
@@ -311,8 +311,12 @@ inline bool IsEqual(const std::wstring_view String1, const std::wstring_view Str
311 return (Compare(String1, String2, CaseInsensitive) == String1.size());
312 }
313
314 +// Parses a boolean from a string. By default only "1"/"0" and "true"/"false"
315 +// (case-insensitive) are recognized. When AllowExtendedForms is true the single
316 +// character forms "t"/"f" (case-insensitive) are also accepted, matching the full
317 +// set understood by Go's strconv.ParseBool (and therefore the Docker CLI).
318 template <typename T>
315 -inline std::optional<bool> ParseBool(const T* String)
319 +inline std::optional<bool> ParseBool(const T* String, bool AllowExtendedForms = false)
320 {
321 if (!String)
322 {
@@ -322,14 +326,16 @@ inline std::optional<bool> ParseBool(const T* String)
326 const std::basic_string_view<T> StringView(String);
327 constexpr T One[] = {T('1'), T('\0')};
328 constexpr T True[] = {T('t'), T('r'), T('u'), T('e'), T('\0')};
325 - if (IsEqual(StringView, One) || IsEqual(StringView, True, true))
329 + constexpr T ShortTrue[] = {T('t'), T('\0')};
330 + if (IsEqual(StringView, One) || IsEqual(StringView, True, true) || (AllowExtendedForms && IsEqual(StringView, ShortTrue, true)))
331 {
332 return true;
333 }
334
335 constexpr T Zero[] = {T('0'), T('\0')};
336 constexpr T False[] = {T('f'), T('a'), T('l'), T('s'), T('e'), T('\0')};
332 - if (IsEqual(StringView, Zero) || IsEqual(StringView, False, true))
337 + constexpr T ShortFalse[] = {T('f'), T('\0')};
338 + if (IsEqual(StringView, Zero) || IsEqual(StringView, False, true) || (AllowExtendedForms && IsEqual(StringView, ShortFalse, true)))
339 {
340 return false;
341 }
src/windows/wslc/arguments/Argument.cpp
+2 -2
@@ -36,7 +36,7 @@ using namespace wsl::windows::wslc::execution;
36 // function below inside the command. In this way all arguments default to "1" use and not required, and
37 // this can only be changed in the command's GetArguments function, so the defaults are always clear and
38 // consistent. Visibility can also be overridden and is defaulted to "Help".
39 -Argument Argument::Create(ArgType type, std::optional<bool> required, std::optional<int> countLimit, std::optional<std::wstring> desc)
39 +Argument Argument::Create(ArgType type, std::optional<bool> required, std::optional<argument::Limit> limit, std::optional<std::wstring> desc)
40 {
41 switch (type)
42 {
@@ -49,7 +49,7 @@ Argument Argument::Create(ArgType type, std::optional<bool> required, std::optio
49 desc.has_value() ? std::move(desc.value()) : std::wstring(Desc), \
50 ArgumentKind, \
51 required.value_or(DefaultRequired), \
52 - countLimit.value_or(DefaultCountLimit)};
52 + limit.value_or(DefaultLimit)};
53
54 WSLC_ARGUMENTS(WSLC_ARG_CREATE_CASE)
55 #undef WSLC_ARG_CREATE_CASE
src/windows/wslc/arguments/Argument.h
+19 -8
@@ -22,7 +22,6 @@ Abstract:
22 #define WSLC_CLI_HELP_ARG L"?"
23 #define WSLC_CLI_HELP_ARG_STRING WSLC_CLI_ARG_ID_STRING WSLC_CLI_HELP_ARG
24 #define NO_ALIAS L""
25 -#define NO_LIMIT -1
25
26 using namespace wsl::windows::wslc::argument;
27
@@ -33,7 +32,7 @@ struct Argument
32 // Default argument configuration constants
33 static constexpr Kind DefaultKind = Kind::Flag;
34 static constexpr bool DefaultRequired = false;
36 - static constexpr int DefaultCountLimit = 1;
35 + static constexpr argument::Limit DefaultLimit = argument::Limit::Single;
36
37 // Full constructor with all parameters
38 Argument(
@@ -43,8 +42,8 @@ struct Argument
42 const std::wstring& desc,
43 argument::Kind kind = DefaultKind,
44 bool required = DefaultRequired,
46 - int countLimit = DefaultCountLimit) :
47 - m_argType(argType), m_name(name), m_alias(alias), m_desc(desc), m_type(kind), m_required(required), m_countLimit(countLimit)
45 + argument::Limit limit = DefaultLimit) :
46 + m_argType(argType), m_name(name), m_alias(alias), m_desc(desc), m_type(kind), m_required(required), m_limit(limit)
47 {
48 }
49
@@ -58,7 +57,7 @@ struct Argument
57 static Argument Create(
58 ArgType type,
59 std::optional<bool> required = std::nullopt,
61 - std::optional<int> countLimit = std::nullopt,
60 + std::optional<argument::Limit> limit = std::nullopt,
61 std::optional<std::wstring> desc = std::nullopt);
62
63 // Gets the argument usage string in the format of "-alias,--name" or just "--name" if no alias.
@@ -89,9 +88,21 @@ struct Argument
88 {
89 return m_type;
90 }
92 - int Limit() const
91 + Limit Limit() const
92 {
94 - return m_countLimit;
93 + return m_limit;
94 + }
95 +
96 + // A single-value argument accepts one value (last-wins on repeats).
97 + bool IsSingle() const
98 + {
99 + return m_limit == argument::Limit::Single;
100 + }
101 +
102 + // An unlimited argument accumulates every value supplied.
103 + bool IsUnlimited() const
104 + {
105 + return m_limit == argument::Limit::Unlimited;
106 }
107
108 // Validates this argument's value in the provided args
@@ -104,6 +115,6 @@ private:
115 std::wstring m_alias;
116 bool m_required = DefaultRequired;
117 argument::Kind m_type = DefaultKind;
107 - int m_countLimit = DefaultCountLimit;
118 + argument::Limit m_limit = DefaultLimit;
119 };
120 } // namespace wsl::windows::wslc
src/windows/wslc/arguments/ArgumentParser.cpp
+77 -55
@@ -81,7 +81,9 @@ void ParseArgumentsStateMachine::ThrowIfError() const
81
82 void ParseArgumentsStateMachine::AdvanceToNextPositional(std::vector<Argument>::iterator& itr) const
83 {
84 - while (itr != m_positionalArgs.end() && (m_executionArgs.Count(itr->Type()) == itr->Limit()))
84 + // Skip positionals that are already full. A single-value positional is full once it
85 + // holds one value; an unlimited positional is never full.
86 + while (itr != m_positionalArgs.end() && itr->IsSingle() && m_executionArgs.Count(itr->Type()) >= 1)
87 {
88 ++itr;
89 }
@@ -120,20 +122,79 @@ bool ParseArgumentsStateMachine::ConsumeOverrideIfPresent(ArgType type)
122 return true;
123 }
124
123 -void ParseArgumentsStateMachine::AddFlag(ArgType type)
125 +void ParseArgumentsStateMachine::ClearArgument(ArgType type)
126 {
125 - if (!ConsumeOverrideIfPresent(type) && m_executionArgs.Contains(type))
127 + // Drop any preloaded overridable default and remove previously parsed entries so the
128 + // argument is left absent. This is the single-value/last-wins primitive shared by
129 + // SetFlag (which then stores the flag's explicit value) and AddValue (single-value args).
130 + ConsumeOverrideIfPresent(type);
131 + m_executionArgs.Remove(type);
132 +}
133 +
134 +void ParseArgumentsStateMachine::SetFlag(ArgType type, bool value)
135 +{
136 + // Boolean flags store their explicit parsed value (true or false) so a flag whose behavior
137 + // is on by default can be turned off with "--flag=false". Clearing first collapses CLI
138 + // duplicates to a single entry and gives docker's last-wins behavior for repeated flags
139 + // (e.g. "--flag --flag=false" ends up false). Read flags back via ArgMap::GetFlag, which
140 + // folds the presence check and the stored value into one test, rather than a bare Contains().
141 + ClearArgument(type);
142 + m_executionArgs.Add(type, value);
143 +}
144 +
145 +std::wstring_view ParseArgumentsStateMachine::StripSurroundingQuotes(std::wstring_view value)
146 +{
147 + if (value.length() >= 2 && value.front() == L'"' && value.back() == L'"')
148 {
127 - // Repeating the same flag on the CLI is a no-op, matching docker.
128 - return;
149 + value = value.substr(1, value.length() - 2);
150 + }
151 +
152 + return value;
153 +}
154 +
155 +ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ApplyFlagValue(ArgType type, std::wstring_view value, const std::wstring_view& currArg)
156 +{
157 + const auto unquoted = StripSurroundingQuotes(value);
158 + const auto boolVal = string::ParseBool(std::wstring(unquoted).c_str(), /*AllowExtendedForms*/ true);
159 + if (!boolVal.has_value())
160 + {
161 + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
162 + }
163 +
164 + SetFlag(type, boolVal.value());
165 + return {};
166 +}
167 +
168 +const Argument* ParseArgumentsStateMachine::FindArgument(ArgType type) const
169 +{
170 + for (const auto& arg : m_arguments)
171 + {
172 + if (arg.Type() == type)
173 + {
174 + return &arg;
175 + }
176 }
177
131 - m_executionArgs.Add(type, true);
178 + return nullptr;
179 }
180
181 void ParseArgumentsStateMachine::AddValue(ArgType type, std::wstring value)
182 {
136 - ConsumeOverrideIfPresent(type);
183 + const Argument* arg = FindArgument(type);
184 + WI_ASSERT(arg != nullptr);
185 +
186 + // Unlimited value args accumulate; single-value args are last-wins. In both cases the
187 + // first CLI value must displace a preloaded overridable default, which ClearArgument
188 + // (single) and ConsumeOverrideIfPresent (unlimited) each handle.
189 + if (arg != nullptr && arg->IsUnlimited())
190 + {
191 + ConsumeOverrideIfPresent(type);
192 + }
193 + else
194 + {
195 + ClearArgument(type);
196 + }
197 +
198 m_executionArgs.Add(type, std::move(value));
199 }
200
@@ -234,9 +295,8 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPos
295 WI_ASSERT(m_anchorPositional.has_value());
296
297 // If we haven't reached the limit for the anchor positional, treat this as another anchor positional.
237 - // Anchors with NO_LIMIT will never be full and therefore will always treat subsequent positionals as anchors.
238 - if ((m_executionArgs.Count(m_anchorPositional.value().Type()) < m_anchorPositional.value().Limit()) ||
239 - (m_anchorPositional.value().Limit() == NO_LIMIT))
298 + // Unlimited anchors are never full and therefore always treat subsequent positionals as anchors.
299 + if (m_anchorPositional.value().IsUnlimited() || (m_executionArgs.Count(m_anchorPositional.value().Type()) < 1))
300 {
301 m_executionArgs.Add(m_anchorPositional.value().Type(), std::wstring{currArg});
302 return {};
@@ -345,22 +405,11 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
405 // Boolean flag - check for adjoined boolean value (e.g., -a=true or -a=false).
406 if (currentPos < currArg.length() && currArg[currentPos] == WSLC_CLI_ARG_SPLIT_CHAR)
407 {
348 - auto boolVal = string::ParseBool(std::wstring(currArg.substr(currentPos + 1)).c_str());
349 - if (!boolVal.has_value())
350 - {
351 - return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
352 - }
353 -
354 - if (boolVal.value())
355 - {
356 - AddFlag(firstArg->Type());
357 - }
358 -
359 - return {};
408 + return ApplyFlagValue(firstArg->Type(), currArg.substr(currentPos + 1), currArg);
409 }
410
411 // No adjoined value — add the flag as true.
363 - AddFlag(firstArg->Type());
412 + SetFlag(firstArg->Type(), true);
413
414 // Process remaining adjoined flags
415 while (currentPos < currArg.length())
@@ -398,21 +447,10 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
447 // Boolean flag in chain — check for adjoined boolean value.
448 if (nextPos < currArg.length() && currArg[nextPos] == WSLC_CLI_ARG_SPLIT_CHAR)
449 {
401 - auto boolVal = string::ParseBool(std::wstring(currArg.substr(nextPos + 1)).c_str());
402 - if (!boolVal.has_value())
403 - {
404 - return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
405 - }
406 -
407 - if (boolVal.value())
408 - {
409 - AddFlag(nextArg->Type());
410 - }
411 -
412 - return {};
450 + return ApplyFlagValue(nextArg->Type(), currArg.substr(nextPos + 1), currArg);
451 }
452
415 - AddFlag(nextArg->Type());
453 + SetFlag(nextArg->Type(), true);
454 currentPos = nextPos;
455 }
456
@@ -463,21 +501,10 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume
501 {
502 if (hasAdjoinedValue)
503 {
466 - auto boolVal = string::ParseBool(std::wstring(argValue).c_str());
467 - if (!boolVal.has_value())
468 - {
469 - return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
470 - }
471 -
472 - if (boolVal.value())
473 - {
474 - AddFlag(arg.Type());
475 - }
476 -
477 - return {};
504 + return ApplyFlagValue(arg.Type(), argValue, currArg);
505 }
506
480 - AddFlag(arg.Type());
507 + SetFlag(arg.Type(), true);
508 return {};
509 }
510
@@ -505,11 +532,6 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume
532 void ParseArgumentsStateMachine::ProcessAdjoinedValue(ArgType type, std::wstring_view value)
533 {
534 // If the adjoined value is wrapped in quotes, strip them off.
508 - if (value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"')
509 - {
510 - value = value.substr(1, value.length() - 2);
511 - }
512 -
513 - AddValue(type, std::wstring{value});
535 + AddValue(type, std::wstring{StripSurroundingQuotes(value)});
536 }
537 } // namespace wsl::windows::wslc
src/windows/wslc/arguments/ArgumentParser.h
+39 -13
@@ -34,11 +34,11 @@ struct ParseArgumentsStateMachine
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.
37 + // clears the preexisting entry first, so a preloaded
38 + // default is replaced rather than appended to. Single-value
39 + // args are last-wins regardless, so a later CLI duplicate
40 + // simply overwrites; unlimited args accumulate once the
41 + // preloaded default has been dropped.
42 ParseArgumentsStateMachine(
43 Invocation& inv,
44 ArgMap& execArgs,
@@ -120,21 +120,47 @@ private:
120 State ProcessNamedArgument(const std::wstring_view& currArg);
121 void ProcessAdjoinedValue(ArgType type, std::wstring_view value);
122
123 + // Strips a single pair of surrounding double quotes from an adjoined value if present
124 + // (e.g. --name="value" or --flag="true"). Shared by the value and flag adjoined-value
125 + // paths so both treat quoted "=value" tokens identically.
126 + static std::wstring_view StripSurroundingQuotes(std::wstring_view value);
127 +
128 void AdvanceToNextPositional(std::vector<Argument>::iterator& itr) const;
129
130 // Backs up one token and stops cleanly so Position() points at the unconsumed token.
131 State BackUpAndStop();
132
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.
133 + // Sets a boolean flag by storing its explicit parsed value (true or false). Clearing first
134 + // collapses CLI duplicates to a single entry, so a repeated flag is docker-style last-wins
135 + // (e.g. "--flag --flag=false" ends up false) and a duplicate "--flag --flag" folds to one
136 + // entry. Consumers read the flag with ArgMap::GetFlag (Contains ? stored value : default),
137 + // which lets a flag default to on and be disabled with "--flag=false".
138 + void SetFlag(ArgType type, bool value);
139 +
140 + // Parses an adjoined boolean token for a flag (e.g. the "false" in "--flag=false" or
141 + // "-f=false"). A single pair of surrounding double quotes is stripped first (so
142 + // "--flag=\"true\"" works like the value path), then the token is parsed as a Docker-style
143 + // boolean (true/false/1/0/t/f, case-insensitive) and applied via SetFlag. Returns an error
144 + // State if the token is not a recognized boolean. Shared by the alias, alias-chain, and
145 + // named-flag paths so all three treat "=value" identically.
146 + State ApplyFlagValue(ArgType type, std::wstring_view value, const std::wstring_view& currArg);
147 +
148 + // Removes all entries for an argument and consumes any overridable-default slot,
149 + // leaving the argument absent. This is the single-value (last-wins) primitive that
150 + // SetFlag builds on; it is written to be reused for other single-value argument
151 + // kinds in the future.
152 + void ClearArgument(ArgType type);
153 +
154 + // Stores a value for a Kind::Value argument. Single-value args are last-wins
155 + // (any previous value, including a preloaded overridable default, is cleared
156 + // first); unlimited args accumulate but still let the first CLI value replace a
157 + // preloaded default.
158 void AddValue(ArgType type, std::wstring value);
159
160 + // Returns the defined argument for a type, or nullptr if it is not one of this
161 + // parser's arguments. Used to consult an argument's Limit while parsing values.
162 + const Argument* FindArgument(ArgType type) const;
163 +
164 // If type is in m_overridableDefaults, removes any existing entry and
165 // consumes the override slot. Returns true if an override was consumed.
166 bool ConsumeOverrideIfPresent(ArgType type);
src/windows/wslc/arguments/ArgumentTypes.h
+29
@@ -37,6 +37,18 @@ enum class Kind
37 Forward,
38 };
39
40 +// How many times an argument may be supplied on a command line.
41 +enum class Limit
42 +{
43 + // Accepts a single value. Repeats are last-wins (docker-style): a later occurrence
44 + // overwrites the earlier one rather than being an error. For flags, the stored value
45 + // is the last one parsed, so "--flag --flag=false" ends up false.
46 + Single,
47 +
48 + // Accepts any number of values, which accumulate (e.g. --publish, --env).
49 + Unlimited,
50 +};
51 +
52 // Generate ArgType enum from X-macro
53 enum class ArgType : size_t
54 {
@@ -98,6 +110,23 @@ namespace details {
110 // This is the main ArgType map used for storing parsed arguments.
111 struct ArgMap : wsl::windows::wslc::EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping>
112 {
113 + // Reads a boolean (Kind::Flag) argument's effective value in one call. A flag stores its
114 + // explicit parsed value when specified (docker-style "--flag"/"--flag=true" => true,
115 + // "--flag=false" => false) and is absent when not specified. Prefer this over a bare
116 + // Contains() for flags: Contains() only tells you the flag was seen, while GetFlag() folds
117 + // the presence check and the stored value into a single "is this flag effectively on?" test.
118 + //
119 + // if (args.GetFlag<ArgType::Quiet>()) { ... } // default-off flag
120 + // bool removeOnExit = args.GetFlag<ArgType::Remove>(true); // default-on flag; --rm=false disables
121 + //
122 + // defaultValue is returned when the flag was not specified; pass true for flags whose
123 + // behavior is on by default and must be turned off with "--flag=false".
124 + template <ArgType E>
125 + bool GetFlag(bool defaultValue = false) const
126 + {
127 + static_assert(std::is_same_v<mapping_t<E>, bool>, "GetFlag is only valid for Kind::Flag arguments");
128 + return Contains(E) ? Get<E>() : defaultValue;
129 + }
130 };
131
132 } // namespace wsl::windows::wslc::argument
src/windows/wslc/commands/ContainerCreateCommand.cpp
+12 -12
@@ -33,14 +33,14 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
33 Argument::Create(ArgType::ForwardArgs),
34 Argument::Create(ArgType::CIDFile),
35 Argument::Create(ArgType::Cpus),
36 - Argument::Create(ArgType::DNS, false, NO_LIMIT),
36 + Argument::Create(ArgType::DNS, false, Limit::Unlimited),
37 // Argument::Create(ArgType::DNSDomain),
38 - Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
39 - Argument::Create(ArgType::DNSSearch, false, NO_LIMIT),
38 + Argument::Create(ArgType::DNSOption, false, Limit::Unlimited),
39 + Argument::Create(ArgType::DNSSearch, false, Limit::Unlimited),
40 Argument::Create(ArgType::Domainname),
41 Argument::Create(ArgType::Entrypoint),
42 - Argument::Create(ArgType::Env, false, NO_LIMIT),
43 - Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
42 + Argument::Create(ArgType::Env, false, Limit::Unlimited),
43 + Argument::Create(ArgType::EnvFile, false, Limit::Unlimited),
44 // Argument::Create(ArgType::GroupId),
45 Argument::Create(ArgType::Gpus),
46 Argument::Create(ArgType::HealthCmd),
@@ -50,26 +50,26 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
50 Argument::Create(ArgType::HealthTimeout),
51 Argument::Create(ArgType::Hostname),
52 Argument::Create(ArgType::Interactive),
53 - Argument::Create(ArgType::Label, false, NO_LIMIT),
53 + Argument::Create(ArgType::Label, false, Limit::Unlimited),
54 Argument::Create(ArgType::Memory),
55 Argument::Create(ArgType::Name),
56 - Argument::Create(ArgType::Network, false, NO_LIMIT),
57 - Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
56 + Argument::Create(ArgType::Network, false, Limit::Unlimited),
57 + Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
58 // Argument::Create(ArgType::NoDNS),
59 // Argument::Create(ArgType::Progress),
60 Argument::Create(ArgType::NoHealthcheck),
61 - Argument::Create(ArgType::Publish, false, NO_LIMIT),
61 + Argument::Create(ArgType::Publish, false, Limit::Unlimited),
62 Argument::Create(ArgType::PublishAll),
63 Argument::Create(ArgType::Remove),
64 // Argument::Create(ArgType::Scheme),
65 Argument::Create(ArgType::ShmSize),
66 Argument::Create(ArgType::StopSignal),
67 Argument::Create(ArgType::StopTimeout),
68 - Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
68 + Argument::Create(ArgType::TMPFS, false, Limit::Unlimited),
69 Argument::Create(ArgType::TTY),
70 - Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
70 + Argument::Create(ArgType::Ulimit, false, Limit::Unlimited),
71 Argument::Create(ArgType::User),
72 - Argument::Create(ArgType::Volume, false, NO_LIMIT),
72 + Argument::Create(ArgType::Volume, false, Limit::Unlimited),
73 // Argument::Create(ArgType::Virtual),
74 Argument::Create(ArgType::WorkDir),
75 };
src/windows/wslc/commands/ContainerExecCommand.cpp
+2 -2
@@ -31,8 +31,8 @@ std::vector<Argument> ContainerExecCommand::GetArguments() const
31 Argument::Create(ArgType::Command, true),
32 Argument::Create(ArgType::ForwardArgs, std::nullopt, std::nullopt, Localization::WSLCCLI_ContainerExecForwardArgsDescription()),
33 Argument::Create(ArgType::Detach),
34 - Argument::Create(ArgType::Env, false, NO_LIMIT),
35 - Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
34 + Argument::Create(ArgType::Env, false, Limit::Unlimited),
35 + Argument::Create(ArgType::EnvFile, false, Limit::Unlimited),
36 Argument::Create(ArgType::Interactive),
37 Argument::Create(ArgType::TTY),
38 Argument::Create(ArgType::User),
src/windows/wslc/commands/ContainerInspectCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> ContainerInspectCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
30 + Argument::Create(ArgType::ContainerId, true, Limit::Unlimited),
31 };
32 }
33
src/windows/wslc/commands/ContainerKillCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> ContainerKillCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
30 + Argument::Create(ArgType::ContainerId, true, Limit::Unlimited),
31 Argument::Create(ArgType::Signal),
32 };
33 }
src/windows/wslc/commands/ContainerListCommand.cpp
+2 -2
@@ -29,7 +29,7 @@ std::vector<Argument> ContainerListCommand::GetArguments() const
29 {
30 return {
31 Argument::Create(ArgType::All),
32 - Argument::Create(ArgType::Filter, false, NO_LIMIT),
32 + Argument::Create(ArgType::Filter, false, Limit::Unlimited),
33 Argument::Create(ArgType::Format),
34 Argument::Create(ArgType::Last),
35 Argument::Create(ArgType::Latest),
@@ -60,7 +60,7 @@ void ContainerListCommand::ExecuteInternal(CLIExecutionContext& context) const
60
61 void ContainerListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
62 {
63 - if (execArgs.Contains(ArgType::Last) && execArgs.Contains(ArgType::Latest))
63 + if (execArgs.Contains(ArgType::Last) && execArgs.GetFlag<ArgType::Latest>())
64 {
65 throw CommandException(Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest"));
66 }
src/windows/wslc/commands/ContainerRemoveCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> ContainerRemoveCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
30 + Argument::Create(ArgType::ContainerId, true, Limit::Unlimited),
31 Argument::Create(ArgType::Force),
32 };
33 }
src/windows/wslc/commands/ContainerRunCommand.cpp
+12 -12
@@ -34,14 +34,14 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
34 Argument::Create(ArgType::CIDFile),
35 Argument::Create(ArgType::Cpus),
36 Argument::Create(ArgType::Detach),
37 - Argument::Create(ArgType::DNS, false, NO_LIMIT),
37 + Argument::Create(ArgType::DNS, false, Limit::Unlimited),
38 // Argument::Create(ArgType::DNSDomain),
39 - Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
40 - Argument::Create(ArgType::DNSSearch, false, NO_LIMIT),
39 + Argument::Create(ArgType::DNSOption, false, Limit::Unlimited),
40 + Argument::Create(ArgType::DNSSearch, false, Limit::Unlimited),
41 Argument::Create(ArgType::Domainname),
42 Argument::Create(ArgType::Entrypoint),
43 - Argument::Create(ArgType::Env, false, NO_LIMIT),
44 - Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
43 + Argument::Create(ArgType::Env, false, Limit::Unlimited),
44 + Argument::Create(ArgType::EnvFile, false, Limit::Unlimited),
45 Argument::Create(ArgType::Gpus),
46 Argument::Create(ArgType::HealthCmd),
47 Argument::Create(ArgType::HealthInterval),
@@ -50,15 +50,15 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
50 Argument::Create(ArgType::HealthTimeout),
51 Argument::Create(ArgType::Hostname),
52 Argument::Create(ArgType::Interactive),
53 - Argument::Create(ArgType::Label, false, NO_LIMIT),
53 + Argument::Create(ArgType::Label, false, Limit::Unlimited),
54 Argument::Create(ArgType::Memory),
55 Argument::Create(ArgType::Name),
56 - Argument::Create(ArgType::Network, false, NO_LIMIT),
57 - Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
56 + Argument::Create(ArgType::Network, false, Limit::Unlimited),
57 + Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
58 // Argument::Create(ArgType::NoDNS),
59 // Argument::Create(ArgType::Progress),
60 Argument::Create(ArgType::NoHealthcheck),
61 - Argument::Create(ArgType::Publish, false, NO_LIMIT),
61 + Argument::Create(ArgType::Publish, false, Limit::Unlimited),
62 Argument::Create(ArgType::PublishAll),
63 // Argument::Create(ArgType::Pull),
64 Argument::Create(ArgType::Remove),
@@ -66,11 +66,11 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
66 Argument::Create(ArgType::ShmSize),
67 Argument::Create(ArgType::StopSignal),
68 Argument::Create(ArgType::StopTimeout),
69 - Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
69 + Argument::Create(ArgType::TMPFS, false, Limit::Unlimited),
70 Argument::Create(ArgType::TTY),
71 - Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
71 + Argument::Create(ArgType::Ulimit, false, Limit::Unlimited),
72 Argument::Create(ArgType::User),
73 - Argument::Create(ArgType::Volume, false, NO_LIMIT),
73 + Argument::Create(ArgType::Volume, false, Limit::Unlimited),
74 // Argument::Create(ArgType::Virtual),
75 Argument::Create(ArgType::WorkDir),
76 };
src/windows/wslc/commands/ContainerStatsCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> ContainerStatsCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::ContainerId, false, NO_LIMIT),
30 + Argument::Create(ArgType::ContainerId, false, Limit::Unlimited),
31 Argument::Create(ArgType::All),
32 Argument::Create(ArgType::Format),
33 Argument::Create(ArgType::NoTrunc),
src/windows/wslc/commands/ContainerStopCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> ContainerStopCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::ContainerId, true, NO_LIMIT),
30 + Argument::Create(ArgType::ContainerId, true, Limit::Unlimited),
31 Argument::Create(ArgType::Signal),
32 Argument::Create(ArgType::Time),
33 };
src/windows/wslc/commands/ImageBuildCommand.cpp
+3 -3
@@ -28,13 +28,13 @@ std::vector<Argument> ImageBuildCommand::GetArguments() const
28 {
29 return {
30 Argument::Create(ArgType::Path, true),
31 - Argument::Create(ArgType::BuildArg, false, NO_LIMIT),
31 + Argument::Create(ArgType::BuildArg, false, Limit::Unlimited),
32 Argument::Create(ArgType::BuildPull),
33 Argument::Create(ArgType::BuildTarget),
34 Argument::Create(ArgType::File),
35 - Argument::Create(ArgType::Label, false, NO_LIMIT),
35 + Argument::Create(ArgType::Label, false, Limit::Unlimited),
36 Argument::Create(ArgType::NoCache),
37 - Argument::Create(ArgType::Tag, false, NO_LIMIT),
37 + Argument::Create(ArgType::Tag, false, Limit::Unlimited),
38 Argument::Create(ArgType::Verbose),
39 };
40 }
src/windows/wslc/commands/ImageInspectCommand.cpp
+1 -1
@@ -28,7 +28,7 @@ namespace wsl::windows::wslc {
28 std::vector<Argument> ImageInspectCommand::GetArguments() const
29 {
30 return {
31 - Argument::Create(ArgType::ImageId, true, NO_LIMIT),
31 + Argument::Create(ArgType::ImageId, true, Limit::Unlimited),
32 };
33 }
34
src/windows/wslc/commands/ImageListCommand.cpp
+1 -1
@@ -28,7 +28,7 @@ namespace wsl::windows::wslc {
28 std::vector<Argument> ImageListCommand::GetArguments() const
29 {
30 return {
31 - Argument::Create(ArgType::Filter, false, NO_LIMIT),
31 + Argument::Create(ArgType::Filter, false, Limit::Unlimited),
32 Argument::Create(ArgType::Format),
33 Argument::Create(ArgType::NoTrunc),
34 Argument::Create(ArgType::Quiet),
src/windows/wslc/commands/ImagePruneCommand.cpp
+1 -1
@@ -29,7 +29,7 @@ std::vector<Argument> ImagePruneCommand::GetArguments() const
29 {
30 return {
31 Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_ImagePruneAllArgDescription()),
32 - Argument::Create(ArgType::Filter, false, NO_LIMIT),
32 + Argument::Create(ArgType::Filter, false, Limit::Unlimited),
33 };
34 }
35
src/windows/wslc/commands/ImageRemoveCommand.cpp
+1 -1
@@ -28,7 +28,7 @@ namespace wsl::windows::wslc {
28 std::vector<Argument> ImageRemoveCommand::GetArguments() const
29 {
30 return {
31 - Argument::Create(ArgType::ImageId, true, NO_LIMIT),
31 + Argument::Create(ArgType::ImageId, true, Limit::Unlimited),
32 Argument::Create(ArgType::ImageForce),
33 Argument::Create(ArgType::NoPrune),
34 };
src/windows/wslc/commands/ImageSaveCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> ImageSaveCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::ImageId, true, NO_LIMIT),
30 + Argument::Create(ArgType::ImageId, true, Limit::Unlimited),
31 Argument::Create(ArgType::Output),
32 };
33 }
src/windows/wslc/commands/InspectCommand.cpp
+1 -1
@@ -22,7 +22,7 @@ namespace wsl::windows::wslc {
22 std::vector<Argument> InspectCommand::GetArguments() const
23 {
24 return {
25 - Argument::Create(ArgType::ObjectId, true, NO_LIMIT),
25 + Argument::Create(ArgType::ObjectId, true, Limit::Unlimited),
26 Argument::Create(ArgType::Type),
27 };
28 }
src/windows/wslc/commands/NetworkConnectCommand.cpp
+4 -4
@@ -29,11 +29,11 @@ std::vector<Argument> NetworkConnectCommand::GetArguments() const
29 return {
30 Argument::Create(ArgType::NetworkName, true),
31 Argument::Create(ArgType::ContainerId, true),
32 - Argument::Create(ArgType::DriverOpt, false, NO_LIMIT),
32 + Argument::Create(ArgType::DriverOpt, false, Limit::Unlimited),
33 Argument::Create(ArgType::IpAddress, false),
34 - Argument::Create(ArgType::Link, false, NO_LIMIT),
35 - Argument::Create(ArgType::LinkLocalIp, false, NO_LIMIT),
36 - Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
34 + Argument::Create(ArgType::Link, false, Limit::Unlimited),
35 + Argument::Create(ArgType::LinkLocalIp, false, Limit::Unlimited),
36 + Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
37 };
38 }
39
src/windows/wslc/commands/NetworkCreateCommand.cpp
+2 -2
@@ -29,8 +29,8 @@ std::vector<Argument> NetworkCreateCommand::GetArguments() const
29 return {
30 Argument::Create(ArgType::NetworkName, true),
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()),
32 + Argument::Create(ArgType::Options, false, Limit::Unlimited),
33 + Argument::Create(ArgType::Label, false, Limit::Unlimited, Localization::WSLCCLI_NetworkLabelArgDescription()),
34 Argument::Create(ArgType::Gateway),
35 Argument::Create(ArgType::Internal),
36 Argument::Create(ArgType::Subnet),
src/windows/wslc/commands/NetworkInspectCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> NetworkInspectCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::NetworkName, true, NO_LIMIT),
30 + Argument::Create(ArgType::NetworkName, true, Limit::Unlimited),
31 };
32 }
33
src/windows/wslc/commands/NetworkPruneCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> NetworkPruneCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::Filter, false, NO_LIMIT),
30 + Argument::Create(ArgType::Filter, false, Limit::Unlimited),
31 };
32 }
33
src/windows/wslc/commands/NetworkRemoveCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> NetworkRemoveCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::NetworkName, true, NO_LIMIT),
30 + Argument::Create(ArgType::NetworkName, true, Limit::Unlimited),
31 Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_NetworkForceArgDescription()),
32 };
33 }
src/windows/wslc/commands/RegistryCommand.cpp
+3 -3
@@ -121,12 +121,12 @@ std::wstring RegistryLoginCommand::LongDescription() const
121
122 void RegistryLoginCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
123 {
124 - if (execArgs.Contains(ArgType::Password) && execArgs.Contains(ArgType::PasswordStdin))
124 + if (execArgs.Contains(ArgType::Password) && execArgs.GetFlag<ArgType::PasswordStdin>())
125 {
126 throw CommandException(Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive());
127 }
128
129 - if (execArgs.Contains(ArgType::PasswordStdin) && !execArgs.Contains(ArgType::Username))
129 + if (execArgs.GetFlag<ArgType::PasswordStdin>() && !execArgs.Contains(ArgType::Username))
130 {
131 throw CommandException(Localization::WSLCCLI_LoginPasswordStdinRequiresUsername());
132 }
@@ -143,7 +143,7 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const
143 // Resolve password: --password, --password-stdin, or interactive prompt.
144 if (!context.Args.Contains(ArgType::Password))
145 {
146 - if (context.Args.Contains(ArgType::PasswordStdin))
146 + if (context.Args.GetFlag<ArgType::PasswordStdin>())
147 {
148 std::wstring line;
149 std::getline(std::wcin, line);
src/windows/wslc/commands/RootCommand.cpp
+1 -1
@@ -103,7 +103,7 @@ std::wstring RootCommand::LongDescription() const
103
104 void RootCommand::ExecuteInternal(CLIExecutionContext& context) const
105 {
106 - if (context.Args.Contains(ArgType::Version))
106 + if (context.Args.GetFlag<ArgType::Version>())
107 {
108 VersionCommand::PrintVersion(context.Reporter);
109 return;
src/windows/wslc/commands/VolumeCreateCommand.cpp
+2 -2
@@ -29,8 +29,8 @@ std::vector<Argument> VolumeCreateCommand::GetArguments() const
29 return {
30 Argument::Create(ArgType::VolumeName),
31 Argument::Create(ArgType::Driver),
32 - Argument::Create(ArgType::Options, false, NO_LIMIT),
33 - Argument::Create(ArgType::Label, false, NO_LIMIT),
32 + Argument::Create(ArgType::Options, false, Limit::Unlimited),
33 + Argument::Create(ArgType::Label, false, Limit::Unlimited),
34 };
35 }
36
src/windows/wslc/commands/VolumeInspectCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> VolumeInspectCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
30 + Argument::Create(ArgType::VolumeName, true, Limit::Unlimited),
31 };
32 }
33
src/windows/wslc/commands/VolumePruneCommand.cpp
+1 -1
@@ -28,7 +28,7 @@ std::vector<Argument> VolumePruneCommand::GetArguments() const
28 {
29 return {
30 Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()),
31 - Argument::Create(ArgType::Filter, false, NO_LIMIT),
31 + Argument::Create(ArgType::Filter, false, Limit::Unlimited),
32 };
33 }
34
src/windows/wslc/commands/VolumeRemoveCommand.cpp
+1 -1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> VolumeRemoveCommand::GetArguments() const
28 {
29 return {
30 - Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
30 + Argument::Create(ArgType::VolumeName, true, Limit::Unlimited),
31 Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumeForceArgDescription()),
32 };
33 }
src/windows/wslc/core/CLIExecutionContext.cpp
+1 -1
@@ -19,7 +19,7 @@ HANDLE CLIExecutionContext::CreateCancelEvent()
19 // This method should be idempotent.
20 void CLIExecutionContext::ApplyGlobalOptions()
21 {
22 - if (GlobalArgs.Contains(ArgType::NoColor))
22 + if (GlobalArgs.GetFlag<ArgType::NoColor>())
23 {
24 Reporter.SetNoColor(true);
25 }
src/windows/wslc/core/Command.cpp
+5 -9
@@ -180,7 +180,7 @@ void Command::OutputHelp(Reporter& reporter, const CommandException* exception)
180
181 reporter.Write(
182 helpLevel, L"{}<{}{}{}{}{}>{}", HelpMetaEmphasis, Format::Default, HelpPlaceholderEmphasis, arg.Name(), Format::Default, HelpMetaEmphasis, Format::Default);
183 - if (arg.Limit() > 1)
183 + if (arg.IsUnlimited())
184 {
185 reporter.Write(helpLevel, L"{}...{}", HelpMetaEmphasis, Format::Default);
186 }
@@ -427,11 +427,12 @@ void Command::ParseArguments(
427 // Validates the ArgMap produced by ParseArguments. ArgMap is assumed to have
428 // been populated and parsed successfully from the invocation and now we are validating
429 // that the arguments provided meet the requirements of the command. This includes checking
430 -// that all required arguments are present and no arguments exceed their count limits.
430 +// that all required arguments are present. Count limits are enforced during parsing
431 +// (single-value args are last-wins), so they are not re-checked here.
432 // Any defined validation for specific ArgTypes are also run.
433 void Command::ValidateArguments(const ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const
434 {
434 - if (source.Contains(ArgType::Help))
435 + if (source.GetFlag<ArgType::Help>())
436 {
437 return;
438 }
@@ -443,11 +444,6 @@ void Command::ValidateArguments(const ArgMap& source, const std::vector<Argument
444 throw CommandException(Localization::WSLCCLI_RequiredArgumentError(arg.Name()));
445 }
446
446 - if ((arg.Limit() > 0) && (arg.Limit() < source.Count(arg.Type())))
447 - {
448 - throw CommandException(Localization::WSLCCLI_TooManyArgumentsError(arg.Name()));
449 - }
450 -
447 if (source.Contains(arg.Type()))
448 {
449 arg.Validate(source);
@@ -463,7 +459,7 @@ void Command::ValidateArguments(const ArgMap& source, const std::vector<Argument
459 void Command::Execute(CLIExecutionContext& context) const
460 {
461 // If Help was part of the validated argument set, we will output help instead of executing.
466 - if (context.Args.Contains(ArgType::Help))
462 + if (context.Args.GetFlag<ArgType::Help>())
463 {
464 OutputHelp(context.Reporter);
465 }
src/windows/wslc/tasks/ContainerTasks.cpp
+16 -16
@@ -197,7 +197,7 @@ void GetContainers(CLIExecutionContext& context)
197 {
198 limit = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::Last>(), L"--last");
199 }
200 - else if (context.Args.Contains(ArgType::Latest))
200 + else if (context.Args.GetFlag<ArgType::Latest>())
201 {
202 limit = 1;
203 }
@@ -216,7 +216,7 @@ void GetContainers(CLIExecutionContext& context)
216 }
217 }
218
219 - context.Data.Add<Data::Containers>(ContainerService::List(session, context.Args.Contains(ArgType::All), limit, filters));
219 + context.Data.Add<Data::Containers>(ContainerService::List(session, context.Args.GetFlag<ArgType::All>(), limit, filters));
220 }
221
222 void InspectContainers(CLIExecutionContext& context)
@@ -548,7 +548,7 @@ void ListContainers(CLIExecutionContext& context)
548 // Note: --all and --filter status= are honored by the Docker daemon when
549 // GetContainers ran; no post-filtering needed here.
550
551 - if (context.Args.Contains(ArgType::Quiet))
551 + if (context.Args.GetFlag<ArgType::Quiet>())
552 {
553 // Print only the container ids
554 for (const auto& container : containers)
@@ -575,7 +575,7 @@ void ListContainers(CLIExecutionContext& context)
575 }
576 case FormatType::Table:
577 {
578 - bool trunc = !context.Args.Contains(ArgType::NoTrunc);
578 + bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
579 using enum ColumnOverflow;
580
581 // Create table with or without column limits based on --no-trunc flag
@@ -623,7 +623,7 @@ void RemoveContainers(CLIExecutionContext& context)
623 WI_ASSERT(context.Data.Contains(Data::Session));
624 auto& session = context.Data.Get<Data::Session>();
625 auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
626 - bool force = context.Args.Contains(ArgType::Force);
626 + bool force = context.Args.GetFlag<ArgType::Force>();
627 for (const auto& id : containerIds)
628 {
629 ContainerService::Delete(session, WideToMultiByte(id), force);
@@ -657,17 +657,17 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
657 options.Name = WideToMultiByte(context.Args.Get<ArgType::Name>());
658 }
659
660 - if (context.Args.Contains(ArgType::TTY))
660 + if (context.Args.GetFlag<ArgType::TTY>())
661 {
662 options.TTY = true;
663 }
664
665 - if (context.Args.Contains(ArgType::Detach))
665 + if (context.Args.GetFlag<ArgType::Detach>())
666 {
667 options.Detach = true;
668 }
669
670 - if (context.Args.Contains(ArgType::Interactive))
670 + if (context.Args.GetFlag<ArgType::Interactive>())
671 {
672 options.Interactive = true;
673 }
@@ -682,7 +682,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
682 }
683 }
684
685 - if (context.Args.Contains(ArgType::PublishAll))
685 + if (context.Args.GetFlag<ArgType::PublishAll>())
686 {
687 options.PublishAll = true;
688 }
@@ -702,7 +702,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
702 }
703 }
704
705 - if (context.Args.Contains(ArgType::Remove))
705 + if (context.Args.GetFlag<ArgType::Remove>())
706 {
707 options.Remove = true;
708 }
@@ -747,7 +747,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
747 options.HealthRetries = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::HealthRetries>());
748 }
749
750 - if (context.Args.Contains(ArgType::NoHealthcheck))
750 + if (context.Args.GetFlag<ArgType::NoHealthcheck>())
751 {
752 options.NoHealthcheck = true;
753 }
@@ -924,7 +924,7 @@ void ShowContainerStats(CLIExecutionContext& context)
924 for (const auto& container : allContainers)
925 {
926 // Skip non-running containers unless --all is specified.
927 - if (!context.Args.Contains(ArgType::All) && container.State != WSLCContainerState::WslcContainerStateRunning)
927 + if (!context.Args.GetFlag<ArgType::All>() && container.State != WSLCContainerState::WslcContainerStateRunning)
928 {
929 continue;
930 }
@@ -984,7 +984,7 @@ void ShowContainerStats(CLIExecutionContext& context)
984 }
985 case FormatType::Table:
986 {
987 - bool trunc = !context.Args.Contains(ArgType::NoTrunc);
987 + bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
988 using enum ColumnOverflow;
989
990 auto table = trunc ? wsl::windows::wslc::TableOutput<8>(
@@ -1037,7 +1037,7 @@ void StartContainer(CLIExecutionContext& context)
1037 WI_ASSERT(context.Data.Contains(Data::Session));
1038 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
1039 const auto& containerId = context.Args.Get<ArgType::ContainerId>();
1040 - const bool attach = context.Args.Contains(ArgType::Attach);
1040 + const bool attach = context.Args.GetFlag<ArgType::Attach>();
1041 context.ExitCode = ContainerService::Start(context.Reporter, context.Data.Get<Data::Session>(), WideToMultiByte(containerId), attach);
1042
1043 if (!attach)
@@ -1074,8 +1074,8 @@ void ViewContainerLogs(CLIExecutionContext& context)
1074 WI_ASSERT(context.Data.Contains(Data::Session));
1075 auto& session = context.Data.Get<Data::Session>();
1076 auto containerId = context.Args.Get<ArgType::ContainerId>();
1077 - bool follow = context.Args.Contains(ArgType::Follow);
1078 - bool timestamps = context.Args.Contains(ArgType::Timestamps);
1077 + bool follow = context.Args.GetFlag<ArgType::Follow>();
1078 + bool timestamps = context.Args.GetFlag<ArgType::Timestamps>();
1079
1080 ULONGLONG tail = 0;
1081 if (context.Args.Contains(ArgType::Tail))
src/windows/wslc/tasks/ImageTasks.cpp
+12 -12
@@ -118,12 +118,12 @@ void BuildImage(CLIExecutionContext& context)
118 }
119
120 WSLCBuildImageFlags flags = WSLCBuildImageFlagsNone;
121 - WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.Contains(ArgType::Verbose));
122 - WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.Contains(ArgType::NoCache));
123 - WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.Contains(ArgType::BuildPull));
121 + WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.GetFlag<ArgType::Verbose>());
122 + WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.GetFlag<ArgType::NoCache>());
123 + WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.GetFlag<ArgType::BuildPull>());
124
125 auto cancelEvent = context.CreateCancelEvent();
126 - BuildImageCallback callback(context.Reporter, cancelEvent, context.Args.Contains(ArgType::Verbose));
126 + BuildImageCallback callback(context.Reporter, cancelEvent, context.Args.GetFlag<ArgType::Verbose>());
127 services::ImageService::Build(session, contextPath, tags, buildArgs, labels, dockerfilePath, target, flags, &callback, cancelEvent);
128 }
129
@@ -155,9 +155,9 @@ void ListImages(CLIExecutionContext& context)
155 WI_ASSERT(context.Data.Contains(Data::Images));
156 auto& images = context.Data.Get<Data::Images>();
157
158 - if (context.Args.Contains(ArgType::Quiet))
158 + if (context.Args.GetFlag<ArgType::Quiet>())
159 {
160 - bool trunc = !context.Args.Contains(ArgType::NoTrunc);
160 + bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
161 for (const auto& image : images)
162 {
163 context.Reporter.Output(L"{}\n", trunc ? TruncateId(image.Id, true) : image.Id);
@@ -182,7 +182,7 @@ void ListImages(CLIExecutionContext& context)
182 }
183 case FormatType::Table:
184 {
185 - bool trunc = !context.Args.Contains(ArgType::NoTrunc);
185 + bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
186 using enum ColumnOverflow;
187
188 // Create table — only IMAGE ID uses fixed width; other columns shrink to fit the console.
@@ -224,7 +224,7 @@ void PullImage(CLIExecutionContext& context)
224 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
225 auto& session = context.Data.Get<Data::Session>();
226 const auto image = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
227 - const bool quiet = context.Args.Contains(ArgType::Quiet);
227 + const bool quiet = context.Args.GetFlag<ArgType::Quiet>();
228
229 // Match `docker pull`: for a name-only reference (no tag or digest) the tag defaults to "latest". Unless quiet,
230 // the client reports this on stdout before contacting the registry.
@@ -265,8 +265,8 @@ void DeleteImage(CLIExecutionContext& context)
265 WI_ASSERT(context.Data.Contains(Data::Session));
266 auto& session = context.Data.Get<Data::Session>();
267 const auto& imageIds = context.Args.GetAll<ArgType::ImageId>();
268 - bool force = context.Args.Contains(ArgType::ImageForce);
269 - bool noPrune = context.Args.Contains(ArgType::NoPrune);
268 + bool force = context.Args.GetFlag<ArgType::ImageForce>();
269 + bool noPrune = context.Args.GetFlag<ArgType::NoPrune>();
270 for (const auto& id : imageIds)
271 {
272 services::ImageService::Delete(session, WideToMultiByte(id), force, noPrune);
@@ -306,7 +306,7 @@ void ImportImage(CLIExecutionContext& context)
306 auto imageId = services::ImageService::Import(context.Reporter, session, input, imageName);
307 if (!imageId.empty())
308 {
309 - bool trunc = !context.Args.Contains(ArgType::NoTrunc);
309 + bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
310 context.Reporter.Output(L"{}\n", MultiByteToWide(TruncateId(imageId, trunc)));
311 }
312 }
@@ -381,7 +381,7 @@ void PruneImages(CLIExecutionContext& context)
381 WI_ASSERT(context.Data.Contains(Data::Session));
382 auto& session = context.Data.Get<Data::Session>();
383
384 - bool all = context.Args.Contains(ArgType::All);
384 + bool all = context.Args.GetFlag<ArgType::All>();
385
386 // Filter syntax (`key=value`) is enforced upstream; here we just split on the first '='.
387 std::vector<std::pair<std::string, std::string>> filters;
src/windows/wslc/tasks/NetworkTasks.cpp
+3 -3
@@ -95,7 +95,7 @@ void CreateNetwork(CLIExecutionContext& context)
95 options.Driver = WideToMultiByte(context.Args.Get<ArgType::Driver>());
96 }
97
98 - options.Internal = context.Args.Contains(ArgType::Internal);
98 + options.Internal = context.Args.GetFlag<ArgType::Internal>();
99
100 if (context.Args.Contains(ArgType::Subnet))
101 {
@@ -116,7 +116,7 @@ void DeleteNetworks(CLIExecutionContext& context)
116 WI_ASSERT(context.Data.Contains(Data::Session));
117 auto& session = context.Data.Get<Data::Session>();
118 auto networkNames = context.Args.GetAll<ArgType::NetworkName>();
119 - const bool force = context.Args.Contains(ArgType::Force);
119 + const bool force = context.Args.GetFlag<ArgType::Force>();
120 for (const auto& name : networkNames)
121 {
122 if (TryDeleteNetwork(context.Reporter, session, WideToMultiByte(name), force))
@@ -165,7 +165,7 @@ void ListNetworks(CLIExecutionContext& context)
165 WI_ASSERT(context.Data.Contains(Data::Networks));
166 auto& networks = context.Data.Get<Data::Networks>();
167
168 - if (context.Args.Contains(ArgType::Quiet))
168 + if (context.Args.GetFlag<ArgType::Quiet>())
169 {
170 for (const auto& network : networks)
171 {
src/windows/wslc/tasks/SessionTasks.cpp
+1 -1
@@ -67,7 +67,7 @@ void ResolveSession(CLIExecutionContext& context)
67 void ListSessions(CLIExecutionContext& context)
68 {
69 auto sessions = SessionService::List();
70 - if (context.Args.Contains(ArgType::Verbose))
70 + if (context.Args.GetFlag<ArgType::Verbose>())
71 {
72 const wchar_t* plural = sessions.size() == 1 ? L"" : L"s";
73 context.Reporter.Output(L"[wslc] Found {} session{}\n", sessions.size(), plural);
src/windows/wslc/tasks/VolumeTasks.cpp
+3 -3
@@ -108,7 +108,7 @@ void DeleteVolumes(CLIExecutionContext& context)
108 WI_ASSERT(context.Data.Contains(Data::Session));
109 auto& session = context.Data.Get<Data::Session>();
110 auto volumeNames = context.Args.GetAll<ArgType::VolumeName>();
111 - const bool force = context.Args.Contains(ArgType::Force);
111 + const bool force = context.Args.GetFlag<ArgType::Force>();
112 for (const auto& name : volumeNames)
113 {
114 if (TryDeleteVolume(context.Reporter, session, WideToMultiByte(name), force))
@@ -157,7 +157,7 @@ void ListVolumes(CLIExecutionContext& context)
157 WI_ASSERT(context.Data.Contains(Data::Volumes));
158 auto& volumes = context.Data.Get<Data::Volumes>();
159
160 - if (context.Args.Contains(ArgType::Quiet))
160 + if (context.Args.GetFlag<ArgType::Quiet>())
161 {
162 for (const auto& volume : volumes)
163 {
@@ -205,7 +205,7 @@ void PruneVolumes(CLIExecutionContext& context)
205 WI_ASSERT(context.Data.Contains(Data::Session));
206 auto& session = context.Data.Get<Data::Session>();
207
208 - const bool all = context.Args.Contains(ArgType::All);
208 + const bool all = context.Args.GetFlag<ArgType::All>();
209
210 std::vector<std::pair<std::string, std::string>> filters;
211 for (const auto& value : context.Args.GetAll<ArgType::Filter>())
test/windows/SimpleTests.cpp
+30
@@ -193,6 +193,36 @@ class SimpleTests
193 VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(wideString.c_str()));
194 }
195
196 + // With AllowExtendedForms the single-letter "t"/"f" forms (case-insensitive) are also
197 + // recognized, matching Go's strconv.ParseBool (and therefore the Docker CLI). Every
198 + // form accepted by default must still parse identically in extended mode.
199 + std::vector<std::pair<LPCSTR, std::optional<bool>>> extendedBoolTests = {
200 + {"1", true},
201 + {"0", false},
202 + {"true", true},
203 + {"false", false},
204 + {"True", true},
205 + {"False", false},
206 + {"t", true},
207 + {"T", true},
208 + {"f", false},
209 + {"F", false},
210 + {nullptr, std::nullopt},
211 + {"", std::nullopt},
212 + {"2", std::nullopt},
213 + {"tr", std::nullopt},
214 + {"true_", std::nullopt},
215 + {"false_", std::nullopt},
216 + };
217 +
218 + for (const auto& [input, expected] : extendedBoolTests)
219 + {
220 + VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(input, true));
221 +
222 + std::wstring wideString = wsl::shared::string::MultiByteToWide(input);
223 + VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(wideString.c_str(), true));
224 + }
225 +
226 // Test wsl::shared::string::ParseMemoryString
227 const std::vector<std::pair<LPCSTR, std::optional<uint64_t>>> testCases{
228 {"0", 0},
test/windows/wslc/ParserTestCases.h
+30 -2
@@ -63,12 +63,12 @@ inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet
63 Argument::Create(ArgType::Remove),
64 Argument::Create(ArgType::Signal),
65 Argument::Create(ArgType::Time),
66 - Argument::Create(ArgType::Publish, false, NO_LIMIT), // Not required, unlimited.
66 + Argument::Create(ArgType::Publish, false, Limit::Unlimited), // Not required, unlimited.
67 };
68
69 case ArgumentSet::List:
70 return {
71 - Argument::Create(ArgType::ContainerId, false, NO_LIMIT), // Optional positional
71 + Argument::Create(ArgType::ContainerId, false, Limit::Unlimited), // Optional positional
72 Argument::Create(ArgType::Help),
73 Argument::Create(ArgType::Verbose),
74 };
@@ -110,6 +110,15 @@ 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)") \
111 WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose --verbose image1)") \
112 \
113 +/* Single-value args are last-wins (docker-style): repeating a single-value arg is legal \
114 + * (no longer a "too many" error); the final occurrence overwrites earlier ones. Contrast \
115 + * with --publish above, which is unlimited and accumulates. Named, adjoined, alias, and \
116 + * interleaved forms all collapse to one value. */ \
117 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --signal 9 --signal 1 image1)") \
118 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --signal=9 --signal=1 image1)") \
119 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -t 5 -t 10 image1)") \
120 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --signal 9 -t 5 --signal 1 image1)") \
121 +\
122 /* Flag parse tests */ \
123 WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -? image1)") \
124 WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -?i image1)") \
@@ -168,6 +177,25 @@ WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 --verbose=false cont2)") \
177 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=invalid cont2)") \
178 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)") \
179 \
180 +/* Boolean flag value tests: named and alias forms accept true/false/1/0 (case-insensitive), \
181 + * and reject non-boolean tokens. Adjoined false forms store the flag with an explicit false \
182 + * value (so it reads back via GetFlag) and parsing still succeeds. Avoid --rm / changing the \
183 + * image1 positional so the harness spot-checks below stay valid. */ \
184 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --interactive=false image1)") \
185 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --interactive=true image1)") \
186 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --interactive=0 image1)") \
187 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --interactive=1 image1)") \
188 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --interactive=maybe image1)") \
189 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -i=false image1)") \
190 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -i=true image1)") \
191 +WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -i=nope image1)") \
192 +/* Docker parity: a boolean flag never consumes the following token, so "true" here is the \
193 + * image positional rather than the flag's value. */ \
194 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --interactive true)") \
195 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc --verbose=0 cont1)") \
196 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc --verbose=1 cont1)") \
197 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 --verbose=FALSE cont2)") \
198 +\
199 /* Root-level globals: strict optionsOnly parsing. Stops cleanly at the first \
200 * non-option token; recognized globals before that are consumed. Production \
201 * uses an additional stopOnUnknown bool that is covered separately by \
test/windows/wslc/WSLCCLIParserUnitTests.cpp
+222 -14
@@ -90,11 +90,6 @@ class WSLCCLIParserUnitTests
90 throw ArgumentException(std::wstring(L"Required argument missing: ") + arg.Name());
91 }
92
93 - if ((arg.Limit() > 0) && (arg.Limit() < args.Count(arg.Type())))
94 - {
95 - throw ArgumentException(std::wstring(L"Too many values for argument: ") + arg.Name());
96 - }
97 -
93 if (args.Contains(arg.Type()))
94 {
95 arg.Validate(args);
@@ -474,9 +469,9 @@ class WSLCCLIParserUnitTests
469 VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
470 }
471
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)
472 + // A preloaded (env-style) default followed by multiple CLI values collapses to the
473 + // final CLI value: the preload is dropped and single-value args are last-wins.
474 + TEST_METHOD(PreloadedDefault_LastCliValueWins)
475 {
476 auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 --signal 1");
477
@@ -492,8 +487,8 @@ class WSLCCLIParserUnitTests
487 }
488 sm.ThrowIfError();
489
495 - // First CLI value replaced the env preload; second CLI value stacked.
496 - VERIFY_ARE_EQUAL(2u, args.Count(ArgType::Signal));
490 + VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
491 + VERIFY_ARE_EQUAL(std::wstring(L"1"), args.Get<ArgType::Signal>());
492 }
493
494 // Preloaded flag default plus CLI mention of the same flag stays a single
@@ -537,9 +532,9 @@ class WSLCCLIParserUnitTests
532 VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
533 }
534
540 - // Duplicate value on the CLI (no override) still stacks so Validate can
541 - // catch the Limit violation.
542 - TEST_METHOD(DuplicateValueOnCli_StillStacks)
535 + // Duplicate single-value arg on the CLI (no preload) is last-wins (docker-style):
536 + // the final value replaces the earlier one instead of accumulating.
537 + TEST_METHOD(DuplicateValueOnCli_LastWins)
538 {
539 auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 --signal 1");
540
@@ -553,7 +548,220 @@ class WSLCCLIParserUnitTests
548 }
549 sm.ThrowIfError();
550
556 - VERIFY_ARE_EQUAL(2u, args.Count(ArgType::Signal));
551 + VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
552 + VERIFY_ARE_EQUAL(std::wstring(L"1"), args.Get<ArgType::Signal>());
553 + }
554 +
555 + // Unlimited value args are exempt from last-wins: every CLI occurrence accumulates.
556 + TEST_METHOD(UnlimitedValueOnCli_Accumulates)
557 + {
558 + ArgMap args = ParseFlags(L"wslc --publish 80:80 --publish 443:443", {Argument::Create(ArgType::Publish, false, Limit::Unlimited)});
559 +
560 + VERIFY_ARE_EQUAL(2u, args.Count(ArgType::Publish));
561 + }
562 +
563 + // Boolean flags store their explicit parsed value: present with true or false when the
564 + // flag is specified, absent when it is not. Consumers read them via ArgMap::GetFlag,
565 + // which returns the stored value if present or a caller-supplied default if absent. The
566 + // helper parses a single command line against the supplied defs and returns the resulting
567 + // ArgMap so each case can assert the stored flag value.
568 + static ArgMap ParseFlags(const std::wstring& commandLine, std::vector<Argument> defs)
569 + {
570 + auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(commandLine);
571 +
572 + ArgMap args;
573 + ParseArgumentsStateMachine sm{inv, args, std::move(defs)};
574 + while (sm.Step())
575 + {
576 + sm.ThrowIfError();
577 + }
578 + sm.ThrowIfError();
579 + return args;
580 + }
581 +
582 + // "--flag" and every recognized true form store a single true entry. The single-letter
583 + // "t"/"T" forms are Docker-parity extensions enabled for the CLI flag path.
584 + TEST_METHOD(Flag_TrueForms_StoreSingleTrueEntry)
585 + {
586 + for (const auto* cmd :
587 + {L"wslc --verbose",
588 + L"wslc --verbose=true",
589 + L"wslc --verbose=1",
590 + L"wslc --verbose=TRUE",
591 + L"wslc --verbose=t",
592 + L"wslc --verbose=T"})
593 + {
594 + Log::Comment(String().Format(L"Testing: %ls", cmd));
595 + ArgMap args = ParseFlags(cmd, {Argument::Create(ArgType::Verbose)});
596 +
597 + VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
598 + VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
599 + VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
600 + VERIFY_IS_TRUE(args.GetFlag<ArgType::Verbose>());
601 + }
602 + }
603 +
604 + // Every recognized false form stores the flag present with value false (a docker-style
605 + // "--flag=false"), so Contains() is true but GetFlag() reports false. The single-letter
606 + // "f"/"F" forms are Docker-parity extensions enabled for the CLI flag path.
607 + TEST_METHOD(Flag_FalseForms_StoreSingleFalseEntry)
608 + {
609 + for (const auto* cmd :
610 + {L"wslc --verbose=false", L"wslc --verbose=0", L"wslc --verbose=False", L"wslc --verbose=f", L"wslc --verbose=F"})
611 + {
612 + Log::Comment(String().Format(L"Testing: %ls", cmd));
613 + ArgMap args = ParseFlags(cmd, {Argument::Create(ArgType::Verbose)});
614 +
615 + VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
616 + VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
617 + VERIFY_IS_FALSE(args.Get<ArgType::Verbose>());
618 + VERIFY_IS_FALSE(args.GetFlag<ArgType::Verbose>());
619 + }
620 + }
621 +
622 + // A non-boolean adjoined value is an error rather than being silently ignored.
623 + TEST_METHOD(Flag_InvalidBoolean_Throws)
624 + {
625 + auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose=maybe");
626 +
627 + std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
628 +
629 + ArgMap args;
630 + ParseArgumentsStateMachine sm{inv, args, std::move(defs)};
631 +
632 + bool threw = false;
633 + try
634 + {
635 + while (sm.Step())
636 + {
637 + sm.ThrowIfError();
638 + }
639 + sm.ThrowIfError();
640 + }
641 + catch (const ArgumentException&)
642 + {
643 + threw = true;
644 + }
645 +
646 + VERIFY_IS_TRUE(threw);
647 + }
648 +
649 + // Docker parity: a space-separated token after a boolean flag is NOT consumed as the
650 + // flag's value; the flag is true and the token becomes the next positional.
651 + TEST_METHOD(Flag_SpaceSeparatedValue_StaysPositional)
652 + {
653 + ArgMap args = ParseFlags(
654 + L"wslc --verbose true", {Argument::Create(ArgType::Verbose), Argument::Create(ArgType::ContainerId, false, Limit::Unlimited)});
655 +
656 + VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
657 + VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
658 + VERIFY_ARE_EQUAL(1u, args.Count(ArgType::ContainerId));
659 + VERIFY_ARE_EQUAL(std::wstring(L"true"), args.Get<ArgType::ContainerId>());
660 + }
661 +
662 + // Alias forms honor adjoined booleans just like the long name.
663 + TEST_METHOD(Flag_AliasAdjoinedBoolean)
664 + {
665 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
666 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=true", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
667 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=false", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
668 + }
669 +
670 + // Docker-parity single-letter forms ("t"/"T"/"f"/"F") are honored on the alias form too.
671 + TEST_METHOD(Flag_AliasShortBooleanForms)
672 + {
673 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=t", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
674 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=T", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
675 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=f", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
676 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=F", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
677 + }
678 +
679 + // An adjoined boolean value may be wrapped in double quotes (e.g. --flag="true"), just like
680 + // an adjoined value argument. The quotes are stripped before the boolean is parsed, on both
681 + // the named and alias forms.
682 + TEST_METHOD(Flag_QuotedAdjoinedBoolean)
683 + {
684 + VERIFY_IS_TRUE(ParseFlags(L"wslc --verbose=\"true\"", {Argument::Create(ArgType::Verbose)}).GetFlag<ArgType::Verbose>());
685 + VERIFY_IS_FALSE(ParseFlags(L"wslc --verbose=\"false\"", {Argument::Create(ArgType::Verbose)}).GetFlag<ArgType::Verbose>());
686 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=\"true\"", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
687 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=\"false\"", {Argument::Create(ArgType::Quiet)}).GetFlag<ArgType::Quiet>());
688 + VERIFY_IS_TRUE(ParseFlags(L"wslc --verbose=\"t\"", {Argument::Create(ArgType::Verbose)}).GetFlag<ArgType::Verbose>());
689 + VERIFY_IS_FALSE(ParseFlags(L"wslc --verbose=\"f\"", {Argument::Create(ArgType::Verbose)}).GetFlag<ArgType::Verbose>());
690 + }
691 +
692 + // In an alias chain, leading flags are true and a trailing "=false" turns only the
693 + // last flag off.
694 + TEST_METHOD(Flag_AliasChain_TrailingFalse)
695 + {
696 + std::vector<Argument> defs = {Argument::Create(ArgType::Quiet), Argument::Create(ArgType::Interactive)};
697 +
698 + ArgMap all = ParseFlags(L"wslc -qi", defs);
699 + VERIFY_IS_TRUE(all.GetFlag<ArgType::Quiet>());
700 + VERIFY_IS_TRUE(all.GetFlag<ArgType::Interactive>());
701 +
702 + ArgMap trailingFalse = ParseFlags(L"wslc -qi=false", defs);
703 + VERIFY_IS_TRUE(trailingFalse.GetFlag<ArgType::Quiet>());
704 + VERIFY_IS_FALSE(trailingFalse.GetFlag<ArgType::Interactive>());
705 + }
706 +
707 + // Repeated flags are last-wins (matching docker) and never accumulate multiple entries:
708 + // "--flag --flag=false" ends up false, the reverse ends up true. The flag is stored either
709 + // way (a single entry), so GetFlag reports the winning value.
710 + TEST_METHOD(Flag_Repeated_LastWins)
711 + {
712 + ArgMap trueThenFalse = ParseFlags(L"wslc --verbose --verbose=false", {Argument::Create(ArgType::Verbose)});
713 + VERIFY_IS_FALSE(trueThenFalse.GetFlag<ArgType::Verbose>());
714 + VERIFY_ARE_EQUAL(1u, trueThenFalse.Count(ArgType::Verbose));
715 +
716 + ArgMap falseThenTrue = ParseFlags(L"wslc --verbose=false --verbose", {Argument::Create(ArgType::Verbose)});
717 + VERIFY_IS_TRUE(falseThenTrue.GetFlag<ArgType::Verbose>());
718 + VERIFY_ARE_EQUAL(1u, falseThenTrue.Count(ArgType::Verbose));
719 +
720 + ArgMap duplicateTrue = ParseFlags(L"wslc --verbose --verbose=true", {Argument::Create(ArgType::Verbose)});
721 + VERIFY_ARE_EQUAL(1u, duplicateTrue.Count(ArgType::Verbose));
722 + }
723 +
724 + // "--flag=false" overrides a preloaded (env-style) default of true, replacing it with a
725 + // single stored false rather than leaving a lingering true. GetFlag then reports false.
726 + TEST_METHOD(Flag_FalseOverridesPreloadedDefault)
727 + {
728 + auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose=false");
729 +
730 + std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
731 +
732 + ArgMap args;
733 + args.Add(ArgType::Verbose, true); // pretend env preloaded it to true
734 +
735 + ParseArgumentsStateMachine sm{inv, args, defs, /*optionsOnly*/ false, /*stopOnUnknown*/ false, /*overridableDefaults*/ defs};
736 + while (sm.Step())
737 + {
738 + sm.ThrowIfError();
739 + }
740 + sm.ThrowIfError();
741 +
742 + VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
743 + VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
744 + VERIFY_IS_FALSE(args.GetFlag<ArgType::Verbose>());
745 + }
746 +
747 + // A flag whose behavior is on by default is read with GetFlag(true): absent yields the
748 + // default (true), "--flag=false" yields false, and "--flag" yields true. A bare Contains()
749 + // cannot express this: it reports true for both "--flag" and "--flag=false" and false when
750 + // the flag is absent, so it distinguishes neither the two stored values nor absent-as-default.
751 + TEST_METHOD(Flag_GetFlagDefaultTrue_DefaultOnFlag)
752 + {
753 + std::vector<Argument> defs = {Argument::Create(ArgType::Remove)};
754 +
755 + ArgMap absent = ParseFlags(L"wslc", defs);
756 + VERIFY_IS_FALSE(absent.Contains(ArgType::Remove));
757 + VERIFY_IS_TRUE(absent.GetFlag<ArgType::Remove>(true));
758 +
759 + ArgMap disabled = ParseFlags(L"wslc --rm=false", defs);
760 + VERIFY_IS_TRUE(disabled.Contains(ArgType::Remove));
761 + VERIFY_IS_FALSE(disabled.GetFlag<ArgType::Remove>(true));
762 +
763 + ArgMap enabled = ParseFlags(L"wslc --rm", defs);
764 + VERIFY_IS_TRUE(enabled.GetFlag<ArgType::Remove>(true));
765 }
766 };
767