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