@samitouri / QOSAMI-WSL / commits / 3e27a6a5

CLI: Cache converted CLI arguments with on-demand revalidation, and unify typed value accessors (#41189)

David Bennett committed Aug 7, 2026 at 18:19 UTC 3e27a6a50177ce4e32e72f7b957aef5db5768062
39 files changed +1435 -579
localization/strings/en-US/Resources.resw
+3
@@ -3135,6 +3135,9 @@ On first run, creates the file with all settings commented out at their defaults
3135 <data name="WSLCCLI_SessionStoragePositionalArgDescription" xml:space="preserve">
3136 <value>Session storage path</value>
3137 </data>
3138 + <data name="WSLCCLI_StoragePathArgDescription" xml:space="preserve">
3139 + <value>Path to the session storage directory</value>
3140 + </data>
3141 <data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
3142 <value>Signal to send</value>
3143 </data>
src/windows/common/EnumVariantMap.h
+4 -2
@@ -54,12 +54,14 @@ enum class EnumBasedVariantMapAction
54 Add,
55 Contains,
56 Get,
57 + GetMutable,
58 GetAll,
59 Count,
60 Remove,
61 };
62
62 -// A callback function that can be used for logging map actions.
63 +// A callback function that can take any action in response to map operations, such as logging
64 +// accesses or maintaining state derived from the map contents.
65 template <typename Enum>
66 using EnumBasedVariantMapActionCallback = void (*)(const void* map, Enum value, EnumBasedVariantMapAction action);
67
@@ -172,7 +174,7 @@ struct EnumBasedVariantMap
174 {
175 if constexpr (Callback)
176 {
175 - Callback(this, E, EnumBasedVariantMapAction::Get);
177 + Callback(this, E, EnumBasedVariantMapAction::GetMutable);
178 }
179 auto itr = m_data.find(E);
180 THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.end(), "Get(%d): key not found", static_cast<int>(E));
src/windows/wslc/arguments/ArgMap.h new
+352
@@ -0,0 +1,352 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgMap.h
8 +
9 +Abstract:
10 +
11 + Declaration of ArgMap, the container for parsed command-line arguments and their validated
12 + (converted) value cache. Split out from ArgumentTypes.h, which holds only the argument enums
13 + and type mappings ArgMap is built on.
14 +
15 +--*/
16 +#pragma once
17 +#include "ArgumentTypes.h"
18 +#include "EnumVariantMap.h"
19 +#include <any>
20 +#include <map>
21 +#include <set>
22 +#include <type_traits>
23 +#include <vector>
24 +#include <utility>
25 +
26 +namespace wsl::windows::wslc::argument {
27 +
28 +struct ArgMap;
29 +
30 +namespace details {
31 + struct RawArgMapAccess;
32 +
33 + template <ArgType E, bool IsFlag = std::is_same_v<typename ArgDataMapping<E>::value_t, bool>>
34 + struct ArgValueTraits;
35 +
36 + template <ArgType E>
37 + struct ArgValueTraits<E, true>
38 + {
39 + using value_t = typename ArgDataMapping<E>::value_t;
40 + static constexpr bool Converted = false;
41 + };
42 +
43 + template <ArgType E>
44 + struct ArgValueTraits<E, false>
45 + {
46 + using converted_t = typename ArgConvertedTypeMapping<E>::value_t;
47 + using value_t = std::conditional_t<std::is_same_v<converted_t, NoConversion>, typename ArgDataMapping<E>::value_t, converted_t>;
48 + static constexpr bool Converted = !std::is_same_v<converted_t, NoConversion>;
49 + };
50 +} // namespace details
51 +
52 +// Validates one argument on demand against its current raw values. Defined in ArgumentValidation.cpp
53 +// so this header stays decoupled from the converter/domain headers.
54 +void EnsureArgumentValidated(ArgMap& map, ArgType type);
55 +
56 +// Map-action callback (defined after ArgMap, as it calls a member): operations that can mutate raw
57 +// values update that ArgType's validation state.
58 +inline void ArgMapInvalidateValidatedCache(const void* map, ArgType type, EnumBasedVariantMapAction action);
59 +
60 +// This is the main ArgType map used for storing parsed arguments.
61 +struct ArgMap : private wsl::windows::wslc::EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping, &ArgMapInvalidateValidatedCache>
62 +{
63 +private:
64 + using Base = wsl::windows::wslc::EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping, &ArgMapInvalidateValidatedCache>;
65 +
66 + friend struct details::RawArgMapAccess;
67 +
68 + // Raw reads are implementation details used by validation and the typed accessors below.
69 + // Callers consume arguments through GetValue/GetAllValues so reads validate and freeze them.
70 + using Base::Get;
71 + using Base::GetAll;
72 +
73 +public:
74 + ArgMap() = default;
75 + ArgMap(const ArgMap&) = default;
76 + ArgMap(ArgMap&&) = default;
77 + ArgMap& operator=(const ArgMap&) = delete;
78 + ArgMap& operator=(ArgMap&&) = delete;
79 +
80 + using Base::Add;
81 + using Base::Contains;
82 + using Base::Count;
83 + using Base::GetCount;
84 + using Base::GetKeys;
85 + using Base::IsMatchingType;
86 + using Base::Remove;
87 +
88 + template <ArgType E>
89 + using value_t = typename details::ArgValueTraits<E>::value_t;
90 +
91 + // Validated-value cache. Argument validation converts raw strings into typed values and caches
92 + // them here so execution reuses them without re-parsing. The store is type-erased (std::any keyed
93 + // by ArgType) to keep this base header free of the domain headers that define the converted types;
94 + // access is by a compile-time ArgType whose value type is derived from the argument's ConvertedType
95 + // (ArgumentConvertedTypes.h), so a wrong-type access is a compile error. A multimap preserves order
96 + // and multiplicity for arguments that allow multiple values.
97 + template <ArgType E>
98 + void AddValidated(typename details::ArgConvertedTypeMapping<E>::value_t value)
99 + {
100 + using value_t = typename details::ArgConvertedTypeMapping<E>::value_t;
101 + static_assert(
102 + !std::is_same_v<value_t, details::NoConversion>,
103 + "This argument has no converted type (NoConversion); it cannot be cached. "
104 + "Declare its ConvertedType in ArgumentDefinitions.h to enable caching.");
105 +
106 + ThrowIfImmutable(E, "add converted validation data");
107 + m_validated.emplace(E, std::any{std::move(value)});
108 + }
109 +
110 + bool ContainsValidated(ArgType type) const
111 + {
112 + return m_validated.find(type) != m_validated.end();
113 + }
114 +
115 + size_t CountValidated(ArgType type) const
116 + {
117 + return m_validated.count(type);
118 + }
119 +
120 + // Drops `type`'s memoized validation state (converted cache and validated record) so it never
121 + // outlives the raw data.
122 + void InvalidateValidated(ArgType type)
123 + {
124 + ThrowIfImmutable(type, "invalidate cached validation data");
125 + ClearValidated(type);
126 + }
127 +
128 + // Records `type` as validated for its current raw values so reads skip re-validation.
129 + void MarkValidated(ArgType type)
130 + {
131 + ThrowIfImmutable(type, "mark the argument as validated");
132 + m_validatedTypes.insert(type);
133 + }
134 +
135 + void HandleMapMutation(ArgType type, EnumBasedVariantMapAction action)
136 + {
137 + WI_ASSERT(action == EnumBasedVariantMapAction::Add || action == EnumBasedVariantMapAction::GetMutable || action == EnumBasedVariantMapAction::Remove);
138 +
139 + const char* operation = nullptr;
140 + switch (action)
141 + {
142 + case EnumBasedVariantMapAction::Add:
143 + operation = "add a raw argument value";
144 + break;
145 +
146 + case EnumBasedVariantMapAction::GetMutable:
147 + operation = "get mutable access to a raw argument value";
148 + break;
149 +
150 + case EnumBasedVariantMapAction::Remove:
151 + operation = "remove the raw argument values";
152 + break;
153 +
154 + default:
155 + WI_ASSERT(false);
156 + return;
157 + }
158 +
159 + ThrowIfImmutable(type, operation);
160 + ClearValidated(type);
161 + }
162 +
163 + // Reads an argument in one call: the cached converted value if the argument declares a
164 + // ConvertedType, otherwise the raw parsed value. An absent argument resolves to defaultValue,
165 + // which defaults to the value type's default constructor. The first resolved default is retained
166 + // so later reads return the same effective value. Caller-provided defaults are already typed and
167 + // do not populate the raw map or change Contains(). A successful read makes the argument immutable.
168 + template <ArgType E>
169 + const value_t<E>& GetValue(value_t<E> defaultValue = {})
170 + {
171 + if (const auto* resolvedDefault = GetResolvedDefault<E>())
172 + {
173 + return *resolvedDefault;
174 + }
175 +
176 + if (!Contains(E))
177 + {
178 + auto [itr, inserted] = m_resolvedDefaults.emplace(E, std::any{std::move(defaultValue)});
179 + WI_ASSERT(inserted);
180 + MarkImmutable(E);
181 +
182 + const auto* value = std::any_cast<value_t<E>>(&itr->second);
183 + WI_ASSERT_MSG(value != nullptr, "resolved default holds the wrong type for this argument");
184 + return *value;
185 + }
186 +
187 + if constexpr (!details::ArgValueTraits<E>::Converted)
188 + {
189 + // Validate-only arguments have no converted cache but can still fail validation, so run
190 + // it on demand before returning the raw value (covers values added post-validation).
191 + EnsureValidated(E);
192 + const auto& value = std::as_const(*this).template Get<E>();
193 + MarkImmutable(E);
194 + return value;
195 + }
196 + else
197 + {
198 + const auto& value = GetValidated<E>();
199 + MarkImmutable(E);
200 + return value;
201 + }
202 + }
203 +
204 + // Like GetValue, but returns every value for an argument that may appear multiple times (ArgMap
205 + // is a multimap), in insertion order. An absent argument returns an empty vector.
206 + template <ArgType E>
207 + auto GetAllValues()
208 + {
209 + static_assert(details::ArgDataMapping<E>::c_kind != Kind::Flag, "GetAllValues is not valid for Kind::Flag arguments.");
210 +
211 + if constexpr (!details::ArgValueTraits<E>::Converted)
212 + {
213 + // See GetValue: ensure validate-only arguments are checked on demand too.
214 + EnsureValidated(E);
215 + auto values = GetAll<E>();
216 + MarkImmutable(E);
217 + return values;
218 + }
219 + else
220 + {
221 + auto values = GetAllValidated<E>();
222 + MarkImmutable(E);
223 + return values;
224 + }
225 + }
226 +
227 +private:
228 + // Validates `type` against its current raw values unless already recorded as validated. The
229 + // record is set by a completed validation and cleared by the map-action callback on any raw
230 + // Add/Remove, so an argument added or overwritten after the up-front pass is validated on
231 + // demand, and its errors reported, exactly like a command-line value.
232 + void EnsureValidated(ArgType type)
233 + {
234 + if (m_validatedTypes.count(type) != 0)
235 + {
236 + return;
237 + }
238 +
239 + EnsureArgumentValidated(*this, type);
240 + }
241 +
242 + // Branch helper for GetValue's converted path. Private so callers go through GetValue.
243 + template <ArgType E>
244 + const typename details::ArgConvertedTypeMapping<E>::value_t& GetValidated()
245 + {
246 + using value_t = typename details::ArgConvertedTypeMapping<E>::value_t;
247 + static_assert(
248 + !std::is_same_v<value_t, details::NoConversion>,
249 + "This argument has no converted type (NoConversion); it cannot be read from the cache. "
250 + "Declare its ConvertedType in ArgumentDefinitions.h to enable caching.");
251 +
252 + // Validate on demand if `E` is not recorded as validated (added or overwritten after the
253 + // up-front pass), so the value read here is converted and its errors reported as usual.
254 + EnsureValidated(E);
255 +
256 + auto itr = m_validated.find(E);
257 + THROW_HR_IF_MSG(E_NOT_SET, itr == m_validated.end(), "GetValidated(%d): argument not validated", static_cast<int>(E));
258 +
259 + // any_cast cannot fail: entries under key E are only ever written by AddValidated<E>, which
260 + // stores exactly value_t. A null result is an internal invariant violation, not a runtime case.
261 + const value_t* value = std::any_cast<value_t>(&itr->second);
262 + WI_ASSERT_MSG(value != nullptr, "validated cache holds the wrong type for this argument");
263 +
264 + return *value;
265 + }
266 +
267 + // Branch helper for GetAllValues's converted path. Private so callers go through GetAllValues.
268 + template <ArgType E>
269 + std::vector<typename details::ArgConvertedTypeMapping<E>::value_t> GetAllValidated()
270 + {
271 + using value_t = typename details::ArgConvertedTypeMapping<E>::value_t;
272 + static_assert(
273 + !std::is_same_v<value_t, details::NoConversion>,
274 + "This argument has no converted type (NoConversion); it cannot be read from the cache. "
275 + "Declare its ConvertedType in ArgumentDefinitions.h to enable caching.");
276 +
277 + // See GetValidated: validate on demand if `E`'s validated record was cleared post-validation.
278 + EnsureValidated(E);
279 +
280 + std::vector<value_t> results;
281 + auto range = m_validated.equal_range(E);
282 + for (auto it = range.first; it != range.second; ++it)
283 + {
284 + // See GetValidated: any_cast cannot fail for a correctly populated cache.
285 + const value_t* value = std::any_cast<value_t>(&it->second);
286 + WI_ASSERT_MSG(value != nullptr, "validated cache holds the wrong type for this argument");
287 + results.push_back(*value);
288 + }
289 +
290 + return results;
291 + }
292 +
293 + template <ArgType E>
294 + const value_t<E>* GetResolvedDefault() const
295 + {
296 + const auto itr = m_resolvedDefaults.find(E);
297 + if (itr == m_resolvedDefaults.end())
298 + {
299 + return nullptr;
300 + }
301 +
302 + const auto* value = std::any_cast<value_t<E>>(&itr->second);
303 + WI_ASSERT_MSG(value != nullptr, "resolved default holds the wrong type for this argument");
304 + return value;
305 + }
306 +
307 + void MarkImmutable(ArgType type)
308 + {
309 + m_immutableTypes.insert(type);
310 + }
311 +
312 + void ClearValidated(ArgType type)
313 + {
314 + m_validated.erase(type);
315 + m_validatedTypes.erase(type);
316 + }
317 +
318 + void ThrowIfImmutable(ArgType type, const char* operation) const
319 + {
320 + THROW_HR_IF_MSG(
321 + E_ILLEGAL_METHOD_CALL,
322 + m_immutableTypes.count(type) != 0,
323 + "ArgMap argument %d is immutable because its effective value was already read by GetValue/GetAllValues; attempted to "
324 + "%hs",
325 + static_cast<int>(type),
326 + operation);
327 + }
328 +
329 + std::multimap<ArgType, std::any> m_validated;
330 + std::map<ArgType, std::any> m_resolvedDefaults;
331 +
332 + // ArgTypes validated against their current raw values. Distinct from m_validated (only converted
333 + // arguments populate that), so validate-only arguments are covered too. Cleared per type by
334 + // InvalidateValidated on a raw Add/Remove.
335 + std::set<ArgType> m_validatedTypes;
336 +
337 + // A successful GetValue/GetAllValues makes that ArgType's raw and validated data immutable.
338 + std::set<ArgType> m_immutableTypes;
339 +};
340 +
341 +// Only operations that can mutate raw values affect validation state; const reads are ignored.
342 +// Recovering the non-const ArgMap from the callback's type-erased pointer is valid because these
343 +// actions originate from non-const base operations. The base subobject is at offset 0 of ArgMap.
344 +inline void ArgMapInvalidateValidatedCache(const void* map, ArgType type, EnumBasedVariantMapAction action)
345 +{
346 + if (action == EnumBasedVariantMapAction::Add || action == EnumBasedVariantMapAction::GetMutable || action == EnumBasedVariantMapAction::Remove)
347 + {
348 + const_cast<ArgMap*>(static_cast<const ArgMap*>(map))->HandleMapMutation(type, action);
349 + }
350 +}
351 +
352 +} // namespace wsl::windows::wslc::argument
src/windows/wslc/arguments/Argument.cpp
+1 -1
@@ -40,7 +40,7 @@ Argument Argument::Create(ArgType type, std::optional<bool> required, std::optio
40 {
41 switch (type)
42 {
43 -#define WSLC_ARG_CREATE_CASE(EnumName, Name, Alias, ArgumentKind, Desc) \
43 +#define WSLC_ARG_CREATE_CASE(EnumName, Name, Alias, ArgumentKind, ConvertedType, Desc) \
44 case ArgType::EnumName: \
45 return Argument{ \
46 type, \
src/windows/wslc/arguments/Argument.h
+4 -3
@@ -12,7 +12,7 @@ Abstract:
12
13 --*/
14 #pragma once
15 -#include "ArgumentTypes.h"
15 +#include "ArgMap.h"
16
17 #include <string>
18
@@ -105,8 +105,9 @@ struct Argument
105 return m_limit == argument::Limit::Unlimited;
106 }
107
108 - // Validates this argument's value in the provided args
109 - void Validate(const ArgMap& execArgs) const;
108 + // Validates this argument's current values, caching the converted result (converted arguments
109 + // only) on `execArgs` so reads reuse it without re-parsing until the raw values change.
110 + void Validate(ArgMap& execArgs) const;
111
112 private:
113 ArgType m_argType;
src/windows/wslc/arguments/ArgumentConvertedTypes.h new
+60
@@ -0,0 +1,60 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ArgumentConvertedTypes.h
8 +
9 +Abstract:
10 +
11 + Declaration of the converted-value type aliases and ArgConvertedTypeMapping specializations that
12 + back ArgMap's typed value cache. Types only; the validation:: converter functions that produce
13 + these values live in ArgumentValidation.h.
14 +
15 +--*/
16 +#pragma once
17 +
18 +#include "ArgumentTypes.h"
19 +#include "ContainerModel.h"
20 +#include "InspectModel.h"
21 +
22 +#include <cstdint>
23 +#include <string>
24 +#include <tuple>
25 +#include <utility>
26 +#include <wslc.h>
27 +
28 +namespace wsl::windows::wslc::services {
29 +struct BuildOutput;
30 +struct BuildSecret;
31 +} // namespace wsl::windows::wslc::services
32 +
33 +namespace wsl::windows::wslc::argument::details {
34 +
35 +// Local aliases so the ConvertedType tokens in the WSLC_ARGUMENTS X-macro (ArgumentDefinitions.h)
36 +// resolve here regardless of include order. Aggregate converted types must be aliased because their
37 +// commas would otherwise break X-macro argument parsing if written inline in the table.
38 +using FormatType = wsl::windows::wslc::models::FormatType;
39 +using InspectType = wsl::windows::wslc::models::InspectType;
40 +using JsonIndent = int;
41 +using WSLCSignal = ::WSLCSignal;
42 +using UlimitValue = std::tuple<std::string, int64_t, int64_t>;
43 +using KeyValuePair = std::pair<std::string, std::string>;
44 +using BuildOutput = wsl::windows::wslc::services::BuildOutput;
45 +using BuildSecret = wsl::windows::wslc::services::BuildSecret;
46 +
47 +// Generate the ArgType -> converted type mapping from the X-macro. Every ArgType gets a
48 +// specialization; arguments that are not converted map to NoConversion (their raw string is used
49 +// directly at execution, and the validated cache accessors reject NoConversion at compile time).
50 +#define WSLC_ARG_CONVERTED_MAPPING(EnumName, Name, Alias, Kind, ConvertedType, Desc) \
51 + template <> \
52 + struct ArgConvertedTypeMapping<ArgType::EnumName> \
53 + { \
54 + using value_t = ConvertedType; \
55 + };
56 +
57 +WSLC_ARGUMENTS(WSLC_ARG_CONVERTED_MAPPING)
58 +#undef WSLC_ARG_CONVERTED_MAPPING
59 +
60 +} // namespace wsl::windows::wslc::argument::details
src/windows/wslc/arguments/ArgumentDefinitions.h
+111 -107
@@ -30,113 +30,117 @@ Abstract:
30 // if you wish to add validation for the new argument or have it use existing validation.
31
32 // X-Macro for defining all arguments in one place
33 -// Format: ARGUMENT(EnumName, Name, Alias, Kind, Desc)
33 +// Format: ARGUMENT(EnumName, Name, Alias, Kind, ConvertedType, Desc)
34 +// ConvertedType is the type the argument's string value is converted to during validation and cached for
35 +// execution (see ArgumentConvertedTypes.h). Use NoConversion for arguments that are not converted to a typed value.
36 // clang-format off
37 #define WSLC_ARGUMENTS(_) \
36 -_(All, "all", L"a", Kind::Flag, Localization::WSLCCLI_AllArgDescription()) \
37 -_(Archive, "archive", L"a", Kind::Flag, Localization::WSLCCLI_ArchiveArgDescription()) \
38 -_(Attach, "attach", L"a", Kind::Flag, Localization::WSLCCLI_AttachArgDescription()) \
39 -_(BuildArg, "build-arg", NO_ALIAS, Kind::Value, Localization::WSLCCLI_BuildArgDescription()) \
40 -_(BuildPull, "pull", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_BuildPullArgDescription()) \
41 -_(BuildTarget, "target", NO_ALIAS, Kind::Value, Localization::WSLCCLI_BuildTargetArgDescription()) \
42 -_(CIDFile, "cidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CIDFileArgDescription()) \
43 -_(Command, "command", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_CommandArgDescription()) \
44 -_(ContainerId, "container-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ContainerIdArgDescription()) \
45 -_(Cpus, "cpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CpusArgDescription()) \
46 -_(Force, "force", L"f", Kind::Flag, Localization::WSLCCLI_ForceArgDescription()) \
47 -_(Detach, "detach", L"d", Kind::Flag, Localization::WSLCCLI_DetachArgDescription()) \
48 -_(DNS, "dns", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSArgDescription()) \
49 -/*_(DNSDomain, "dns-domain", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSDomainArgDescription())*/ \
50 -_(DNSOption, "dns-option", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSOptionArgDescription()) \
51 -_(DNSSearch, "dns-search", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSSearchArgDescription()) \
52 -_(Domainname, "domainname", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DomainnameArgDescription()) \
53 -_(Driver, "driver", L"d", Kind::Value, Localization::WSLCCLI_DriverOptionDescription()) \
54 -_(DriverOpt, "driver-opt", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DriverOptArgDescription()) \
55 -_(Entrypoint, "entrypoint", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EntrypointArgDescription()) \
56 -_(Env, "env", L"e", Kind::Value, Localization::WSLCCLI_EnvArgDescription()) \
57 -_(EnvFile, "env-file", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EnvFileArgDescription()) \
58 -_(File, "file", L"f", Kind::Value, Localization::WSLCCLI_FileArgDescription()) \
59 -_(Filter, "filter", L"f", Kind::Value, Localization::WSLCCLI_FilterArgDescription()) \
60 -_(Follow, "follow", L"f", Kind::Flag, Localization::WSLCCLI_FollowArgDescription()) \
61 -_(Timestamps, "timestamps", L"t", Kind::Flag, Localization::WSLCCLI_TimestampsArgDescription()) \
62 -_(Since, "since", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SinceArgDescription()) \
63 -_(Until, "until", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UntilArgDescription()) \
64 -_(Format, "format", NO_ALIAS, Kind::Value, Localization::WSLCCLI_FormatArgDescription()) \
65 -_(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, Localization::WSLCCLI_ForwardArgsDescription()) \
66 -_(Gateway, "gateway", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkGatewayArgDescription()) \
67 -_(Gpus, "gpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GpusArgDescription()) \
68 -/*_(GroupId, "groupid", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GroupIdArgDescription())*/ \
69 -_(HealthCmd, "health-cmd", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthCmdArgDescription()) \
70 -_(HealthInterval, "health-interval", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthIntervalArgDescription()) \
71 -_(HealthRetries, "health-retries", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthRetriesArgDescription()) \
72 -_(HealthStartPeriod, "health-start-period", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthStartPeriodArgDescription()) \
73 -_(HealthTimeout, "health-timeout", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthTimeoutArgDescription()) \
74 -_(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, Localization::WSLCCLI_HelpArgDescription()) \
75 -_(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \
76 -_(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \
77 -_(ImageId, "image", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImageIdArgDescription()) \
78 -_(ImportFile, "file", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImportFileArgDescription()) \
79 -_(IidFile, "iidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_IidFileArgDescription()) \
80 -_(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \
81 -_(InspectFormat, "format", NO_ALIAS, Kind::Value, Localization::WSLCCLI_InspectFormatArgDescription()) \
82 -_(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
83 -_(Internal, "internal", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NetworkInternalArgDescription()) \
84 -_(IpAddress, "ip", NO_ALIAS, Kind::Value, Localization::WSLCCLI_IpAddressArgDescription()) \
85 -_(IpRange, "ip-range", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkIpRangeArgDescription()) \
86 -_(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
87 -_(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
88 -_(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
89 -_(Link, "link", NO_ALIAS, Kind::Value, Localization::WSLCCLI_LinkArgDescription()) \
90 -_(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
91 -_(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
92 -_(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
93 -_(Network, "network", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkArgDescription()) \
94 -_(NetworkAlias, "network-alias", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkAliasArgDescription()) \
95 -_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
96 -/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
97 -_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
98 -_(NoColor, "no-color", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoColorArgDescription()) \
99 -_(NoHealthcheck, "no-healthcheck", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoHealthcheckArgDescription()) \
100 -_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \
101 -_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \
102 -_(ObjectId, "object-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ObjectIdArgDescription()) \
103 -_(Options, "opt", L"o", Kind::Value, Localization::WSLCCLI_OptionsArgDescription()) \
104 -_(Output, "output", L"o", Kind::Value, Localization::WSLCCLI_OutputArgDescription()) \
105 -_(Password, "password", L"p", Kind::Value, Localization::WSLCCLI_LoginPasswordArgDescription()) \
106 -_(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \
107 -_(Path, "path", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_PathArgDescription()) \
108 -/*_(Progress, "progress", NO_ALIAS, Kind::Value, Localization::WSLCCLI_ProgressArgDescription())*/ \
109 -_(Publish, "publish", L"p", Kind::Value, Localization::WSLCCLI_PublishArgDescription()) \
110 -_(PublishAll, "publish-all", L"P", Kind::Flag, Localization::WSLCCLI_PublishAllArgDescription()) \
111 -/*_(Pull, "pull", NO_ALIAS, Kind::Value, Localization::WSLCCLI_PullArgDescription())*/ \
112 -_(Quiet, "quiet", L"q", Kind::Flag, Localization::WSLCCLI_QuietArgDescription()) \
113 -_(Remove, "rm", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_RemoveArgDescription()) \
114 -/*_(Scheme, "scheme", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SchemeArgDescription())*/ \
115 -_(Secret, "secret", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SecretArgDescription()) \
116 -_(Server, "server", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_LoginServerArgDescription()) \
117 -_(Session, "session", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SessionIdArgDescription()) \
118 -_(ShmSize, "shm-size", NO_ALIAS, Kind::Value, Localization::WSLCCLI_ShmSizeArgDescription()) \
119 -_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
120 -_(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription()) \
121 -_(Source, "source", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SourceArgDescription()) \
122 -_(StopSignal, "stop-signal", NO_ALIAS, Kind::Value, Localization::WSLCCLI_StopSignalArgDescription()) \
123 -_(StopTimeout, "stop-timeout", NO_ALIAS, Kind::Value, Localization::WSLCCLI_StopTimeoutArgDescription()) \
124 -_(Subnet, "subnet", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkSubnetArgDescription()) \
125 -_(Tail, "tail", L"n", Kind::Value, Localization::WSLCCLI_TailArgDescription()) \
126 -_(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \
127 -_(Target, "target", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_TargetArgDescription()) \
128 -_(Time, "time", L"t", Kind::Value, Localization::WSLCCLI_TimeArgDescription()) \
129 -_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
130 -_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
131 -_(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \
132 -_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UlimitArgDescription()) \
133 -_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
134 -_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
135 -_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
136 -_(Version, "version", L"v", Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \
137 -/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \
138 -_(Volume, "volume", L"v", Kind::Value, Localization::WSLCCLI_VolumeArgDescription()) \
139 -_(VolumeName, "volume-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_VolumeNameArgDescription()) \
140 -_(Volumes, "volumes", L"v", Kind::Flag, Localization::WSLCCLI_RemoveVolumesArgDescription()) \
141 -_(WorkDir, "workdir", L"w", Kind::Value, Localization::WSLCCLI_WorkingDirArgDescription()) \
38 +_(All, "all", L"a", Kind::Flag, NoConversion, Localization::WSLCCLI_AllArgDescription()) \
39 +_(Archive, "archive", L"a", Kind::Flag, NoConversion, Localization::WSLCCLI_ArchiveArgDescription()) \
40 +_(Attach, "attach", L"a", Kind::Flag, NoConversion, Localization::WSLCCLI_AttachArgDescription()) \
41 +_(BuildArg, "build-arg", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_BuildArgDescription()) \
42 +_(BuildLabel, "label", L"l", Kind::Value, NoConversion, Localization::WSLCCLI_LabelArgDescription()) \
43 +_(BuildOutput, "output", L"o", Kind::Value, BuildOutput, Localization::WSLCCLI_OutputArgDescription()) \
44 +_(BuildPull, "pull", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_BuildPullArgDescription()) \
45 +_(BuildTarget, "target", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_BuildTargetArgDescription()) \
46 +_(CIDFile, "cidfile", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_CIDFileArgDescription()) \
47 +_(Command, "command", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_CommandArgDescription()) \
48 +_(ContainerId, "container-id", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_ContainerIdArgDescription()) \
49 +_(Cpus, "cpus", NO_ALIAS, Kind::Value, int64_t, Localization::WSLCCLI_CpusArgDescription()) \
50 +_(Force, "force", L"f", Kind::Flag, NoConversion, Localization::WSLCCLI_ForceArgDescription()) \
51 +_(Detach, "detach", L"d", Kind::Flag, NoConversion, Localization::WSLCCLI_DetachArgDescription()) \
52 +_(DNS, "dns", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_DNSArgDescription()) \
53 +/*_(DNSDomain, "dns-domain", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_DNSDomainArgDescription())*/ \
54 +_(DNSOption, "dns-option", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_DNSOptionArgDescription()) \
55 +_(DNSSearch, "dns-search", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_DNSSearchArgDescription()) \
56 +_(Domainname, "domainname", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_DomainnameArgDescription()) \
57 +_(Driver, "driver", L"d", Kind::Value, NoConversion, Localization::WSLCCLI_DriverOptionDescription()) \
58 +_(DriverOpt, "driver-opt", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_DriverOptArgDescription()) \
59 +_(Entrypoint, "entrypoint", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_EntrypointArgDescription()) \
60 +_(Env, "env", L"e", Kind::Value, NoConversion, Localization::WSLCCLI_EnvArgDescription()) \
61 +_(EnvFile, "env-file", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_EnvFileArgDescription()) \
62 +_(File, "file", L"f", Kind::Value, NoConversion, Localization::WSLCCLI_FileArgDescription()) \
63 +_(Filter, "filter", L"f", Kind::Value, KeyValuePair, Localization::WSLCCLI_FilterArgDescription()) \
64 +_(Follow, "follow", L"f", Kind::Flag, NoConversion, Localization::WSLCCLI_FollowArgDescription()) \
65 +_(Timestamps, "timestamps", L"t", Kind::Flag, NoConversion, Localization::WSLCCLI_TimestampsArgDescription()) \
66 +_(Since, "since", NO_ALIAS, Kind::Value, ULONGLONG, Localization::WSLCCLI_SinceArgDescription()) \
67 +_(Until, "until", NO_ALIAS, Kind::Value, ULONGLONG, Localization::WSLCCLI_UntilArgDescription()) \
68 +_(Format, "format", NO_ALIAS, Kind::Value, FormatType, Localization::WSLCCLI_FormatArgDescription()) \
69 +_(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, NoConversion, Localization::WSLCCLI_ForwardArgsDescription()) \
70 +_(Gateway, "gateway", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkGatewayArgDescription()) \
71 +_(Gpus, "gpus", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_GpusArgDescription()) \
72 +/*_(GroupId, "groupid", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_GroupIdArgDescription())*/ \
73 +_(HealthCmd, "health-cmd", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_HealthCmdArgDescription()) \
74 +_(HealthInterval, "health-interval", NO_ALIAS, Kind::Value, int64_t, Localization::WSLCCLI_HealthIntervalArgDescription()) \
75 +_(HealthRetries, "health-retries", NO_ALIAS, Kind::Value, int, Localization::WSLCCLI_HealthRetriesArgDescription()) \
76 +_(HealthStartPeriod,"health-start-period", NO_ALIAS, Kind::Value, int64_t, Localization::WSLCCLI_HealthStartPeriodArgDescription()) \
77 +_(HealthTimeout, "health-timeout", NO_ALIAS, Kind::Value, int64_t, Localization::WSLCCLI_HealthTimeoutArgDescription()) \
78 +_(Help, "help", WSLC_CLI_HELP_ARG,Kind::Flag, NoConversion, Localization::WSLCCLI_HelpArgDescription()) \
79 +_(Hostname, "hostname", L"h", Kind::Value, NoConversion, Localization::WSLCCLI_HostnameArgDescription()) \
80 +_(ImageForce, "force", L"f", Kind::Flag, NoConversion, Localization::WSLCCLI_ImageForceArgDescription()) \
81 +_(ImageId, "image", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_ImageIdArgDescription()) \
82 +_(ImportFile, "file", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_ImportFileArgDescription()) \
83 +_(IidFile, "iidfile", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_IidFileArgDescription()) \
84 +_(Input, "input", L"i", Kind::Value, NoConversion, Localization::WSLCCLI_InputArgDescription()) \
85 +_(InspectFormat, "format", NO_ALIAS, Kind::Value, JsonIndent, Localization::WSLCCLI_InspectFormatArgDescription()) \
86 +_(Interactive, "interactive", L"i", Kind::Flag, NoConversion, Localization::WSLCCLI_InteractiveArgDescription()) \
87 +_(Internal, "internal", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NetworkInternalArgDescription()) \
88 +_(IpAddress, "ip", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_IpAddressArgDescription()) \
89 +_(IpRange, "ip-range", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkIpRangeArgDescription()) \
90 +_(Label, "label", L"l", Kind::Value, KeyValuePair, Localization::WSLCCLI_LabelArgDescription()) \
91 +_(Last, "last", L"n", Kind::Value, int, Localization::WSLCCLI_LastArgDescription()) \
92 +_(Latest, "latest", L"l", Kind::Flag, NoConversion, Localization::WSLCCLI_LatestArgDescription()) \
93 +_(Link, "link", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkArgDescription()) \
94 +_(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
95 +_(Memory, "memory", L"m", Kind::Value, int64_t, Localization::WSLCCLI_MemoryArgDescription()) \
96 +_(Name, "name", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NameArgDescription()) \
97 +_(Network, "network", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkArgDescription()) \
98 +_(NetworkAlias, "network-alias", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkAliasArgDescription()) \
99 +_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_NetworkNameArgDescription()) \
100 +/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoDNSArgDescription())*/ \
101 +_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoCacheArgDescription()) \
102 +_(NoColor, "no-color", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoColorArgDescription()) \
103 +_(NoHealthcheck, "no-healthcheck", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoHealthcheckArgDescription()) \
104 +_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoPruneArgDescription()) \
105 +_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoTruncArgDescription()) \
106 +_(ObjectId, "object-id", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_ObjectIdArgDescription()) \
107 +_(Options, "opt", L"o", Kind::Value, KeyValuePair, Localization::WSLCCLI_OptionsArgDescription()) \
108 +_(Output, "output", L"o", Kind::Value, NoConversion, Localization::WSLCCLI_OutputArgDescription()) \
109 +_(Password, "password", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_LoginPasswordArgDescription()) \
110 +_(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \
111 +_(Path, "path", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_PathArgDescription()) \
112 +/*_(Progress, "progress", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_ProgressArgDescription())*/ \
113 +_(Publish, "publish", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_PublishArgDescription()) \
114 +_(PublishAll, "publish-all", L"P", Kind::Flag, NoConversion, Localization::WSLCCLI_PublishAllArgDescription()) \
115 +/*_(Pull, "pull", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_PullArgDescription())*/ \
116 +_(Quiet, "quiet", L"q", Kind::Flag, NoConversion, Localization::WSLCCLI_QuietArgDescription()) \
117 +_(Remove, "rm", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_RemoveArgDescription()) \
118 +/*_(Scheme, "scheme", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_SchemeArgDescription())*/ \
119 +_(Secret, "secret", NO_ALIAS, Kind::Value, BuildSecret, Localization::WSLCCLI_SecretArgDescription()) \
120 +_(Server, "server", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_LoginServerArgDescription()) \
121 +_(Session, "session", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_SessionIdArgDescription()) \
122 +_(ShmSize, "shm-size", NO_ALIAS, Kind::Value, int64_t, Localization::WSLCCLI_ShmSizeArgDescription()) \
123 +_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_StoragePathArgDescription()) \
124 +_(Signal, "signal", L"s", Kind::Value, WSLCSignal, Localization::WSLCCLI_SignalArgDescription()) \
125 +_(Source, "source", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_SourceArgDescription()) \
126 +_(StopSignal, "stop-signal", NO_ALIAS, Kind::Value, WSLCSignal, Localization::WSLCCLI_StopSignalArgDescription()) \
127 +_(StopTimeout, "stop-timeout", NO_ALIAS, Kind::Value, int, Localization::WSLCCLI_StopTimeoutArgDescription()) \
128 +_(Subnet, "subnet", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkSubnetArgDescription()) \
129 +_(Tail, "tail", L"n", Kind::Value, ULONGLONG, Localization::WSLCCLI_TailArgDescription()) \
130 +_(Tag, "tag", L"t", Kind::Value, NoConversion, Localization::WSLCCLI_TagArgDescription()) \
131 +_(Target, "target", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_TargetArgDescription()) \
132 +_(Time, "time", L"t", Kind::Value, LONG, Localization::WSLCCLI_TimeArgDescription()) \
133 +_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_TMPFSArgDescription()) \
134 +_(TTY, "tty", L"t", Kind::Flag, NoConversion, Localization::WSLCCLI_TTYArgDescription()) \
135 +_(Type, "type", L"t", Kind::Value, InspectType, Localization::WSLCCLI_TypeArgDescription()) \
136 +_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, UlimitValue, Localization::WSLCCLI_UlimitArgDescription()) \
137 +_(User, "user", L"u", Kind::Value, NoConversion, Localization::WSLCCLI_UserArgDescription()) \
138 +_(Username, "username", L"u", Kind::Value, NoConversion, Localization::WSLCCLI_LoginUsernameArgDescription()) \
139 +_(Verbose, "verbose", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_VerboseArgDescription()) \
140 +_(Version, "version", L"v", Kind::Flag, NoConversion, Localization::WSLCCLI_VersionArgDescription()) \
141 +/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_VirtualArgDescription())*/ \
142 +_(Volume, "volume", L"v", Kind::Value, NoConversion, Localization::WSLCCLI_VolumeArgDescription()) \
143 +_(VolumeName, "volume-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_VolumeNameArgDescription()) \
144 +_(Volumes, "volumes", L"v", Kind::Flag, NoConversion, Localization::WSLCCLI_RemoveVolumesArgDescription()) \
145 +_(WorkDir, "workdir", L"w", Kind::Value, NoConversion, Localization::WSLCCLI_WorkingDirArgDescription()) \
146 // clang-format on
src/windows/wslc/arguments/ArgumentParser.cpp
+1 -1
@@ -136,7 +136,7 @@ void ParseArgumentsStateMachine::SetFlag(ArgType type, bool value)
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
139 + // (e.g. "--flag --flag=false" ends up false). Read flags back via ArgMap::GetValue(defaultValue), 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);
src/windows/wslc/arguments/ArgumentParser.h
+2 -2
@@ -15,7 +15,7 @@ Abstract:
15 #include "Argument.h"
16 #include "Exceptions.h"
17 #include "Invocation.h"
18 -#include "ArgumentTypes.h"
18 +#include "ArgMap.h"
19
20 #include <optional>
21 #include <string>
@@ -133,7 +133,7 @@ private:
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),
136 + // entry. Consumers read the flag with ArgMap::GetValue(defaultValue),
137 // which lets a flag default to on and be disabled with "--flag=false".
138 void SetFlag(ArgType type, bool value);
139
src/windows/wslc/arguments/ArgumentTypes.h
+17 -25
@@ -13,11 +13,11 @@ Abstract:
13 --*/
14 #pragma once
15 #include "ArgumentDefinitions.h"
16 -#include "EnumVariantMap.h"
16 #include <string>
17 #include <vector>
18 #include <array>
19 #include <type_traits>
20 +#include <utility>
21
22 namespace wsl::windows::wslc::argument {
23 // General format: commandname [Flag | Value]* [Positional]* [Forward]
@@ -52,7 +52,7 @@ enum class Limit
52 // Generate ArgType enum from X-macro
53 enum class ArgType : size_t
54 {
55 -#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, Desc) EnumName,
55 +#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, ConvertedType, Desc) EnumName,
56 WSLC_ARGUMENTS(WSLC_ARG_ENUM)
57 #undef WSLC_ARG_ENUM
58
@@ -95,38 +95,30 @@ namespace details {
95 };
96
97 // Generate data mappings from X-macro - Kind determines the type
98 -#define WSLC_ARG_MAPPING(EnumName, Name, Alias, ArgumentKind, Desc) \
98 +#define WSLC_ARG_MAPPING(EnumName, Name, Alias, ArgumentKind, ConvertedType, Desc) \
99 template <> \
100 struct ArgDataMapping<ArgType::EnumName> \
101 { \
102 using value_t = typename KindToType<ArgumentKind>::type; \
103 + static constexpr Kind c_kind = ArgumentKind; \
104 };
105
106 WSLC_ARGUMENTS(WSLC_ARG_MAPPING)
107 #undef WSLC_ARG_MAPPING
108
108 -} // namespace details
109 -
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
109 + // Sentinel type for arguments that are not converted to a typed value during validation
110 + // (their raw string is used directly at execution). Arguments mapped to NoConversion cannot
111 + // be read from or written to the validated cache; doing so is a compile error.
112 + struct NoConversion
113 {
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 -};
114 + };
115 +
116 + // Maps an ArgType to the type its string value is converted to during validation. Declared here
117 + // so ArgMap's cache accessors can name the converted type without depending on the domain headers
118 + // that define it; the specializations live in ArgumentConvertedTypes.h.
119 + template <ArgType D>
120 + struct ArgConvertedTypeMapping;
121 +
122 +} // namespace details
123
124 } // namespace wsl::windows::wslc::argument
src/windows/wslc/arguments/ArgumentValidation.cpp
+129 -51
@@ -14,13 +14,15 @@ Abstract:
14
15 #include "precomp.h"
16 #include "Argument.h"
17 -#include "ArgumentTypes.h"
17 +#include "ArgMap.h"
18 #include "ArgumentValidation.h"
19 #include "ContainerModel.h"
20 #include "Exceptions.h"
21 #include "ImageService.h"
22 #include "Localization.h"
23 #include <algorithm>
24 +#include <type_traits>
25 +#include <utility>
26 #include <wslc.h>
27
28 using namespace wsl::windows::common;
@@ -28,50 +30,104 @@ using namespace wsl::shared;
30 using namespace wsl::shared::string;
31
32 namespace wsl::windows::wslc {
31 -// Common argument validation that occurs across multiple commands.
32 -void Argument::Validate(const ArgMap& execArgs) const
33 +
34 +namespace argument::details {
35 + struct RawArgMapAccess
36 + {
37 + template <ArgType E>
38 + static auto GetAll(const ArgMap& map)
39 + {
40 + return map.GetAll<E>();
41 + }
42 + };
43 +} // namespace argument::details
44 +
45 +namespace {
46 + using argument::details::RawArgMapAccess;
47 +
48 + // Converts each raw value for argument A using the provided converter and caches the result on
49 + // the ArgMap. This is the single point where an argument's string input is converted; execution
50 + // later reads the cached value via GetValue/GetAllValues.
51 + template <ArgType A, typename Converter>
52 + void CacheConverted(ArgMap& execArgs, const std::wstring& argName, Converter&& convert)
53 + {
54 + using value_t = typename details::ArgConvertedTypeMapping<A>::value_t;
55 + using converted_t = decltype(convert(std::declval<const std::wstring&>(), std::declval<const std::wstring&>()));
56 + static_assert(
57 + std::is_same_v<converted_t, value_t>,
58 + "converter return type must exactly match the argument's declared ConvertedType in ArgumentDefinitions.h");
59 +
60 + for (const auto& value : RawArgMapAccess::GetAll<A>(execArgs))
61 + {
62 + execArgs.AddValidated<A>(convert(value, argName));
63 + }
64 +
65 + // Sanity check: each raw value for this argument must produce exactly one cached value.
66 + WI_ASSERT(execArgs.CountValidated(A) == execArgs.Count(A));
67 + }
68 +} // namespace
69 +
70 +// Common per-argument validation, run both by the up-front pass and on demand from ArgMap's read
71 +// path. Arguments with a converted type are converted and cached here; the type is recorded as
72 +// validated on success.
73 +void Argument::Validate(ArgMap& execArgs) const
74 {
75 switch (m_argType)
76 {
77 + case ArgType::BuildLabel:
78 + for (const auto& value : RawArgMapAccess::GetAll<ArgType::BuildLabel>(execArgs))
79 + {
80 + validation::ParseLabel(value);
81 + }
82 + break;
83 +
84 + case ArgType::BuildOutput:
85 + CacheConverted<ArgType::BuildOutput>(
86 + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseOutputSpec(value); });
87 + break;
88 +
89 case ArgType::Format:
37 - validation::ValidateFormatTypeFromString(execArgs.GetAll<ArgType::Format>(), m_name);
90 + CacheConverted<ArgType::Format>(execArgs, m_name, validation::GetFormatTypeFromString);
91 break;
92
93 case ArgType::InspectFormat:
41 - validation::ValidateInspectFormatTypeFromString(execArgs.GetAll<ArgType::InspectFormat>(), m_name);
94 + CacheConverted<ArgType::InspectFormat>(execArgs, m_name, validation::GetInspectJsonIndentFromString);
95 break;
96
97 case ArgType::Signal:
45 - validation::ValidateWSLCSignalFromString(execArgs.GetAll<ArgType::Signal>(), m_name);
98 + CacheConverted<ArgType::Signal>(execArgs, m_name, validation::GetWSLCSignalFromString);
99 break;
100
101 case ArgType::StopSignal:
49 - validation::ValidateWSLCSignalFromString(execArgs.GetAll<ArgType::StopSignal>(), m_name);
102 + CacheConverted<ArgType::StopSignal>(execArgs, m_name, validation::GetWSLCSignalFromString);
103 break;
104
105 case ArgType::StopTimeout:
53 - validation::ValidateIntegerFromString<long>(execArgs.GetAll<ArgType::StopTimeout>(), m_name);
106 + CacheConverted<ArgType::StopTimeout>(execArgs, m_name, [](const std::wstring& value, const std::wstring& name) {
107 + return validation::GetIntegerFromString<int>(value, name);
108 + });
109 break;
110
111 case ArgType::ShmSize:
57 - validation::ValidateMemorySize(execArgs.GetAll<ArgType::ShmSize>(), m_name);
112 + CacheConverted<ArgType::ShmSize>(execArgs, m_name, validation::GetMemorySizeFromString);
113 break;
114
115 case ArgType::HealthInterval:
61 - validation::ValidateDuration(execArgs.GetAll<ArgType::HealthInterval>(), m_name);
116 + CacheConverted<ArgType::HealthInterval>(execArgs, m_name, validation::GetDurationNanosFromString);
117 break;
118
119 case ArgType::HealthTimeout:
65 - validation::ValidateDuration(execArgs.GetAll<ArgType::HealthTimeout>(), m_name);
120 + CacheConverted<ArgType::HealthTimeout>(execArgs, m_name, validation::GetDurationNanosFromString);
121 break;
122
123 case ArgType::HealthStartPeriod:
69 - validation::ValidateDuration(execArgs.GetAll<ArgType::HealthStartPeriod>(), m_name);
124 + CacheConverted<ArgType::HealthStartPeriod>(execArgs, m_name, validation::GetDurationNanosFromString);
125 break;
126
127 case ArgType::HealthRetries:
73 - validation::ValidateIntegerFromString<int>(
74 - execArgs.GetAll<ArgType::HealthRetries>(), m_name, [](int value) { return value >= 0; });
128 + CacheConverted<ArgType::HealthRetries>(execArgs, m_name, [](const std::wstring& value, const std::wstring& name) {
129 + return validation::GetIntegerFromString<int>(value, name, [](int v) { return v >= 0; });
130 + });
131 break;
132
133 case ArgType::NoHealthcheck:
@@ -83,73 +139,91 @@ void Argument::Validate(const ArgMap& execArgs) const
139 break;
140
141 case ArgType::Memory:
86 - validation::ValidateMemorySize(execArgs.GetAll<ArgType::Memory>(), m_name);
142 + CacheConverted<ArgType::Memory>(execArgs, m_name, validation::GetMemorySizeFromString);
143 break;
144
145 case ArgType::Cpus:
90 - validation::ValidateNanoCpus(execArgs.GetAll<ArgType::Cpus>(), m_name);
146 + CacheConverted<ArgType::Cpus>(execArgs, m_name, validation::GetNanoCpusFromString);
147 break;
148
149 case ArgType::Ulimit:
94 - validation::ValidateUlimit(execArgs.GetAll<ArgType::Ulimit>(), m_name);
150 + CacheConverted<ArgType::Ulimit>(execArgs, m_name, validation::ParseUlimit);
151 break;
152
153 case ArgType::Tail:
98 - validation::ValidateIntegerFromString<ULONGLONG>(
99 - execArgs.GetAll<ArgType::Tail>(), m_name, [](auto value) { return value != 0; });
154 + CacheConverted<ArgType::Tail>(execArgs, m_name, [](const std::wstring& value, const std::wstring& name) {
155 + return validation::GetIntegerFromString<ULONGLONG>(value, name, [](ULONGLONG v) { return v != 0; });
156 + });
157 break;
158
159 case ArgType::Time:
103 - validation::ValidateIntegerFromString<LONGLONG>(execArgs.GetAll<ArgType::Time>(), m_name);
160 + CacheConverted<ArgType::Time>(execArgs, m_name, [](const std::wstring& value, const std::wstring& name) {
161 + return validation::GetIntegerFromString<LONG>(value, name);
162 + });
163 break;
164
165 case ArgType::Secret:
107 - {
108 - for (const auto& spec : execArgs.GetAll<ArgType::Secret>())
109 - {
110 - std::ignore = validation::ParseSecretSpec(spec);
111 - }
166 + CacheConverted<ArgType::Secret>(
167 + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseSecretSpec(value); });
168 break;
113 - }
169
170 case ArgType::Since:
116 - validation::ValidateTimestamp(execArgs.GetAll<ArgType::Since>(), m_name);
171 + CacheConverted<ArgType::Since>(execArgs, m_name, validation::GetTimestampFromString);
172 break;
173
174 case ArgType::Until:
120 - validation::ValidateTimestamp(execArgs.GetAll<ArgType::Until>(), m_name);
175 + CacheConverted<ArgType::Until>(execArgs, m_name, validation::GetTimestampFromString);
176 break;
177
178 case ArgType::Last:
124 - validation::ValidateIntegerFromString<int>(execArgs.GetAll<ArgType::Last>(), m_name);
179 + CacheConverted<ArgType::Last>(execArgs, m_name, [](const std::wstring& value, const std::wstring& name) {
180 + return validation::GetIntegerFromString<int>(value, name);
181 + });
182 break;
183
184 case ArgType::Filter:
128 - validation::ValidateFilter(execArgs.GetAll<ArgType::Filter>());
185 + CacheConverted<ArgType::Filter>(
186 + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseFilter(value); });
187 + break;
188 +
189 + case ArgType::Label:
190 + CacheConverted<ArgType::Label>(
191 + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseLabel(value); });
192 + break;
193 +
194 + case ArgType::Options:
195 + CacheConverted<ArgType::Options>(
196 + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseDriverOption(value); });
197 + break;
198 +
199 + case ArgType::Type:
200 + CacheConverted<ArgType::Type>(execArgs, m_name, validation::GetInspectTypeFromString);
201 break;
202
203 case ArgType::Gpus:
132 - validation::ValidateGpus(execArgs.GetAll<ArgType::Gpus>(), m_name);
204 + validation::ValidateGpus(RawArgMapAccess::GetAll<ArgType::Gpus>(execArgs), m_name);
205 break;
206
207 case ArgType::Volume:
136 - validation::ValidateVolumeMount(execArgs.GetAll<ArgType::Volume>());
208 + validation::ValidateVolumeMount(RawArgMapAccess::GetAll<ArgType::Volume>(execArgs));
209 break;
210
211 case ArgType::WorkDir:
212 {
141 - const auto& value = execArgs.Get<ArgType::WorkDir>();
142 - if (value.empty() ||
143 - std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
213 + for (const auto& value : RawArgMapAccess::GetAll<ArgType::WorkDir>(execArgs))
214 {
145 - throw ArgumentException(Localization::WSLCCLI_WorkingDirEmptyError(m_name));
215 + if (value.empty() ||
216 + std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
217 + {
218 + throw ArgumentException(Localization::WSLCCLI_WorkingDirEmptyError(m_name));
219 + }
220 }
221 break;
222 }
223
224 case ArgType::Network:
225 {
152 - for (const auto& value : execArgs.GetAll<ArgType::Network>())
226 + for (const auto& value : RawArgMapAccess::GetAll<ArgType::Network>(execArgs))
227 {
228 if (value.empty() ||
229 std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
@@ -167,7 +241,7 @@ void Argument::Validate(const ArgMap& execArgs) const
241
242 case ArgType::NetworkAlias:
243 {
170 - for (const auto& value : execArgs.GetAll<ArgType::NetworkAlias>())
244 + for (const auto& value : RawArgMapAccess::GetAll<ArgType::NetworkAlias>(execArgs))
245 {
246 if (value.empty() ||
247 std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
@@ -181,9 +255,26 @@ void Argument::Validate(const ArgMap& execArgs) const
255 default:
256 break;
257 }
258 +
259 + // Mark validated only on success: a throw above (invalid value) skips this, so the next read
260 + // re-validates and reports the same error again.
261 + execArgs.MarkValidated(m_argType);
262 }
263 } // namespace wsl::windows::wslc
264
265 +namespace wsl::windows::wslc::argument {
266 +
267 +// On-demand validation for ArgMap's read path. Clears any stale converted cache first (idempotent),
268 +// then Argument::Validate re-checks the raw values, throwing for an invalid one and recording the
269 +// type as validated on success.
270 +void EnsureArgumentValidated(ArgMap& map, ArgType type)
271 +{
272 + map.InvalidateValidated(type);
273 + Argument::Create(type).Validate(map);
274 +}
275 +
276 +} // namespace wsl::windows::wslc::argument
277 +
278 namespace wsl::windows::wslc::validation {
279
280 void ValidateWSLCSignalFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
@@ -228,19 +319,6 @@ void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const
319 }
320 }
321
231 -void ValidateInspectFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
232 -{
233 - constexpr std::wstring_view supportedValues = L"json";
234 -
235 - for (const auto& value : values)
236 - {
237 - if (!IsEqual(value, L"json"))
238 - {
239 - throw ArgumentException(Localization::WSLCCLI_InvalidFormatValueError(argName, value, supportedValues));
240 - }
241 - }
242 -}
243 -
322 void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName)
323 {
324 for (const auto& value : values)
src/windows/wslc/arguments/ArgumentValidation.h
+1 -3
@@ -16,6 +16,7 @@ Abstract:
16 #include "Exceptions.h"
17 #include "ContainerModel.h"
18 #include "InspectModel.h"
19 +#include "ArgumentConvertedTypes.h"
20 #include "SpecParsing.h"
21 #include <string>
22 #include <tuple>
@@ -74,9 +75,6 @@ void ValidateUlimit(const std::vector<std::wstring>& values, const std::wstring&
75
76 void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName);
77
77 -// The inspect family only renders JSON, so `json` (single line) is the sole accepted value.
78 -void ValidateInspectFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName);
79 -
78 void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName);
79
80 void ValidateVolumeMount(const std::vector<std::wstring>& values);
src/windows/wslc/arguments/SpecParsing.cpp
+28 -19
@@ -612,35 +612,44 @@ ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring&
612
613 models::FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName)
614 {
615 - if (IsEqual(input, L"json"))
616 - {
617 - return models::FormatType::Json;
618 - }
619 - else if (IsEqual(input, L"table"))
615 + // Single source of truth for the accepted format values. It drives both parsing and the error
616 + // message's supported-values list, so adding a type here updates both automatically.
617 + static constexpr std::pair<std::wstring_view, models::FormatType> c_formatTypes[] = {
618 + {L"json", models::FormatType::Json},
619 + {L"table", models::FormatType::Table},
620 + };
621 +
622 + for (const auto& [name, type] : c_formatTypes)
623 {
621 - return models::FormatType::Table;
624 + if (IsEqual(input, name))
625 + {
626 + return type;
627 + }
628 }
623 - else
629 +
630 + std::wstring supportedValues;
631 + for (const auto& formatType : c_formatTypes)
632 {
625 - constexpr std::wstring_view supportedValues = L"json, table";
626 - throw ArgumentException(Localization::WSLCCLI_InvalidFormatValueError(argName, input, supportedValues));
633 + if (!supportedValues.empty())
634 + {
635 + supportedValues += L", ";
636 + }
637 +
638 + supportedValues += formatType.first;
639 }
640 +
641 + throw ArgumentException(Localization::WSLCCLI_InvalidFormatValueError(argName, input, supportedValues));
642 }
643
630 -models::FormatType GetOutputFormat(const argument::ArgMap& args)
644 +int GetInspectJsonIndentFromString(const std::wstring& input, const std::wstring& argName)
645 {
632 - if (!args.Contains(argument::ArgType::Format))
646 + if (!IsEqual(input, L"json"))
647 {
634 - return models::FormatType::Table;
648 + constexpr std::wstring_view supportedValues = L"json";
649 + throw ArgumentException(Localization::WSLCCLI_InvalidFormatValueError(argName, input, supportedValues));
650 }
651
637 - return GetFormatTypeFromString(args.Get<argument::ArgType::Format>());
638 -}
639 -
640 -int GetInspectJsonIndent(const argument::ArgMap& args)
641 -{
642 - // Validation guarantees the only accepted value is "json", so its presence alone selects compact.
643 - return args.Contains(argument::ArgType::InspectFormat) ? wsl::shared::c_jsonCompactIndent : wsl::shared::c_jsonPrettyPrintIndent;
652 + return wsl::shared::c_jsonCompactIndent;
653 }
654
655 models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName)
src/windows/wslc/arguments/SpecParsing.h
+2 -6
@@ -14,7 +14,6 @@ Abstract:
14 --*/
15 #pragma once
16
17 -#include "ArgumentTypes.h"
17 #include "ContainerModel.h"
18 #include "InspectModel.h"
19 #include <string>
@@ -82,11 +81,8 @@ ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring&
81 // Parses an output format ("json"/"table") into a FormatType.
82 models::FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {});
83
85 -// Resolves the --format argument, falling back to table when it was not supplied.
86 -models::FormatType GetOutputFormat(const argument::ArgMap& args);
87 -
88 -// Returns the json::dump() indent for the inspect family: compact for `--format json`, indented otherwise.
89 -int GetInspectJsonIndent(const argument::ArgMap& args);
84 +// Parses the inspect family's sole supported format ("json") into its compact json::dump() indent.
85 +int GetInspectJsonIndentFromString(const std::wstring& input, const std::wstring& argName = {});
86
87 // Parses an inspect target ("image"/"container"/"network"/"volume") into an InspectType.
88 models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName);
src/windows/wslc/commands/ContainerAttachCommand.cpp
+2 -1
@@ -12,6 +12,7 @@ Abstract:
12
13 --*/
14
15 +#include "ArgumentConvertedTypes.h"
16 #include "ContainerCommand.h"
17 #include "CLIExecutionContext.h"
18 #include "ContainerTasks.h"
@@ -45,6 +46,6 @@ void ContainerAttachCommand::ExecuteInternal(CLIExecutionContext& context) const
46 {
47 context //
48 << ResolveSession //
48 - << AttachContainer(context.Args.Get<ArgType::ContainerId>());
49 + << AttachContainer(context.Args.GetValue<ArgType::ContainerId>());
50 }
51 } // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerCommand.h
+1 -1
@@ -150,7 +150,7 @@ struct ContainerListCommand final : public Command
150
151 protected:
152 void ExecuteInternal(CLIExecutionContext& context) const override;
153 - void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
153 + void ValidateArgumentsInternal(ArgMap& execArgs) const override;
154 };
155
156 // Logs Command
src/windows/wslc/commands/ContainerListCommand.cpp
+2 -2
@@ -58,9 +58,9 @@ void ContainerListCommand::ExecuteInternal(CLIExecutionContext& context) const
58 }
59 // clang-format on
60
61 -void ContainerListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
61 +void ContainerListCommand::ValidateArgumentsInternal(ArgMap& execArgs) const
62 {
63 - if (execArgs.Contains(ArgType::Last) && execArgs.GetFlag<ArgType::Latest>())
63 + if (execArgs.Contains(ArgType::Last) && execArgs.GetValue<ArgType::Latest>())
64 {
65 throw CommandException(Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest"));
66 }
src/windows/wslc/commands/ImageBuildCommand.cpp
+2 -2
@@ -33,9 +33,9 @@ std::vector<Argument> ImageBuildCommand::GetArguments() const
33 Argument::Create(ArgType::BuildTarget),
34 Argument::Create(ArgType::File),
35 Argument::Create(ArgType::IidFile),
36 - Argument::Create(ArgType::Label, false, Limit::Unlimited),
36 + Argument::Create(ArgType::BuildLabel, false, Limit::Unlimited),
37 Argument::Create(ArgType::NoCache),
38 - Argument::Create(ArgType::Output, false, std::nullopt, Localization::WSLCCLI_BuildOutputArgDescription()),
38 + Argument::Create(ArgType::BuildOutput, false, std::nullopt, Localization::WSLCCLI_BuildOutputArgDescription()),
39 Argument::Create(ArgType::Secret, false, Limit::Unlimited),
40 Argument::Create(ArgType::Tag, false, Limit::Unlimited),
41 Argument::Create(ArgType::Verbose),
src/windows/wslc/commands/RegistryCommand.cpp
+4 -4
@@ -74,14 +74,14 @@ std::wstring RegistryLoginCommand::LongDescription() const
74 return Localization::WSLCCLI_LoginLongDesc();
75 }
76
77 -void RegistryLoginCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
77 +void RegistryLoginCommand::ValidateArgumentsInternal(ArgMap& execArgs) const
78 {
79 - if (execArgs.Contains(ArgType::Password) && execArgs.GetFlag<ArgType::PasswordStdin>())
79 + if (execArgs.Contains(ArgType::Password) && execArgs.GetValue<ArgType::PasswordStdin>())
80 {
81 throw CommandException(Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive());
82 }
83
84 - if (execArgs.GetFlag<ArgType::PasswordStdin>() && !execArgs.Contains(ArgType::Username))
84 + if (execArgs.GetValue<ArgType::PasswordStdin>() && !execArgs.Contains(ArgType::Username))
85 {
86 throw CommandException(Localization::WSLCCLI_LoginPasswordStdinRequiresUsername());
87 }
@@ -98,7 +98,7 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const
98 // Resolve password: --password, --password-stdin, or interactive prompt.
99 if (!context.Args.Contains(ArgType::Password))
100 {
101 - if (context.Args.GetFlag<ArgType::PasswordStdin>())
101 + if (context.Args.GetValue<ArgType::PasswordStdin>())
102 {
103 context.Args.Add(ArgType::Password, context.Terminal.ReadLine().value_or(std::wstring{}));
104 }
src/windows/wslc/commands/RegistryCommand.h
+1 -1
@@ -47,7 +47,7 @@ struct RegistryLoginCommand final : public Command
47 std::wstring LongDescription() const override;
48
49 protected:
50 - void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
50 + void ValidateArgumentsInternal(ArgMap& execArgs) const override;
51 void ExecuteInternal(CLIExecutionContext& context) const override;
52 };
53
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.GetFlag<ArgType::Version>())
106 + if (context.Args.GetValue<ArgType::Version>())
107 {
108 VersionCommand::PrintVersion(context.Terminal);
109 return;
src/windows/wslc/commands/VersionCommand.cpp
+2 -2
@@ -13,7 +13,7 @@ Abstract:
13 --*/
14
15 #include "VersionCommand.h"
16 -#include "ArgumentValidation.h"
16 +#include "ArgumentConvertedTypes.h"
17 #include "CLIExecutionContext.h"
18 #include "JsonUtils.h"
19
@@ -47,7 +47,7 @@ void VersionCommand::PrintVersion(Terminal& terminal)
47
48 void VersionCommand::ExecuteInternal(CLIExecutionContext& context) const
49 {
50 - FormatType format = validation::GetOutputFormat(context.Args);
50 + const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
51
52 switch (format)
53 {
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.GetFlag<ArgType::NoColor>())
22 + if (GlobalArgs.GetValue<ArgType::NoColor>())
23 {
24 Terminal.SetNoColor(true);
25 }
src/windows/wslc/core/CLIExecutionContext.h
+1 -1
@@ -12,7 +12,7 @@ Abstract:
12
13 --*/
14 #pragma once
15 -#include "ArgumentTypes.h"
15 +#include "ArgMap.h"
16 #include "ExecutionContextData.h"
17 #include "Terminal.h"
18 #include <optional>
src/windows/wslc/core/Command.cpp
+4 -4
@@ -430,9 +430,9 @@ void Command::ParseArguments(
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
433 +void Command::ValidateArguments(ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const
434 {
435 - if (source.GetFlag<ArgType::Help>())
435 + if (source.GetValue<ArgType::Help>())
436 {
437 return;
438 }
@@ -459,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.
462 - if (context.Args.GetFlag<ArgType::Help>())
462 + if (context.Args.GetValue<ArgType::Help>())
463 {
464 OutputHelp(context.Terminal);
465 }
@@ -476,7 +476,7 @@ void Execute(CLIExecutionContext& context, std::unique_ptr<Command>& command)
476 command->Execute(context);
477 }
478
479 -void Command::ValidateArgumentsInternal(const ArgMap&) const
479 +void Command::ValidateArgumentsInternal(ArgMap&) const
480 {
481 // Commands may not need any extra validation; they'll override if they do.
482 }
src/windows/wslc/core/Command.h
+11 -4
@@ -14,7 +14,7 @@ Abstract:
14 #pragma once
15 #include "Argument.h"
16 #include "Exceptions.h"
17 -#include "ArgumentTypes.h"
17 +#include "ArgMap.h"
18 #include "CLIExecutionContext.h"
19 #include "Invocation.h"
20 #include "ArgumentParser.h"
@@ -127,9 +127,9 @@ struct Command
127 ParseArguments(inv, target, GetAllArguments());
128 }
129
130 - void ValidateArguments(const ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const;
130 + void ValidateArguments(ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const;
131
132 - void ValidateArguments(const ArgMap& source) const
132 + void ValidateArguments(ArgMap& source) const
133 {
134 ValidateArguments(source, GetAllArguments(), true);
135 }
@@ -137,7 +137,14 @@ struct Command
137 virtual void Execute(CLIExecutionContext& context) const;
138
139 protected:
140 - virtual void ValidateArgumentsInternal(const ArgMap& source) const;
140 + // Command-specific validation hook, run after the shared per-argument Argument::Validate pass.
141 + // Override to enforce cross-argument rules that per-argument validation cannot express, such as
142 + // mutually-exclusive arguments or required argument combinations.
143 + //
144 + // Contract: this hook enforces relationships between already-validated arguments. It receives a
145 + // GetValue/GetAllValues make the selected argument immutable after returning it. Converted
146 + // arguments are validated on demand if needed.
147 + virtual void ValidateArgumentsInternal(ArgMap& source) const;
148 virtual void ExecuteInternal(CLIExecutionContext& context) const = 0;
149
150 private:
src/windows/wslc/core/EnvironmentOptions.h
+1 -1
@@ -13,7 +13,7 @@ Abstract:
13 --*/
14 #pragma once
15 #include "Argument.h"
16 -#include "ArgumentTypes.h"
16 +#include "ArgMap.h"
17
18 #include <vector>
19
src/windows/wslc/tasks/ContainerTasks.cpp
+76 -117
@@ -12,7 +12,7 @@ Abstract:
12
13 --*/
14 #include "Argument.h"
15 -#include "ArgumentValidation.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "AsyncExecution.h"
17 #include "CLIExecutionContext.h"
18 #include "ContainerModel.h"
@@ -169,7 +169,7 @@ void CreateContainer(CLIExecutionContext& context)
169 auto result = ContainerService::Create(
170 context.Terminal,
171 context.Data.Get<Data::Session>(),
172 - WideToMultiByte(context.Args.Get<ArgType::ImageId>()),
172 + WideToMultiByte(context.Args.GetValue<ArgType::ImageId>()),
173 context.Data.Get<Data::ContainerOptions>());
174 context.Terminal.Output(L"{}\n", MultiByteToWide(result.Id));
175 }
@@ -182,7 +182,7 @@ void ExecContainer(CLIExecutionContext& context)
182 context.ExitCode = ContainerService::Exec(
183 context.Terminal,
184 context.Data.Get<Data::Session>(),
185 - WideToMultiByte(context.Args.Get<ArgType::ContainerId>()),
185 + WideToMultiByte(context.Args.GetValue<ArgType::ContainerId>()),
186 context.Data.Get<Data::ContainerOptions>());
187 }
188
@@ -195,35 +195,24 @@ void GetContainers(CLIExecutionContext& context)
195
196 if (context.Args.Contains(ArgType::Last))
197 {
198 - limit = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::Last>(), L"--last");
198 + limit = context.Args.GetValue<ArgType::Last>();
199 }
200 - else if (context.Args.GetFlag<ArgType::Latest>())
200 + else if (context.Args.GetValue<ArgType::Latest>())
201 {
202 limit = 1;
203 }
204
205 - // Filter syntax (`key=value`) is enforced upstream; here we just split on the first '='.
206 - std::vector<std::pair<std::string, std::string>> filters;
207 - if (context.Args.Contains(ArgType::Filter))
208 - {
209 - for (const auto& wideValue : context.Args.GetAll<ArgType::Filter>())
210 - {
211 - std::string raw = WideToMultiByte(wideValue);
212 - const auto eq = raw.find('=');
213 - WI_ASSERT(eq != std::string::npos);
205 + // Filter values are parsed and cached during argument validation.
206 + auto filters = context.Args.GetAllValues<ArgType::Filter>();
207
215 - filters.emplace_back(raw.substr(0, eq), raw.substr(eq + 1));
216 - }
217 - }
218 -
219 - context.Data.Add<Data::Containers>(ContainerService::List(session, context.Args.GetFlag<ArgType::All>(), limit, filters));
208 + context.Data.Add<Data::Containers>(ContainerService::List(session, context.Args.GetValue<ArgType::All>(), limit, filters));
209 }
210
211 void InspectContainers(CLIExecutionContext& context)
212 {
213 WI_ASSERT(context.Data.Contains(Data::Session));
214 auto& session = context.Data.Get<Data::Session>();
226 - auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
215 + auto containerIds = context.Args.GetAllValues<ArgType::ContainerId>();
216 std::vector<wsl::windows::common::wslc_schema::InspectContainer> result;
217 for (const auto& id : containerIds)
218 {
@@ -238,7 +227,7 @@ void InspectContainers(CLIExecutionContext& context)
227 }
228 }
229
241 - auto json = ToJson(result, validation::GetInspectJsonIndent(context.Args));
230 + auto json = ToJson(result, context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent));
231 context.Terminal.Output(L"{}\n", MultiByteToWide(json));
232 }
233
@@ -246,11 +235,11 @@ void KillContainers(CLIExecutionContext& context)
235 {
236 WI_ASSERT(context.Data.Contains(Data::Session));
237 auto& session = context.Data.Get<Data::Session>();
249 - auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
238 + auto containerIds = context.Args.GetAllValues<ArgType::ContainerId>();
239 WSLCSignal signal = WSLCSignalSIGKILL;
240 if (context.Args.Contains(ArgType::Signal))
241 {
253 - signal = validation::GetWSLCSignalFromString(context.Args.Get<ArgType::Signal>());
242 + signal = context.Args.GetValue<ArgType::Signal>();
243 }
244
245 for (const auto& id : containerIds)
@@ -265,11 +254,11 @@ void ExportContainer(CLIExecutionContext& context)
254 WI_ASSERT(context.Data.Contains(Data::Session));
255 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
256 auto& session = context.Data.Get<Data::Session>();
268 - auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
257 + auto containerId = WideToMultiByte(context.Args.GetValue<ArgType::ContainerId>());
258
259 if (context.Args.Contains(ArgType::Output))
260 {
272 - auto& output = context.Args.Get<ArgType::Output>();
261 + auto& output = context.Args.GetValue<ArgType::Output>();
262 ContainerService::Export(session, containerId, output);
263 }
264 else
@@ -291,8 +280,8 @@ void ContainerCp(CLIExecutionContext& context)
280 WI_ASSERT(context.Args.Contains(ArgType::Target));
281
282 auto& session = context.Data.Get<Data::Session>();
294 - const auto& source = context.Args.Get<ArgType::Source>();
295 - const auto& target = context.Args.Get<ArgType::Target>();
283 + const auto& source = context.Args.GetValue<ArgType::Source>();
284 + const auto& target = context.Args.GetValue<ArgType::Target>();
285
286 // Determine copy direction by looking for CONTAINER:PATH patterns.
287 // A single letter before ':' is a Windows drive path (e.g. C:\path), not a container reference.
@@ -554,7 +543,7 @@ void ListContainers(CLIExecutionContext& context)
543 // Note: --all and --filter status= are honored by the Docker daemon when
544 // GetContainers ran; no post-filtering needed here.
545
557 - if (context.Args.GetFlag<ArgType::Quiet>())
546 + if (context.Args.GetValue<ArgType::Quiet>())
547 {
548 // Print only the container ids
549 for (const auto& container : containers)
@@ -565,7 +554,7 @@ void ListContainers(CLIExecutionContext& context)
554 return;
555 }
556
568 - FormatType format = validation::GetOutputFormat(context.Args);
557 + const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
558
559 switch (format)
560 {
@@ -577,7 +566,7 @@ void ListContainers(CLIExecutionContext& context)
566 }
567 case FormatType::Table:
568 {
580 - bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
569 + bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
570 using enum ColumnOverflow;
571
572 // Create table with or without column limits based on --no-trunc flag
@@ -624,9 +613,9 @@ void RemoveContainers(CLIExecutionContext& context)
613 {
614 WI_ASSERT(context.Data.Contains(Data::Session));
615 auto& session = context.Data.Get<Data::Session>();
627 - auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
628 - bool force = context.Args.GetFlag<ArgType::Force>();
629 - bool deleteVolumes = context.Args.GetFlag<ArgType::Volumes>();
616 + auto containerIds = context.Args.GetAllValues<ArgType::ContainerId>();
617 + bool force = context.Args.GetValue<ArgType::Force>();
618 + bool deleteVolumes = context.Args.GetValue<ArgType::Volumes>();
619 for (const auto& id : containerIds)
620 {
621 ContainerService::Delete(session, WideToMultiByte(id), force, deleteVolumes);
@@ -642,7 +631,7 @@ void RunContainer(CLIExecutionContext& context)
631 context.ExitCode = ContainerService::Run(
632 context.Terminal,
633 context.Data.Get<Data::Session>(),
645 - WideToMultiByte(context.Args.Get<ArgType::ImageId>()),
634 + WideToMultiByte(context.Args.GetValue<ArgType::ImageId>()),
635 context.Data.Get<Data::ContainerOptions>());
636 }
637
@@ -652,32 +641,21 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
641
642 if (context.Args.Contains(ArgType::CIDFile))
643 {
655 - options.CidFile = context.Args.Get<ArgType::CIDFile>();
644 + options.CidFile = context.Args.GetValue<ArgType::CIDFile>();
645 }
646
647 if (context.Args.Contains(ArgType::Name))
648 {
660 - options.Name = WideToMultiByte(context.Args.Get<ArgType::Name>());
649 + options.Name = WideToMultiByte(context.Args.GetValue<ArgType::Name>());
650 }
651
663 - if (context.Args.GetFlag<ArgType::TTY>())
664 - {
665 - options.TTY = true;
666 - }
667 -
668 - if (context.Args.GetFlag<ArgType::Detach>())
669 - {
670 - options.Detach = true;
671 - }
672 -
673 - if (context.Args.GetFlag<ArgType::Interactive>())
674 - {
675 - options.Interactive = true;
676 - }
652 + options.TTY = context.Args.GetValue<ArgType::TTY>();
653 + options.Detach = context.Args.GetValue<ArgType::Detach>();
654 + options.Interactive = context.Args.GetValue<ArgType::Interactive>();
655
656 if (context.Args.Contains(ArgType::Publish))
657 {
680 - auto ports = context.Args.GetAll<ArgType::Publish>();
658 + auto ports = context.Args.GetAllValues<ArgType::Publish>();
659 options.Ports.reserve(options.Ports.size() + ports.size());
660 for (const auto& port : ports)
661 {
@@ -685,10 +663,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
663 }
664 }
665
688 - if (context.Args.GetFlag<ArgType::PublishAll>())
689 - {
690 - options.PublishAll = true;
691 - }
666 + options.PublishAll = context.Args.GetValue<ArgType::PublishAll>();
667
668 if (context.Args.Contains(ArgType::Gpus))
669 {
@@ -697,7 +672,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
672
673 if (context.Args.Contains(ArgType::Volume))
674 {
700 - auto volumes = context.Args.GetAll<ArgType::Volume>();
675 + auto volumes = context.Args.GetAllValues<ArgType::Volume>();
676 options.Volumes.reserve(options.Volumes.size() + volumes.size());
677 for (const auto& volume : volumes)
678 {
@@ -705,82 +680,70 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
680 }
681 }
682
708 - if (context.Args.GetFlag<ArgType::Remove>())
709 - {
710 - options.Remove = true;
711 - }
683 + options.Remove = context.Args.GetValue<ArgType::Remove>();
684
685 if (context.Args.Contains(ArgType::StopSignal))
686 {
715 - options.StopSignal = validation::GetWSLCSignalFromString(context.Args.Get<ArgType::StopSignal>());
687 + options.StopSignal = context.Args.GetValue<ArgType::StopSignal>();
688 }
689
690 if (context.Args.Contains(ArgType::StopTimeout))
691 {
720 - options.StopTimeout = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::StopTimeout>());
692 + options.StopTimeout = context.Args.GetValue<ArgType::StopTimeout>();
693 }
694
695 if (context.Args.Contains(ArgType::ShmSize))
696 {
725 - options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get<ArgType::ShmSize>());
697 + options.ShmSize = context.Args.GetValue<ArgType::ShmSize>();
698 }
699
700 if (context.Args.Contains(ArgType::HealthCmd))
701 {
730 - options.HealthCmd = WideToMultiByte(context.Args.Get<ArgType::HealthCmd>());
702 + options.HealthCmd = WideToMultiByte(context.Args.GetValue<ArgType::HealthCmd>());
703 }
704
705 if (context.Args.Contains(ArgType::HealthInterval))
706 {
735 - options.HealthInterval = validation::GetDurationNanosFromString(context.Args.Get<ArgType::HealthInterval>());
707 + options.HealthInterval = context.Args.GetValue<ArgType::HealthInterval>();
708 }
709
710 if (context.Args.Contains(ArgType::HealthTimeout))
711 {
740 - options.HealthTimeout = validation::GetDurationNanosFromString(context.Args.Get<ArgType::HealthTimeout>());
712 + options.HealthTimeout = context.Args.GetValue<ArgType::HealthTimeout>();
713 }
714
715 if (context.Args.Contains(ArgType::HealthStartPeriod))
716 {
745 - options.HealthStartPeriod = validation::GetDurationNanosFromString(context.Args.Get<ArgType::HealthStartPeriod>());
717 + options.HealthStartPeriod = context.Args.GetValue<ArgType::HealthStartPeriod>();
718 }
719
720 if (context.Args.Contains(ArgType::HealthRetries))
721 {
750 - options.HealthRetries = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::HealthRetries>());
722 + options.HealthRetries = context.Args.GetValue<ArgType::HealthRetries>();
723 }
724
753 - if (context.Args.GetFlag<ArgType::NoHealthcheck>())
754 - {
755 - options.NoHealthcheck = true;
756 - }
725 + options.NoHealthcheck = context.Args.GetValue<ArgType::NoHealthcheck>();
726
727 if (context.Args.Contains(ArgType::Memory))
728 {
760 - options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get<ArgType::Memory>());
729 + options.MemoryBytes = context.Args.GetValue<ArgType::Memory>();
730 }
731
732 if (context.Args.Contains(ArgType::Cpus))
733 {
765 - options.NanoCpus = validation::GetNanoCpusFromString(context.Args.Get<ArgType::Cpus>());
734 + options.NanoCpus = context.Args.GetValue<ArgType::Cpus>();
735 }
736
768 - if (context.Args.Contains(ArgType::Ulimit))
769 - {
770 - for (const auto& value : context.Args.GetAll<ArgType::Ulimit>())
771 - {
772 - options.Ulimits.emplace_back(validation::ParseUlimit(value));
773 - }
774 - }
737 + options.Ulimits = context.Args.GetAllValues<ArgType::Ulimit>();
738
739 if (context.Args.Contains(ArgType::Command))
740 {
778 - options.Arguments.emplace_back(WideToMultiByte(context.Args.Get<ArgType::Command>()));
741 + options.Arguments.emplace_back(WideToMultiByte(context.Args.GetValue<ArgType::Command>()));
742 }
743
744 if (context.Args.Contains(ArgType::EnvFile))
745 {
783 - auto const& envFiles = context.Args.GetAll<ArgType::EnvFile>();
746 + auto envFiles = context.Args.GetAllValues<ArgType::EnvFile>();
747 for (const auto& envFile : envFiles)
748 {
749 auto parsedEnvVars = EnvironmentVariable::ParseFile(envFile);
@@ -793,7 +756,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
756
757 if (context.Args.Contains(ArgType::Env))
758 {
796 - auto const& envArgs = context.Args.GetAll<ArgType::Env>();
759 + auto envArgs = context.Args.GetAllValues<ArgType::Env>();
760 for (const auto& arg : envArgs)
761 {
762 auto envVar = EnvironmentVariable::Parse(arg);
@@ -806,22 +769,22 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
769
770 if (context.Args.Contains(ArgType::Entrypoint))
771 {
809 - options.Entrypoint.push_back(WideToMultiByte(context.Args.Get<ArgType::Entrypoint>()));
772 + options.Entrypoint.push_back(WideToMultiByte(context.Args.GetValue<ArgType::Entrypoint>()));
773 }
774
775 if (context.Args.Contains(ArgType::Hostname))
776 {
814 - options.Hostname = WideToMultiByte(context.Args.Get<ArgType::Hostname>());
777 + options.Hostname = WideToMultiByte(context.Args.GetValue<ArgType::Hostname>());
778 }
779
780 if (context.Args.Contains(ArgType::Domainname))
781 {
819 - options.Domainname = WideToMultiByte(context.Args.Get<ArgType::Domainname>());
782 + options.Domainname = WideToMultiByte(context.Args.GetValue<ArgType::Domainname>());
783 }
784
785 if (context.Args.Contains(ArgType::DNS))
786 {
824 - auto dnsServers = context.Args.GetAll<ArgType::DNS>();
787 + auto dnsServers = context.Args.GetAllValues<ArgType::DNS>();
788 options.DnsServers.reserve(options.DnsServers.size() + dnsServers.size());
789 for (const auto& value : dnsServers)
790 {
@@ -831,7 +794,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
794
795 if (context.Args.Contains(ArgType::DNSSearch))
796 {
834 - auto dnsSearch = context.Args.GetAll<ArgType::DNSSearch>();
797 + auto dnsSearch = context.Args.GetAllValues<ArgType::DNSSearch>();
798 options.DnsSearchDomains.reserve(options.DnsSearchDomains.size() + dnsSearch.size());
799 for (const auto& value : dnsSearch)
800 {
@@ -841,7 +804,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
804
805 if (context.Args.Contains(ArgType::DNSOption))
806 {
844 - auto dnsOptions = context.Args.GetAll<ArgType::DNSOption>();
807 + auto dnsOptions = context.Args.GetAllValues<ArgType::DNSOption>();
808 options.DnsOptions.reserve(options.DnsOptions.size() + dnsOptions.size());
809 for (const auto& value : dnsOptions)
810 {
@@ -851,7 +814,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
814
815 if (context.Args.Contains(ArgType::Network))
816 {
854 - auto networks = context.Args.GetAll<ArgType::Network>();
817 + auto networks = context.Args.GetAllValues<ArgType::Network>();
818 options.Networks.reserve(options.Networks.size() + networks.size());
819 for (const auto& value : networks)
820 {
@@ -861,7 +824,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
824
825 if (context.Args.Contains(ArgType::NetworkAlias))
826 {
864 - auto aliases = context.Args.GetAll<ArgType::NetworkAlias>();
827 + auto aliases = context.Args.GetAllValues<ArgType::NetworkAlias>();
828 options.NetworkAliases.reserve(aliases.size());
829 for (const auto& value : aliases)
830 {
@@ -871,12 +834,12 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
834
835 if (context.Args.Contains(ArgType::User))
836 {
874 - options.User = WideToMultiByte(context.Args.Get<ArgType::User>());
837 + options.User = WideToMultiByte(context.Args.GetValue<ArgType::User>());
838 }
839
840 if (context.Args.Contains(ArgType::TMPFS))
841 {
879 - auto tmpfs = context.Args.GetAll<ArgType::TMPFS>();
842 + auto tmpfs = context.Args.GetAllValues<ArgType::TMPFS>();
843 options.Tmpfs.reserve(options.Tmpfs.size() + tmpfs.size());
844 for (const auto& value : tmpfs)
845 {
@@ -884,18 +847,14 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
847 }
848 }
849
887 - if (context.Args.Contains(ArgType::Label))
850 + for (const auto& label : context.Args.GetAllValues<ArgType::Label>())
851 {
889 - for (const auto& label : context.Args.GetAll<ArgType::Label>())
890 - {
891 - auto parsed = validation::ParseLabel(label);
892 - options.Labels.emplace_back(parsed.first, parsed.second);
893 - }
852 + options.Labels.push_back(label);
853 }
854
855 if (context.Args.Contains(ArgType::ForwardArgs))
856 {
898 - auto const& forwardArgs = context.Args.Get<ArgType::ForwardArgs>();
857 + auto const& forwardArgs = context.Args.GetValue<ArgType::ForwardArgs>();
858 options.Arguments.reserve(options.Arguments.size() + forwardArgs.size());
859 for (const auto& arg : forwardArgs)
860 {
@@ -905,7 +864,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
864
865 if (context.Args.Contains(ArgType::WorkDir))
866 {
908 - options.WorkingDirectory = WideToMultiByte(context.Args.Get<ArgType::WorkDir>());
867 + options.WorkingDirectory = WideToMultiByte(context.Args.GetValue<ArgType::WorkDir>());
868 }
869
870 context.Data.Add<Data::ContainerOptions>(std::move(options));
@@ -916,7 +875,7 @@ void ShowContainerStats(CLIExecutionContext& context)
875 WI_ASSERT(context.Data.Contains(Data::Session));
876 auto& session = context.Data.Get<Data::Session>();
877
919 - auto containers = context.Args.GetAll<ArgType::ContainerId>();
878 + auto containers = context.Args.GetAllValues<ArgType::ContainerId>();
879
880 // If any are specified we use those, otherwise we show all containers.
881 const bool userSpecifiedContainers = !containers.empty();
@@ -927,7 +886,7 @@ void ShowContainerStats(CLIExecutionContext& context)
886 for (const auto& container : allContainers)
887 {
888 // Skip non-running containers unless --all is specified.
930 - if (!context.Args.GetFlag<ArgType::All>() && container.State != WSLCContainerState::WslcContainerStateRunning)
889 + if (!context.Args.GetValue<ArgType::All>() && container.State != WSLCContainerState::WslcContainerStateRunning)
890 {
891 continue;
892 }
@@ -972,7 +931,7 @@ void ShowContainerStats(CLIExecutionContext& context)
931 10 // Batch Size - chosen to be around typical expected container use while protecting against extreme cases.
932 );
933
975 - FormatType format = validation::GetOutputFormat(context.Args);
934 + const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
935
936 switch (format)
937 {
@@ -983,7 +942,7 @@ void ShowContainerStats(CLIExecutionContext& context)
942 }
943 case FormatType::Table:
944 {
986 - bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
945 + bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
946 using enum ColumnOverflow;
947
948 auto table = trunc ? wsl::windows::wslc::TableOutput<8>(
@@ -1035,8 +994,8 @@ void StartContainer(CLIExecutionContext& context)
994 {
995 WI_ASSERT(context.Data.Contains(Data::Session));
996 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
1038 - const auto& containerId = context.Args.Get<ArgType::ContainerId>();
1039 - const bool attach = context.Args.GetFlag<ArgType::Attach>();
997 + const auto& containerId = context.Args.GetValue<ArgType::ContainerId>();
998 + const bool attach = context.Args.GetValue<ArgType::Attach>();
999 context.ExitCode = ContainerService::Start(context.Terminal, context.Data.Get<Data::Session>(), WideToMultiByte(containerId), attach);
1000
1001 if (!attach)
@@ -1049,16 +1008,16 @@ void StopContainers(CLIExecutionContext& context)
1008 {
1009 WI_ASSERT(context.Data.Contains(Data::Session));
1010 auto& session = context.Data.Get<Data::Session>();
1052 - auto containersToStop = context.Args.GetAll<ArgType::ContainerId>();
1011 + auto containersToStop = context.Args.GetAllValues<ArgType::ContainerId>();
1012 StopContainerOptions options;
1013 if (context.Args.Contains(ArgType::Signal))
1014 {
1056 - options.Signal = validation::GetWSLCSignalFromString(context.Args.Get<ArgType::Signal>());
1015 + options.Signal = context.Args.GetValue<ArgType::Signal>();
1016 }
1017
1018 if (context.Args.Contains(ArgType::Time))
1019 {
1061 - options.Timeout = validation::GetIntegerFromString<LONG>(context.Args.Get<ArgType::Time>());
1020 + options.Timeout = context.Args.GetValue<ArgType::Time>();
1021 }
1022
1023 for (const auto& id : containersToStop)
@@ -1072,14 +1031,14 @@ void ViewContainerLogs(CLIExecutionContext& context)
1031 {
1032 WI_ASSERT(context.Data.Contains(Data::Session));
1033 auto& session = context.Data.Get<Data::Session>();
1075 - auto containerId = context.Args.Get<ArgType::ContainerId>();
1076 - bool follow = context.Args.GetFlag<ArgType::Follow>();
1077 - bool timestamps = context.Args.GetFlag<ArgType::Timestamps>();
1034 + auto containerId = context.Args.GetValue<ArgType::ContainerId>();
1035 + bool follow = context.Args.GetValue<ArgType::Follow>();
1036 + bool timestamps = context.Args.GetValue<ArgType::Timestamps>();
1037
1038 ULONGLONG tail = 0;
1039 if (context.Args.Contains(ArgType::Tail))
1040 {
1082 - tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>());
1041 + tail = context.Args.GetValue<ArgType::Tail>();
1042 }
1043
1044 // N.B. since=0 and until=0 mean "unset" — the Docker API omits the parameter when the value is 0,
@@ -1088,13 +1047,13 @@ void ViewContainerLogs(CLIExecutionContext& context)
1047 ULONGLONG since = 0;
1048 if (context.Args.Contains(ArgType::Since))
1049 {
1091 - since = validation::GetTimestampFromString(context.Args.Get<ArgType::Since>());
1050 + since = context.Args.GetValue<ArgType::Since>();
1051 }
1052
1053 ULONGLONG until = 0;
1054 if (context.Args.Contains(ArgType::Until))
1055 {
1097 - until = validation::GetTimestampFromString(context.Args.Get<ArgType::Until>());
1056 + until = context.Args.GetValue<ArgType::Until>();
1057 }
1058
1059 ContainerService::Logs(session, WideToMultiByte(containerId), follow, timestamps, since, until, tail);
src/windows/wslc/tasks/ImageTasks.cpp
+40 -76
@@ -12,7 +12,7 @@ Abstract:
12
13 --*/
14 #include "Argument.h"
15 -#include "ArgumentValidation.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "BuildImageCallback.h"
17 #include "CLIExecutionContext.h"
18 #include "ContainerService.h"
@@ -95,58 +95,44 @@ void BuildImage(CLIExecutionContext& context)
95 WI_ASSERT(context.Data.Contains(Data::Session));
96 WI_ASSERT(context.Args.Contains(ArgType::Path));
97 auto& session = context.Data.Get<Data::Session>();
98 - auto& contextPath = context.Args.Get<ArgType::Path>();
98 + auto& contextPath = context.Args.GetValue<ArgType::Path>();
99
100 - auto tags = context.Args.GetAll<ArgType::Tag>();
101 - auto buildArgs = context.Args.GetAll<ArgType::BuildArg>();
102 - auto labels = context.Args.GetAll<ArgType::Label>();
103 - for (const auto& label : labels)
104 - {
105 - validation::ParseLabel(label);
106 - }
107 -
108 - std::vector<services::BuildSecret> secrets;
109 - if (context.Args.Contains(ArgType::Secret))
110 - {
111 - for (const auto& spec : context.Args.GetAll<ArgType::Secret>())
112 - {
113 - secrets.push_back(validation::ParseSecretSpec(spec));
114 - }
115 - }
100 + auto tags = context.Args.GetAllValues<ArgType::Tag>();
101 + auto buildArgs = context.Args.GetAllValues<ArgType::BuildArg>();
102 + auto labels = context.Args.GetAllValues<ArgType::BuildLabel>();
103 + auto secrets = context.Args.GetAllValues<ArgType::Secret>();
104
105 std::wstring dockerfilePath;
106 if (context.Args.Contains(ArgType::File))
107 {
120 - dockerfilePath = context.Args.Get<ArgType::File>();
108 + dockerfilePath = context.Args.GetValue<ArgType::File>();
109 }
110
111 std::wstring target;
112 if (context.Args.Contains(ArgType::BuildTarget))
113 {
126 - target = context.Args.Get<ArgType::BuildTarget>();
114 + target = context.Args.GetValue<ArgType::BuildTarget>();
115 }
116
117 std::optional<services::BuildOutput> output;
130 - if (context.Args.Contains(ArgType::Output))
118 + if (context.Args.Contains(ArgType::BuildOutput))
119 {
132 - // Validate and normalize the spec client-side; ImageService::Build decides how to route the
133 - // exporter (stream a destination file/dir back over a handle, or run entirely in the VM).
134 - output = validation::ParseOutputSpec(context.Args.Get<ArgType::Output>());
120 + output = context.Args.GetValue<ArgType::BuildOutput>();
121 }
122
123 std::optional<std::wstring> iidFilePath;
124 if (context.Args.Contains(ArgType::IidFile))
125 {
140 - iidFilePath = context.Args.Get<ArgType::IidFile>();
126 + iidFilePath = context.Args.GetValue<ArgType::IidFile>();
127 }
128
129 WSLCBuildImageFlags flags = WSLCBuildImageFlagsNone;
144 - WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.GetFlag<ArgType::Verbose>());
145 - WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.GetFlag<ArgType::NoCache>());
146 - WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.GetFlag<ArgType::BuildPull>());
130 + WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.GetValue<ArgType::Verbose>());
131 + WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.GetValue<ArgType::NoCache>());
132 + WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.GetValue<ArgType::BuildPull>());
133
134 auto cancelEvent = context.CreateCancelEvent();
149 - BuildImageCallback callback(context.Terminal, cancelEvent, context.Args.GetFlag<ArgType::Verbose>());
135 + BuildImageCallback callback(context.Terminal, cancelEvent, context.Args.GetValue<ArgType::Verbose>());
136 services::ImageService::Build(
137 session, contextPath, tags, buildArgs, labels, secrets, dockerfilePath, target, output, iidFilePath, flags, &callback, cancelEvent);
138 }
@@ -156,19 +142,8 @@ void GetImages(CLIExecutionContext& context)
142 WI_ASSERT(context.Data.Contains(Data::Session));
143 auto& session = context.Data.Get<Data::Session>();
144
159 - // Filter syntax (`key=value`) is enforced upstream; here we just split on the first '='.
160 - std::vector<std::pair<std::string, std::string>> filters;
161 - if (context.Args.Contains(ArgType::Filter))
162 - {
163 - for (const auto& wideValue : context.Args.GetAll<ArgType::Filter>())
164 - {
165 - std::string raw = WideToMultiByte(wideValue);
166 - const auto eq = raw.find('=');
167 - WI_ASSERT(eq != std::string::npos);
168 -
169 - filters.emplace_back(raw.substr(0, eq), raw.substr(eq + 1));
170 - }
171 - }
145 + // Filter values are parsed and cached during argument validation.
146 + auto filters = context.Args.GetAllValues<ArgType::Filter>();
147
148 auto images = ImageService::List(session, filters);
149 context.Data.Add<Data::Images>(std::move(images));
@@ -179,9 +154,9 @@ void ListImages(CLIExecutionContext& context)
154 WI_ASSERT(context.Data.Contains(Data::Images));
155 auto& images = context.Data.Get<Data::Images>();
156
182 - if (context.Args.GetFlag<ArgType::Quiet>())
157 + if (context.Args.GetValue<ArgType::Quiet>())
158 {
184 - bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
159 + bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
160 for (const auto& image : images)
161 {
162 context.Terminal.Output(L"{}\n", trunc ? TruncateId(image.Id, true) : image.Id);
@@ -190,7 +165,7 @@ void ListImages(CLIExecutionContext& context)
165 return;
166 }
167
193 - FormatType format = validation::GetOutputFormat(context.Args);
168 + const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
169
170 switch (format)
171 {
@@ -202,7 +177,7 @@ void ListImages(CLIExecutionContext& context)
177 }
178 case FormatType::Table:
179 {
205 - bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
180 + bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
181 using enum ColumnOverflow;
182
183 // Create table — only IMAGE ID uses fixed width; other columns shrink to fit the console.
@@ -243,8 +218,8 @@ void PullImage(CLIExecutionContext& context)
218 WI_ASSERT(context.Data.Contains(Data::Session));
219 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
220 auto& session = context.Data.Get<Data::Session>();
246 - const auto image = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
247 - const bool quiet = context.Args.GetFlag<ArgType::Quiet>();
221 + const auto image = WideToMultiByte(context.Args.GetValue<ArgType::ImageId>());
222 + const bool quiet = context.Args.GetValue<ArgType::Quiet>();
223
224 // Match `docker pull`: for a name-only reference (no tag or digest) the tag defaults to "latest". Unless quiet,
225 // the client reports this on stdout before contacting the registry.
@@ -274,7 +249,7 @@ void PushImage(CLIExecutionContext& context)
249 WI_ASSERT(context.Data.Contains(Data::Session));
250 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
251 auto& session = context.Data.Get<Data::Session>();
277 - auto& imageId = context.Args.Get<ArgType::ImageId>();
252 + auto& imageId = context.Args.GetValue<ArgType::ImageId>();
253
254 ImageProgressCallback callback(context.Terminal, Terminal::Level::Output);
255 services::ImageService::Push(context.Terminal, session, WideToMultiByte(imageId), &callback);
@@ -284,9 +259,9 @@ void DeleteImage(CLIExecutionContext& context)
259 {
260 WI_ASSERT(context.Data.Contains(Data::Session));
261 auto& session = context.Data.Get<Data::Session>();
287 - const auto& imageIds = context.Args.GetAll<ArgType::ImageId>();
288 - bool force = context.Args.GetFlag<ArgType::ImageForce>();
289 - bool noPrune = context.Args.GetFlag<ArgType::NoPrune>();
262 + auto imageIds = context.Args.GetAllValues<ArgType::ImageId>();
263 + bool force = context.Args.GetValue<ArgType::ImageForce>();
264 + bool noPrune = context.Args.GetValue<ArgType::NoPrune>();
265 for (const auto& id : imageIds)
266 {
267 services::ImageService::Delete(session, WideToMultiByte(id), force, noPrune);
@@ -300,7 +275,7 @@ void LoadImage(CLIExecutionContext& context)
275
276 if (context.Args.Contains(ArgType::Input))
277 {
303 - auto& input = context.Args.Get<ArgType::Input>();
278 + auto& input = context.Args.GetValue<ArgType::Input>();
279 auto callback = wil::MakeOrThrow<WSLCImageLoadCallback>(context.Terminal);
280 services::ImageService::Load(context.Terminal, session, input, callback.Get());
281 return;
@@ -319,14 +294,14 @@ void ImportImage(CLIExecutionContext& context)
294 std::string imageName;
295 if (context.Args.Contains(ArgType::ImageId))
296 {
322 - imageName = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
297 + imageName = WideToMultiByte(context.Args.GetValue<ArgType::ImageId>());
298 }
299
325 - auto& input = context.Args.Get<ArgType::ImportFile>();
300 + auto& input = context.Args.GetValue<ArgType::ImportFile>();
301 auto imageId = services::ImageService::Import(context.Terminal, session, input, imageName);
302 if (!imageId.empty())
303 {
329 - bool trunc = !context.Args.GetFlag<ArgType::NoTrunc>();
304 + bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
305 context.Terminal.Output(L"{}\n", MultiByteToWide(TruncateId(imageId, trunc)));
306 }
307 }
@@ -336,7 +311,7 @@ void InspectImages(CLIExecutionContext& context)
311 WI_ASSERT(context.Data.Contains(Data::Session));
312 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
313 auto& session = context.Data.Get<Data::Session>();
339 - auto imageIds = context.Args.GetAll<ArgType::ImageId>();
314 + auto imageIds = context.Args.GetAllValues<ArgType::ImageId>();
315
316 std::vector<wsl::windows::common::wslc_schema::InspectImage> result;
317 for (const auto& id : imageIds)
@@ -352,7 +327,7 @@ void InspectImages(CLIExecutionContext& context)
327 }
328 }
329
355 - auto json = ToJson(result, validation::GetInspectJsonIndent(context.Args));
330 + auto json = ToJson(result, context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent));
331 context.Terminal.Output(L"{}\n", MultiByteToWide(json));
332 }
333
@@ -361,7 +336,7 @@ void SaveImage(CLIExecutionContext& context)
336 WI_ASSERT(context.Data.Contains(Data::Session));
337 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
338 auto& session = context.Data.Get<Data::Session>();
364 - auto imageIds = context.Args.GetAll<ArgType::ImageId>();
339 + auto imageIds = context.Args.GetAllValues<ArgType::ImageId>();
340
341 std::vector<std::string> images;
342 images.reserve(imageIds.size());
@@ -372,7 +347,7 @@ void SaveImage(CLIExecutionContext& context)
347
348 if (context.Args.Contains(ArgType::Output))
349 {
375 - auto& output = context.Args.Get<ArgType::Output>();
350 + auto& output = context.Args.GetValue<ArgType::Output>();
351 services::ImageService::Save(session, images, output, context.CreateCancelEvent());
352 }
353 else
@@ -391,8 +366,8 @@ void TagImage(CLIExecutionContext& context)
366 {
367 WI_ASSERT(context.Data.Contains(Data::Session));
368 auto& session = context.Data.Get<Data::Session>();
394 - auto& source = context.Args.Get<ArgType::Source>();
395 - auto& target = context.Args.Get<ArgType::Target>();
369 + auto& source = context.Args.GetValue<ArgType::Source>();
370 + auto& target = context.Args.GetValue<ArgType::Target>();
371 services::ImageService::Tag(session, WideToMultiByte(source), WideToMultiByte(target));
372 }
373
@@ -401,21 +376,10 @@ void PruneImages(CLIExecutionContext& context)
376 WI_ASSERT(context.Data.Contains(Data::Session));
377 auto& session = context.Data.Get<Data::Session>();
378
404 - bool all = context.Args.GetFlag<ArgType::All>();
379 + bool all = context.Args.GetValue<ArgType::All>();
380
406 - // Filter syntax (`key=value`) is enforced upstream; here we just split on the first '='.
407 - std::vector<std::pair<std::string, std::string>> filters;
408 - if (context.Args.Contains(ArgType::Filter))
409 - {
410 - for (const auto& wideValue : context.Args.GetAll<ArgType::Filter>())
411 - {
412 - std::string raw = WideToMultiByte(wideValue);
413 - const auto eq = raw.find('=');
414 - WI_ASSERT(eq != std::string::npos);
415 -
416 - filters.emplace_back(raw.substr(0, eq), raw.substr(eq + 1));
417 - }
418 - }
381 + // Filter values are parsed and cached during argument validation.
382 + auto filters = context.Args.GetAllValues<ArgType::Filter>();
383
384 auto result = ImageService::Prune(session, all, filters);
385
src/windows/wslc/tasks/InspectTasks.cpp
+4 -4
@@ -12,7 +12,7 @@ Abstract:
12 --*/
13
14 #include "Argument.h"
15 -#include "ArgumentValidation.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "InspectTasks.h"
17 #include "InspectModel.h"
18 #include "ImageService.h"
@@ -72,13 +72,13 @@ void Inspect(CLIExecutionContext& context)
72 {
73 WI_ASSERT(context.Data.Contains(Data::Session));
74 auto& session = context.Data.Get<Data::Session>();
75 - auto objectIds = context.Args.GetAll<ArgType::ObjectId>();
75 + auto objectIds = context.Args.GetAllValues<ArgType::ObjectId>();
76
77 nlohmann::json array = nlohmann::json::array();
78 auto type = InspectType::All;
79 if (context.Args.Contains(ArgType::Type))
80 {
81 - type = validation::GetInspectTypeFromString(context.Args.Get<ArgType::Type>(), L"type");
81 + type = context.Args.GetValue<ArgType::Type>();
82 }
83
84 for (const auto& objectId : objectIds)
@@ -113,6 +113,6 @@ void Inspect(CLIExecutionContext& context)
113 }
114
115 // Always print the array, even if it's empty or an error was encountered
116 - context.Terminal.Output(L"{}\n", MultiByteToWide(array.dump(validation::GetInspectJsonIndent(context.Args))));
116 + context.Terminal.Output(L"{}\n", MultiByteToWide(array.dump(context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent))));
117 }
118 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/NetworkTasks.cpp
+28 -31
@@ -12,7 +12,7 @@ Abstract:
12
13 --*/
14 #include "Argument.h"
15 -#include "ArgumentValidation.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "CLIExecutionContext.h"
17 #include "NetworkModel.h"
18 #include "NetworkService.h"
@@ -78,38 +78,38 @@ void CreateNetwork(CLIExecutionContext& context)
78 WI_ASSERT(context.Args.Contains(ArgType::NetworkName));
79
80 models::CreateNetworkOptions options{};
81 - options.Name = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
81 + options.Name = WideToMultiByte(context.Args.GetValue<ArgType::NetworkName>());
82
83 - for (const auto& option : context.Args.GetAll<ArgType::Options>())
83 + for (const auto& option : context.Args.GetAllValues<ArgType::Options>())
84 {
85 - options.DriverOpts.push_back(validation::ParseDriverOption(option));
85 + options.DriverOpts.push_back(option);
86 }
87
88 - for (const auto& label : context.Args.GetAll<ArgType::Label>())
88 + for (const auto& label : context.Args.GetAllValues<ArgType::Label>())
89 {
90 - options.Labels.push_back(validation::ParseLabel(label));
90 + options.Labels.push_back(label);
91 }
92
93 if (context.Args.Contains(ArgType::Driver))
94 {
95 - options.Driver = WideToMultiByte(context.Args.Get<ArgType::Driver>());
95 + options.Driver = WideToMultiByte(context.Args.GetValue<ArgType::Driver>());
96 }
97
98 - options.Internal = context.Args.GetFlag<ArgType::Internal>();
98 + options.Internal = context.Args.GetValue<ArgType::Internal>();
99
100 if (context.Args.Contains(ArgType::Subnet))
101 {
102 - options.Subnet = WideToMultiByte(context.Args.Get<ArgType::Subnet>());
102 + options.Subnet = WideToMultiByte(context.Args.GetValue<ArgType::Subnet>());
103 }
104
105 if (context.Args.Contains(ArgType::Gateway))
106 {
107 - options.Gateway = WideToMultiByte(context.Args.Get<ArgType::Gateway>());
107 + options.Gateway = WideToMultiByte(context.Args.GetValue<ArgType::Gateway>());
108 }
109
110 if (context.Args.Contains(ArgType::IpRange))
111 {
112 - options.IpRange = WideToMultiByte(context.Args.Get<ArgType::IpRange>());
112 + options.IpRange = WideToMultiByte(context.Args.GetValue<ArgType::IpRange>());
113 }
114
115 NetworkService::Create(context.Terminal, context.Data.Get<Data::Session>(), options);
@@ -120,8 +120,8 @@ void DeleteNetworks(CLIExecutionContext& context)
120 {
121 WI_ASSERT(context.Data.Contains(Data::Session));
122 auto& session = context.Data.Get<Data::Session>();
123 - auto networkNames = context.Args.GetAll<ArgType::NetworkName>();
124 - const bool force = context.Args.GetFlag<ArgType::Force>();
123 + auto networkNames = context.Args.GetAllValues<ArgType::NetworkName>();
124 + const bool force = context.Args.GetValue<ArgType::Force>();
125 for (const auto& name : networkNames)
126 {
127 if (TryDeleteNetwork(context.Terminal, session, WideToMultiByte(name), force))
@@ -146,7 +146,7 @@ void InspectNetworks(CLIExecutionContext& context)
146 {
147 WI_ASSERT(context.Data.Contains(Data::Session));
148 auto& session = context.Data.Get<Data::Session>();
149 - auto networkNames = context.Args.GetAll<ArgType::NetworkName>();
149 + auto networkNames = context.Args.GetAllValues<ArgType::NetworkName>();
150 std::vector<wsl::windows::common::wslc_schema::Network> result;
151 for (const auto& name : networkNames)
152 {
@@ -161,7 +161,7 @@ void InspectNetworks(CLIExecutionContext& context)
161 }
162 }
163
164 - auto json = ToJson(result, validation::GetInspectJsonIndent(context.Args));
164 + auto json = ToJson(result, context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent));
165 context.Terminal.Output(L"{}\n", MultiByteToWide(json));
166 }
167
@@ -170,7 +170,7 @@ void ListNetworks(CLIExecutionContext& context)
170 WI_ASSERT(context.Data.Contains(Data::Networks));
171 auto& networks = context.Data.Get<Data::Networks>();
172
173 - if (context.Args.GetFlag<ArgType::Quiet>())
173 + if (context.Args.GetValue<ArgType::Quiet>())
174 {
175 for (const auto& network : networks)
176 {
@@ -180,7 +180,7 @@ void ListNetworks(CLIExecutionContext& context)
180 return;
181 }
182
183 - FormatType format = validation::GetOutputFormat(context.Args);
183 + const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
184
185 switch (format)
186 {
@@ -215,11 +215,8 @@ void PruneNetworks(CLIExecutionContext& context)
215 WI_ASSERT(context.Data.Contains(Data::Session));
216 auto& session = context.Data.Get<Data::Session>();
217
218 - std::vector<std::pair<std::string, std::string>> filters;
219 - for (const auto& value : context.Args.GetAll<ArgType::Filter>())
220 - {
221 - filters.push_back(validation::ParseFilter(value));
222 - }
218 + // Filter values are parsed and cached during argument validation.
219 + auto filters = context.Args.GetAllValues<ArgType::Filter>();
220
221 auto result = NetworkService::Prune(session, filters);
222
@@ -238,8 +235,8 @@ void ConnectNetwork(CLIExecutionContext& context)
235
236 const auto& endpoint = context.Data.Get<Data::NetworkEndpointOptions>();
237 models::ConnectNetworkOptions options{};
241 - options.NetworkName = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
242 - options.ContainerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
238 + options.NetworkName = WideToMultiByte(context.Args.GetValue<ArgType::NetworkName>());
239 + options.ContainerId = WideToMultiByte(context.Args.GetValue<ArgType::ContainerId>());
240 options.Aliases = endpoint.Aliases;
241 options.IpAddress = endpoint.IpAddress;
242 options.Links = endpoint.Links;
@@ -255,8 +252,8 @@ void DisconnectNetwork(CLIExecutionContext& context)
252 WI_ASSERT(context.Args.Contains(ArgType::NetworkName));
253 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
254
258 - const auto networkName = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
259 - const auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
255 + const auto networkName = WideToMultiByte(context.Args.GetValue<ArgType::NetworkName>());
256 + const auto containerId = WideToMultiByte(context.Args.GetValue<ArgType::ContainerId>());
257 NetworkService::Disconnect(context.Data.Get<Data::Session>(), networkName, containerId);
258 }
259
@@ -264,27 +261,27 @@ void SetNetworkEndpointOptionsFromArgs(CLIExecutionContext& context)
261 {
262 models::NetworkEndpointOptions options{};
263
267 - for (const auto& alias : context.Args.GetAll<ArgType::NetworkAlias>())
264 + for (const auto& alias : context.Args.GetAllValues<ArgType::NetworkAlias>())
265 {
266 options.Aliases.emplace_back(WideToMultiByte(alias));
267 }
268
269 if (context.Args.Contains(ArgType::IpAddress))
270 {
274 - options.IpAddress = WideToMultiByte(context.Args.Get<ArgType::IpAddress>());
271 + options.IpAddress = WideToMultiByte(context.Args.GetValue<ArgType::IpAddress>());
272 }
273
277 - for (const auto& link : context.Args.GetAll<ArgType::Link>())
274 + for (const auto& link : context.Args.GetAllValues<ArgType::Link>())
275 {
276 options.Links.emplace_back(WideToMultiByte(link));
277 }
278
282 - for (const auto& linkLocalIp : context.Args.GetAll<ArgType::LinkLocalIp>())
279 + for (const auto& linkLocalIp : context.Args.GetAllValues<ArgType::LinkLocalIp>())
280 {
281 options.LinkLocalIps.emplace_back(WideToMultiByte(linkLocalIp));
282 }
283
287 - for (const auto& driverOpt : context.Args.GetAll<ArgType::DriverOpt>())
284 + for (const auto& driverOpt : context.Args.GetAllValues<ArgType::DriverOpt>())
285 {
286 options.DriverOpts.emplace_back(WideToMultiByte(driverOpt));
287 }
src/windows/wslc/tasks/RegistryTasks.cpp
+5 -4
@@ -12,6 +12,7 @@ Abstract:
12
13 --*/
14 #include "Argument.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "CLIExecutionContext.h"
17 #include "RegistryService.h"
18 #include "RegistryTasks.h"
@@ -33,14 +34,14 @@ void Login(CLIExecutionContext& context)
34
35 auto& session = context.Data.Get<Data::Session>();
36
36 - auto username = WideToMultiByte(context.Args.Get<ArgType::Username>());
37 - auto password = WideToMultiByte(context.Args.Get<ArgType::Password>());
37 + auto username = WideToMultiByte(context.Args.GetValue<ArgType::Username>());
38 + auto password = WideToMultiByte(context.Args.GetValue<ArgType::Password>());
39
40 auto serverAddress = std::string(RegistryService::DefaultServer);
41
42 if (context.Args.Contains(ArgType::Server))
43 {
43 - serverAddress = WideToMultiByte(context.Args.Get<ArgType::Server>());
44 + serverAddress = WideToMultiByte(context.Args.GetValue<ArgType::Server>());
45 }
46
47 auto [credUsername, credSecret] = RegistryService::Authenticate(session, serverAddress, username, password);
@@ -55,7 +56,7 @@ void Logout(CLIExecutionContext& context)
56
57 if (context.Args.Contains(ArgType::Server))
58 {
58 - serverAddress = WideToMultiByte(context.Args.Get<ArgType::Server>());
59 + serverAddress = WideToMultiByte(context.Args.GetValue<ArgType::Server>());
60 }
61
62 RegistryService::Erase(serverAddress);
src/windows/wslc/tasks/SessionTasks.cpp
+7 -6
@@ -12,6 +12,7 @@ Abstract:
12
13 --*/
14 #include "Argument.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "CLIExecutionContext.h"
17 #include "SessionService.h"
18 #include "SessionTasks.h"
@@ -37,7 +38,7 @@ void OpenSessionIfSpecified(CLIExecutionContext& context)
38 {
39 if (context.GlobalArgs.Contains(ArgType::Session))
40 {
40 - const auto& sessionName = context.GlobalArgs.Get<ArgType::Session>();
41 + const auto& sessionName = context.GlobalArgs.GetValue<ArgType::Session>();
42 context.Data.Add<Data::Session>(SessionService::OpenSession(sessionName));
43 }
44 }
@@ -67,7 +68,7 @@ void ResolveSession(CLIExecutionContext& context)
68 void ListSessions(CLIExecutionContext& context)
69 {
70 auto sessions = SessionService::List();
70 - if (context.Args.GetFlag<ArgType::Verbose>())
71 + if (context.Args.GetValue<ArgType::Verbose>())
72 {
73 const wchar_t* plural = sessions.size() == 1 ? L"" : L"s";
74 context.Terminal.Output(L"[wslc] Found {} session{}\n", sessions.size(), plural);
@@ -100,10 +101,10 @@ void RunInSession(CLIExecutionContext& context)
101 auto& session = context.Data.Get<Data::Session>();
102
103 std::vector<std::string> arguments;
103 - arguments.emplace_back(wsl::windows::common::string::WideToMultiByte(context.Args.Get<ArgType::Command>()));
104 + arguments.emplace_back(wsl::windows::common::string::WideToMultiByte(context.Args.GetValue<ArgType::Command>()));
105 if (context.Args.Contains(ArgType::ForwardArgs))
106 {
106 - for (const auto& arg : context.Args.Get<ArgType::ForwardArgs>())
107 + for (const auto& arg : context.Args.GetValue<ArgType::ForwardArgs>())
108 {
109 arguments.emplace_back(wsl::windows::common::string::WideToMultiByte(arg));
110 }
@@ -114,12 +115,12 @@ void RunInSession(CLIExecutionContext& context)
115
116 void EnterSession(CLIExecutionContext& context)
117 {
117 - auto storagePath = std::filesystem::absolute(context.Args.Get<ArgType::StoragePath>());
118 + auto storagePath = std::filesystem::absolute(context.Args.GetValue<ArgType::StoragePath>());
119
120 std::wstring sessionName;
121 if (context.Args.Contains(ArgType::Name))
122 {
122 - sessionName = context.Args.Get<ArgType::Name>();
123 + sessionName = context.Args.GetValue<ArgType::Name>();
124 }
125 else
126 {
src/windows/wslc/tasks/VolumeTasks.cpp
+16 -21
@@ -12,7 +12,7 @@ Abstract:
12
13 --*/
14 #include "Argument.h"
15 -#include "ArgumentValidation.h"
15 +#include "ArgumentConvertedTypes.h"
16 #include "CLIExecutionContext.h"
17 #include "VolumeModel.h"
18 #include "VolumeService.h"
@@ -79,24 +79,22 @@ void CreateVolume(CLIExecutionContext& context)
79 models::CreateVolumeOptions options{};
80 if (context.Args.Contains(ArgType::VolumeName))
81 {
82 - options.Name = WideToMultiByte(context.Args.Get<ArgType::VolumeName>());
82 + options.Name = WideToMultiByte(context.Args.GetValue<ArgType::VolumeName>());
83 }
84
85 - for (const auto& option : context.Args.GetAll<ArgType::Options>())
85 + for (const auto& option : context.Args.GetAllValues<ArgType::Options>())
86 {
87 - auto parsed = validation::ParseDriverOption(option);
88 - options.DriverOpts.emplace_back(parsed.first, parsed.second);
87 + options.DriverOpts.push_back(option);
88 }
89
91 - for (const auto& label : context.Args.GetAll<ArgType::Label>())
90 + for (const auto& label : context.Args.GetAllValues<ArgType::Label>())
91 {
93 - auto parsed = validation::ParseLabel(label);
94 - options.Labels.emplace_back(parsed.first, parsed.second);
92 + options.Labels.push_back(label);
93 }
94
95 if (context.Args.Contains(ArgType::Driver))
96 {
99 - options.Driver = WideToMultiByte(context.Args.Get<ArgType::Driver>());
97 + options.Driver = WideToMultiByte(context.Args.GetValue<ArgType::Driver>());
98 }
99
100 auto result = VolumeService::Create(context.Data.Get<Data::Session>(), options);
@@ -107,8 +105,8 @@ void DeleteVolumes(CLIExecutionContext& context)
105 {
106 WI_ASSERT(context.Data.Contains(Data::Session));
107 auto& session = context.Data.Get<Data::Session>();
110 - auto volumeNames = context.Args.GetAll<ArgType::VolumeName>();
111 - const bool force = context.Args.GetFlag<ArgType::Force>();
108 + auto volumeNames = context.Args.GetAllValues<ArgType::VolumeName>();
109 + const bool force = context.Args.GetValue<ArgType::Force>();
110 for (const auto& name : volumeNames)
111 {
112 if (TryDeleteVolume(context.Terminal, session, WideToMultiByte(name), force))
@@ -133,7 +131,7 @@ void InspectVolumes(CLIExecutionContext& context)
131 {
132 WI_ASSERT(context.Data.Contains(Data::Session));
133 auto& session = context.Data.Get<Data::Session>();
136 - auto volumeNames = context.Args.GetAll<ArgType::VolumeName>();
134 + auto volumeNames = context.Args.GetAllValues<ArgType::VolumeName>();
135 std::vector<wsl::windows::common::wslc_schema::InspectVolume> result;
136 for (const auto& name : volumeNames)
137 {
@@ -148,7 +146,7 @@ void InspectVolumes(CLIExecutionContext& context)
146 }
147 }
148
151 - auto json = ToJson(result, validation::GetInspectJsonIndent(context.Args));
149 + auto json = ToJson(result, context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent));
150 context.Terminal.Output(L"{}\n", MultiByteToWide(json));
151 }
152
@@ -157,7 +155,7 @@ void ListVolumes(CLIExecutionContext& context)
155 WI_ASSERT(context.Data.Contains(Data::Volumes));
156 auto& volumes = context.Data.Get<Data::Volumes>();
157
160 - if (context.Args.GetFlag<ArgType::Quiet>())
158 + if (context.Args.GetValue<ArgType::Quiet>())
159 {
160 for (const auto& volume : volumes)
161 {
@@ -167,7 +165,7 @@ void ListVolumes(CLIExecutionContext& context)
165 return;
166 }
167
170 - FormatType format = validation::GetOutputFormat(context.Args);
168 + const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
169
170 switch (format)
171 {
@@ -201,13 +199,10 @@ void PruneVolumes(CLIExecutionContext& context)
199 WI_ASSERT(context.Data.Contains(Data::Session));
200 auto& session = context.Data.Get<Data::Session>();
201
204 - const bool all = context.Args.GetFlag<ArgType::All>();
202 + const bool all = context.Args.GetValue<ArgType::All>();
203
206 - std::vector<std::pair<std::string, std::string>> filters;
207 - for (const auto& value : context.Args.GetAll<ArgType::Filter>())
208 - {
209 - filters.push_back(validation::ParseFilter(value));
210 - }
204 + // Filter values are parsed and cached during argument validation.
205 + auto filters = context.Args.GetAllValues<ArgType::Filter>();
206
207 auto result = VolumeService::Prune(context.Terminal, session, all, filters);
208
test/windows/wslc/ParserTestCases.h
+1 -1
@@ -179,7 +179,7 @@ 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 \
182 + * value (so it reads back via GetValue) 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)") \
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+453 -17
@@ -17,8 +17,10 @@ Abstract:
17 #include "WSLCCLITestHelpers.h"
18
19 #include "Argument.h"
20 -#include "ArgumentTypes.h"
20 +#include "ArgMap.h"
21 #include "ArgumentValidation.h"
22 +#include "ImageService.h"
23 +#include "JsonUtils.h"
24 #include "Exceptions.h"
25 #include <wslc.h>
26
@@ -31,6 +33,12 @@ using namespace WEX::Common;
33 using namespace WEX::TestExecution;
34
35 namespace WSLCCLIArgumentUnitTests {
36 +using RawArgMapBase = EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping, &ArgMapInvalidateValidatedCache>;
37 +
38 +static_assert(!std::is_convertible_v<ArgMap*, RawArgMapBase*>);
39 +static_assert(!std::is_copy_assignable_v<ArgMap>);
40 +static_assert(!std::is_move_assignable_v<ArgMap>);
41 +
42 class WSLCCLIArgumentUnitTests
43 {
44 WSLC_TEST_CLASS(WSLCCLIArgumentUnitTests)
@@ -168,11 +176,11 @@ class WSLCCLIArgumentUnitTests
176 VERIFY_IS_TRUE(argsContainer.Contains(ArgType::ForwardArgs));
177
178 // Verify basic retrieval
171 - auto retrievedBool = argsContainer.Get<ArgType::Help>();
179 + auto retrievedBool = argsContainer.GetValue<ArgType::Help>();
180 VERIFY_ARE_EQUAL(retrievedBool, true);
173 - auto retrievedString = argsContainer.Get<ArgType::ContainerId>();
181 + auto retrievedString = argsContainer.GetValue<ArgType::ContainerId>();
182 VERIFY_ARE_EQUAL(retrievedString, std::wstring(L"test"));
175 - auto retrievedStringSet = argsContainer.Get<ArgType::ForwardArgs>();
183 + auto retrievedStringSet = argsContainer.GetValue<ArgType::ForwardArgs>();
184 VERIFY_ARE_EQUAL(retrievedStringSet[0], std::wstring(L"test1"));
185 VERIFY_ARE_EQUAL(retrievedStringSet[1], std::wstring(L"test2"));
186
@@ -181,22 +189,25 @@ class WSLCCLIArgumentUnitTests
189 argsContainer.Add(ArgType::Publish, std::wstring(L"test2"));
190 argsContainer.Add(ArgType::Publish, std::wstring(L"test3"));
191 VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
184 - auto publishArgs = argsContainer.GetAll<ArgType::Publish>();
192 + auto publishArgs = argsContainer.GetAllValues<ArgType::Publish>();
193 VERIFY_ARE_EQUAL(publishArgs.size(), 3);
194 VERIFY_ARE_EQUAL(publishArgs[0], std::wstring(L"test1"));
195 VERIFY_ARE_EQUAL(publishArgs[1], std::wstring(L"test2"));
196 VERIFY_ARE_EQUAL(publishArgs[2], std::wstring(L"test3"));
197
198 // Verify Remove
191 - argsContainer.Remove(ArgType::Publish);
192 - VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 0);
199 + ArgMap removeArgs;
200 + removeArgs.Add<ArgType::Publish>(L"test");
201 + removeArgs.Remove(ArgType::Publish);
202 + VERIFY_ARE_EQUAL(removeArgs.Count(ArgType::Publish), 0);
203
204 // Verify compile time add works like runtime add for multimap types.
195 - argsContainer.Add<ArgType::Publish>(L"test1");
196 - argsContainer.Add<ArgType::Publish>(L"test2");
197 - argsContainer.Add<ArgType::Publish>(L"test3");
198 - VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
199 - publishArgs = argsContainer.GetAll<ArgType::Publish>();
205 + ArgMap compileTimeArgs;
206 + compileTimeArgs.Add<ArgType::Publish>(L"test1");
207 + compileTimeArgs.Add<ArgType::Publish>(L"test2");
208 + compileTimeArgs.Add<ArgType::Publish>(L"test3");
209 + VERIFY_ARE_EQUAL(compileTimeArgs.Count(ArgType::Publish), 3);
210 + publishArgs = compileTimeArgs.GetAllValues<ArgType::Publish>();
211 VERIFY_ARE_EQUAL(publishArgs.size(), 3);
212 VERIFY_ARE_EQUAL(publishArgs[0], std::wstring(L"test1"));
213 VERIFY_ARE_EQUAL(publishArgs[1], std::wstring(L"test2"));
@@ -216,12 +227,437 @@ class WSLCCLIArgumentUnitTests
227 VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
228 VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::ForwardArgs), 1);
229 VERIFY_ARE_EQUAL(argsContainer.GetCount(), 6); // 1 Help + 1 ContainerId + 3 Publish + 1 ForwardArgs
219 - argsContainer.Remove(ArgType::Help);
220 - argsContainer.Remove(ArgType::ContainerId);
221 - argsContainer.Remove(ArgType::Publish);
222 - argsContainer.Remove(ArgType::ForwardArgs);
223 - VERIFY_ARE_EQUAL(argsContainer.GetCount(), 0);
230 }
231 +
232 + // Test: Verify the validated-value cache stores and returns converted results so that a
233 + // conversion performed during validation is reused during execution. Access is by a compile-time
234 + // ArgType, so the value type is fixed by the argument's ConvertedType and cannot be mismatched.
235 + TEST_METHOD(ValidatedCache_StoresAndRetrievesConvertedValues)
236 + {
237 + ArgMap args;
238 +
239 + // Populate raw arguments so the validated-cache invariant (raw count == validated count,
240 + // enforced by a debug assert in the cache readers) holds when values are read below.
241 + args.Add(ArgType::StopTimeout, std::wstring(L"30"));
242 + args.Add(ArgType::Filter, std::wstring(L"status=running"));
243 + args.Add(ArgType::Filter, std::wstring(L"label=env=prod"));
244 +
245 + // Nothing cached yet.
246 + VERIFY_IS_FALSE(args.ContainsValidated(ArgType::StopTimeout));
247 +
248 + // A conversion that produces a non-string type (string -> int). The value type is fixed by
249 + // ArgType::StopTimeout's ConvertedType (int), so no type is supplied by the caller.
250 + args.AddValidated<ArgType::StopTimeout>(30);
251 + VERIFY_IS_TRUE(args.ContainsValidated(ArgType::StopTimeout));
252 + VERIFY_ARE_EQUAL(args.CountValidated(ArgType::StopTimeout), static_cast<size_t>(1));
253 + VERIFY_ARE_EQUAL(args.GetValue<ArgType::StopTimeout>(), 30);
254 +
255 + // Multiple cached values for one argument preserve insertion order.
256 + args.AddValidated<ArgType::Filter>(std::pair<std::string, std::string>{"status", "running"});
257 + args.AddValidated<ArgType::Filter>(std::pair<std::string, std::string>{"label", "env=prod"});
258 + VERIFY_ARE_EQUAL(args.CountValidated(ArgType::Filter), static_cast<size_t>(2));
259 + auto filters = args.GetAllValues<ArgType::Filter>();
260 + VERIFY_ARE_EQUAL(filters.size(), static_cast<size_t>(2));
261 + VERIFY_ARE_EQUAL(filters[0].first, std::string("status"));
262 + VERIFY_ARE_EQUAL(filters[0].second, std::string("running"));
263 + VERIFY_ARE_EQUAL(filters[1].first, std::string("label"));
264 + VERIFY_ARE_EQUAL(filters[1].second, std::string("env=prod"));
265 +
266 + // GetAllValidated returns empty when nothing is cached for the argument.
267 + auto empty = args.GetAllValues<ArgType::Signal>();
268 + VERIFY_IS_TRUE(empty.empty());
269 +
270 + // An absent argument resolves to its value type's default-constructed value.
271 + VERIFY_IS_FALSE(args.ContainsValidated(ArgType::Memory));
272 + VERIFY_ARE_EQUAL(args.CountValidated(ArgType::Memory), static_cast<size_t>(0));
273 + VERIFY_ARE_EQUAL(args.GetValue<ArgType::Memory>(), int64_t{});
274 + VERIFY_IS_FALSE(args.Contains(ArgType::Memory));
275 + }
276 +
277 + // Helper: run validation for a single-value argument and return the converted result (type fixed
278 + // by the argument's ConvertedType). Drives both paths for every converted ArgType its callers
279 + // exercise: the eager path (an explicit validation pass) and the on-demand path (a converted read
280 + // with no prior validation pass, which must self-validate). The returned value is the on-demand
281 + // result, so callers' expected-value assertions verify the on-demand output equals what the
282 + // validation pass produces. Both paths run the same Argument::Validate, so their results match by
283 + // construction; this asserts the on-demand trigger fires and caches an equal number of values.
284 + template <ArgType E>
285 + static auto ValidateAndGetCached(const std::wstring& raw)
286 + {
287 + ArgMap eager;
288 + eager.Add(E, std::wstring(raw));
289 + Argument::Create(E).Validate(eager);
290 + VERIFY_IS_TRUE(eager.ContainsValidated(E));
291 +
292 + ArgMap onDemand;
293 + onDemand.Add(E, std::wstring(raw));
294 + VERIFY_IS_FALSE(onDemand.ContainsValidated(E)); // no validation pass ran
295 + auto value = onDemand.GetValue<E>(); // triggers on-demand validation
296 + VERIFY_IS_TRUE(onDemand.ContainsValidated(E));
297 + VERIFY_ARE_EQUAL(onDemand.CountValidated(E), eager.CountValidated(E));
298 + return value;
299 + }
300 +
301 + // Helper: as ValidateAndGetCached, for an argument that appears multiple times (ArgMap is a
302 + // multimap). Runs the eager and on-demand paths and returns every on-demand converted value in
303 + // insertion order.
304 + template <ArgType E>
305 + static auto ValidateAndGetAllCached(const std::vector<std::wstring>& raws)
306 + {
307 + ArgMap eager;
308 + for (const auto& raw : raws)
309 + {
310 + eager.Add(E, std::wstring(raw));
311 + }
312 +
313 + Argument::Create(E).Validate(eager);
314 +
315 + // The cache must hold exactly one converted value per raw value in the map.
316 + VERIFY_ARE_EQUAL(eager.CountValidated(E), eager.Count(E));
317 + VERIFY_ARE_EQUAL(eager.CountValidated(E), raws.size());
318 +
319 + ArgMap onDemand;
320 + for (const auto& raw : raws)
321 + {
322 + onDemand.Add(E, std::wstring(raw));
323 + }
324 +
325 + VERIFY_IS_FALSE(onDemand.ContainsValidated(E)); // no validation pass ran
326 + auto values = onDemand.GetAllValues<E>(); // triggers on-demand validation
327 + VERIFY_ARE_EQUAL(onDemand.CountValidated(E), eager.CountValidated(E));
328 + return values;
329 + }
330 +
331 + // Test: Every ArgType whose validation converts its raw string into a typed value must cache
332 + // that value on the ArgMap during Argument::Validate, so execution reads it back without
333 + // re-converting. This drives the real validation + caching path for each converted ArgType.
334 + TEST_METHOD(ArgumentValidate_ConvertsAndCachesEveryConvertedArgType)
335 + {
336 + // string -> FormatType
337 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Format>(L"json"), FormatType::Json);
338 +
339 + // string -> json::dump() indentation
340 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::InspectFormat>(L"json"), wsl::shared::c_jsonCompactIndent);
341 +
342 + // string -> WSLCSignal (Signal and StopSignal share the converter)
343 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Signal>(L"SIGTERM"), WSLCSignalSIGTERM);
344 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::StopSignal>(L"SIGKILL"), WSLCSignalSIGKILL);
345 +
346 + // string -> int
347 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::StopTimeout>(L"30"), 30);
348 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::HealthRetries>(L"3"), 3);
349 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Last>(L"5"), 5);
350 +
351 + // string -> LONG
352 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Time>(L"5"), 5L);
353 +
354 + // string -> ULONGLONG (Tail is a raw integer; Since/Until go through the timestamp parser)
355 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Tail>(L"10"), 10ULL);
356 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Since>(L"100"), validation::GetTimestampFromString(L"100"));
357 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Until>(L"200"), validation::GetTimestampFromString(L"200"));
358 +
359 + // string -> int64_t (memory sizes). The cached value matches the converter's result.
360 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Memory>(L"512M"), validation::GetMemorySizeFromString(L"512M"));
361 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::ShmSize>(L"64M"), validation::GetMemorySizeFromString(L"64M"));
362 +
363 + // string -> int64_t (durations, in nanoseconds)
364 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::HealthInterval>(L"30s"), validation::GetDurationNanosFromString(L"30s"));
365 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::HealthTimeout>(L"30s"), validation::GetDurationNanosFromString(L"30s"));
366 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::HealthStartPeriod>(L"30s"), validation::GetDurationNanosFromString(L"30s"));
367 +
368 + // string -> int64_t (nano CPUs)
369 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Cpus>(L"1.5"), validation::GetNanoCpusFromString(L"1.5"));
370 +
371 + // string -> tuple<name, soft, hard> (ulimit)
372 + auto ulimit = ValidateAndGetCached<ArgType::Ulimit>(L"nofile=1024:2048");
373 + VERIFY_ARE_EQUAL(std::get<0>(ulimit), std::string("nofile"));
374 + VERIFY_ARE_EQUAL(std::get<1>(ulimit), 1024LL);
375 + VERIFY_ARE_EQUAL(std::get<2>(ulimit), 2048LL);
376 +
377 + // string -> pair<key, value> (filter). A single Validate call caches every raw value in order.
378 + {
379 + ArgMap args;
380 + args.Add(ArgType::Filter, std::wstring(L"status=running"));
381 + args.Add(ArgType::Filter, std::wstring(L"label=env=prod")); // split on first '='
382 + Argument::Create(ArgType::Filter).Validate(args);
383 + auto filters = args.GetAllValues<ArgType::Filter>();
384 + VERIFY_ARE_EQUAL(filters.size(), static_cast<size_t>(2));
385 + VERIFY_ARE_EQUAL(filters[0].first, std::string("status"));
386 + VERIFY_ARE_EQUAL(filters[0].second, std::string("running"));
387 + VERIFY_ARE_EQUAL(filters[1].first, std::string("label"));
388 + VERIFY_ARE_EQUAL(filters[1].second, std::string("env=prod"));
389 + }
390 +
391 + // string -> InspectType (inspect object type)
392 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Type>(L"container"), InspectType::Container);
393 +
394 + // string -> BuildOutput (docker-style build exporter spec)
395 + {
396 + auto output = ValidateAndGetCached<ArgType::BuildOutput>(L"type=tar,dest=-");
397 + VERIFY_ARE_EQUAL(output.Type, std::wstring(L"tar"));
398 + VERIFY_ARE_EQUAL(output.Dest, std::wstring(L"-"));
399 + }
400 +
401 + // string -> pair<key, value> (label and driver option share the key=value shape)
402 + {
403 + auto label = ValidateAndGetCached<ArgType::Label>(L"env=prod");
404 + VERIFY_ARE_EQUAL(label.first, std::string("env"));
405 + VERIFY_ARE_EQUAL(label.second, std::string("prod"));
406 +
407 + auto option = ValidateAndGetCached<ArgType::Options>(L"com.docker.network.bridge.name=br0");
408 + VERIFY_ARE_EQUAL(option.first, std::string("com.docker.network.bridge.name"));
409 + VERIFY_ARE_EQUAL(option.second, std::string("br0"));
410 + }
411 +
412 + // string -> BuildSecret (docker-style --secret spec resolved to an id and value bytes)
413 + {
414 + ScopedEnvVariable env(L"WSLC_UT_CONV_SECRET", L"conv-value");
415 + auto secret = ValidateAndGetCached<ArgType::Secret>(L"id=convtest,env=WSLC_UT_CONV_SECRET");
416 + VERIFY_ARE_EQUAL(secret.Id, std::wstring(L"convtest"));
417 + const std::string expected = "conv-value";
418 + VERIFY_IS_TRUE(std::vector<BYTE>(expected.begin(), expected.end()) == secret.Value);
419 + }
420 + }
421 +
422 + // Test: Because ArgMap is a multimap and any command may allow an argument to repeat, a single
423 + // Argument::Validate call must convert and cache every occurrence, in order. Covers the
424 + // different converter result shapes (integer, enum, tuple, pair). The helper also asserts the
425 + // cached count matches the number of raw values in the map.
426 + TEST_METHOD(ArgumentValidate_CachesEveryValueForRepeatedArg)
427 + {
428 + // Integer converter, multiple values -> all cached in order.
429 + auto retries = ValidateAndGetAllCached<ArgType::HealthRetries>({L"1", L"2", L"3"});
430 + VERIFY_ARE_EQUAL(retries.size(), static_cast<size_t>(3));
431 + VERIFY_ARE_EQUAL(retries[0], 1);
432 + VERIFY_ARE_EQUAL(retries[1], 2);
433 + VERIFY_ARE_EQUAL(retries[2], 3);
434 +
435 + // int64_t converter (memory sizes), multiple values -> all cached in order.
436 + auto memories = ValidateAndGetAllCached<ArgType::Memory>({L"128M", L"256M"});
437 + VERIFY_ARE_EQUAL(memories.size(), static_cast<size_t>(2));
438 + VERIFY_ARE_EQUAL(memories[0], validation::GetMemorySizeFromString(L"128M"));
439 + VERIFY_ARE_EQUAL(memories[1], validation::GetMemorySizeFromString(L"256M"));
440 +
441 + // Enum converter, multiple values -> all cached in order.
442 + auto signals = ValidateAndGetAllCached<ArgType::Signal>({L"SIGTERM", L"SIGKILL", L"SIGHUP"});
443 + VERIFY_ARE_EQUAL(signals.size(), static_cast<size_t>(3));
444 + VERIFY_ARE_EQUAL(signals[0], WSLCSignalSIGTERM);
445 + VERIFY_ARE_EQUAL(signals[1], WSLCSignalSIGKILL);
446 + VERIFY_ARE_EQUAL(signals[2], WSLCSignalSIGHUP);
447 +
448 + // Tuple converter (ulimit), multiple values -> all cached in order.
449 + auto ulimits = ValidateAndGetAllCached<ArgType::Ulimit>({L"nofile=1024:2048", L"nproc=512:1024"});
450 + VERIFY_ARE_EQUAL(ulimits.size(), static_cast<size_t>(2));
451 + VERIFY_ARE_EQUAL(std::get<0>(ulimits[0]), std::string("nofile"));
452 + VERIFY_ARE_EQUAL(std::get<1>(ulimits[0]), 1024LL);
453 + VERIFY_ARE_EQUAL(std::get<2>(ulimits[0]), 2048LL);
454 + VERIFY_ARE_EQUAL(std::get<0>(ulimits[1]), std::string("nproc"));
455 + VERIFY_ARE_EQUAL(std::get<1>(ulimits[1]), 512LL);
456 + VERIFY_ARE_EQUAL(std::get<2>(ulimits[1]), 1024LL);
457 +
458 + // Pair converter (filter), multiple values -> all cached in order.
459 + auto filters = ValidateAndGetAllCached<ArgType::Filter>({L"status=running", L"name=web", L"label=env=prod"});
460 + VERIFY_ARE_EQUAL(filters.size(), static_cast<size_t>(3));
461 + VERIFY_ARE_EQUAL(filters[0].first, std::string("status"));
462 + VERIFY_ARE_EQUAL(filters[0].second, std::string("running"));
463 + VERIFY_ARE_EQUAL(filters[1].first, std::string("name"));
464 + VERIFY_ARE_EQUAL(filters[1].second, std::string("web"));
465 + VERIFY_ARE_EQUAL(filters[2].first, std::string("label"));
466 + VERIFY_ARE_EQUAL(filters[2].second, std::string("env=prod"));
467 +
468 + // BuildSecret converter (secret specs), multiple values -> all cached in order.
469 + {
470 + ScopedEnvVariable envA(L"WSLC_UT_CONV_SECRET_A", L"value-a");
471 + ScopedEnvVariable envB(L"WSLC_UT_CONV_SECRET_B", L"value-b");
472 + auto secrets = ValidateAndGetAllCached<ArgType::Secret>(
473 + {L"id=seca,env=WSLC_UT_CONV_SECRET_A", L"id=secb,env=WSLC_UT_CONV_SECRET_B"});
474 + VERIFY_ARE_EQUAL(secrets.size(), static_cast<size_t>(2));
475 + VERIFY_ARE_EQUAL(secrets[0].Id, std::wstring(L"seca"));
476 + VERIFY_ARE_EQUAL(secrets[1].Id, std::wstring(L"secb"));
477 + const std::string expectedA = "value-a";
478 + const std::string expectedB = "value-b";
479 + VERIFY_IS_TRUE(std::vector<BYTE>(expectedA.begin(), expectedA.end()) == secrets[0].Value);
480 + VERIFY_IS_TRUE(std::vector<BYTE>(expectedB.begin(), expectedB.end()) == secrets[1].Value);
481 + }
482 + }
483 +
484 + // Test: Every validate-only ArgType (checked during validation but not converted into a
485 + // distinct typed value that execution consumes) must NOT populate the cache. Execution reads
486 + // the raw value for these instead.
487 + TEST_METHOD(ArgumentValidate_ValidateOnlyArgsAreNotCached)
488 + {
489 + struct Case
490 + {
491 + ArgType Type;
492 + std::wstring Value;
493 + };
494 +
495 + const std::vector<Case> cases = {
496 + {ArgType::Gpus, L"all"},
497 + {ArgType::Volume, LR"(C:\hostPath:/containerPath)"},
498 + {ArgType::WorkDir, L"/app"},
499 + {ArgType::Network, L"bridge"},
500 + {ArgType::NetworkAlias, L"myalias"},
501 + };
502 +
503 + for (const auto& c : cases)
504 + {
505 + ArgMap args;
506 + args.Add(c.Type, std::wstring(c.Value));
507 + Argument::Create(c.Type).Validate(args);
508 + VERIFY_IS_FALSE(args.ContainsValidated(c.Type));
509 + }
510 +
511 + // NoHealthcheck is a flag whose validation only rejects conflicting health options. With
512 + // no conflicts present it passes and caches nothing.
513 + ArgMap noHealthcheck;
514 + noHealthcheck.Add(ArgType::NoHealthcheck, true);
515 + Argument::Create(ArgType::NoHealthcheck).Validate(noHealthcheck);
516 + VERIFY_IS_FALSE(noHealthcheck.ContainsValidated(ArgType::NoHealthcheck));
517 + }
518 +
519 + // Test: When conversion fails during validation, Validate throws and nothing is cached.
520 + TEST_METHOD(ArgumentValidate_InvalidValueThrowsAndCachesNothing)
521 + {
522 + ArgMap args;
523 + args.Add(ArgType::Format, std::wstring(L"xml"));
524 + VERIFY_THROWS(Argument::Create(ArgType::Format).Validate(args), ArgumentException);
525 + VERIFY_IS_FALSE(args.ContainsValidated(ArgType::Format));
526 + }
527 +
528 + // Note: on-demand validation for every converted ArgType (reading with no prior validation pass
529 + // and getting the same result the pass produces) is covered by the tests above:
530 + // ValidateAndGetCached / ValidateAndGetAllCached drive both the eager and on-demand paths and
531 + // return the on-demand value, so those tests' expected-value assertions verify on-demand output
532 + // for all converted shapes. The tests below cover the behaviors unique to the on-demand trigger:
533 + // a bad value fails the same way as on the command line, a value added after the validation pass
534 + // is re-validated, and validate-only arguments (no converted value) are checked on demand too.
535 +
536 + // Test: An invalid value read on demand (no prior validation pass) throws ArgumentException --
537 + // the same failure the up-front validation pass raises for that value. This proves an argument
538 + // populated during execution routes to the same user error path as a bad command-line value,
539 + // and that a failed on-demand validation leaves nothing cached.
540 + TEST_METHOD(ArgumentValidate_OnDemandInvalidValueThrows)
541 + {
542 + ArgMap args;
543 + args.Add(ArgType::Format, std::wstring(L"xml")); // not a valid FormatType
544 + VERIFY_IS_FALSE(args.ContainsValidated(ArgType::Format));
545 + VERIFY_THROWS(args.GetValue<ArgType::Format>(), ArgumentException);
546 + VERIFY_IS_FALSE(args.ContainsValidated(ArgType::Format));
547 + }
548 +
549 + // Test: Raw values can change after the up-front validation pass until the argument is read.
550 + // The mutation invalidates the cache, and the first read validates the final values.
551 + TEST_METHOD(ArgumentValidate_PostValidationAddBeforeReadRevalidates)
552 + {
553 + ArgMap args;
554 + args.Add(ArgType::Signal, std::wstring(L"SIGTERM"));
555 + Argument::Create(ArgType::Signal).Validate(args);
556 + VERIFY_ARE_EQUAL(args.CountValidated(ArgType::Signal), static_cast<size_t>(1));
557 +
558 + // Add a second raw value before the first read. The map-action callback drops the cache.
559 + args.Add(ArgType::Signal, std::wstring(L"SIGKILL"));
560 + VERIFY_ARE_EQUAL(args.CountValidated(ArgType::Signal), static_cast<size_t>(0));
561 +
562 + // The first read re-validates both raw values on demand, in insertion order.
563 + auto signals = args.GetAllValues<ArgType::Signal>();
564 + VERIFY_ARE_EQUAL(signals.size(), static_cast<size_t>(2));
565 + VERIFY_ARE_EQUAL(signals[0], WSLCSignalSIGTERM);
566 + VERIFY_ARE_EQUAL(signals[1], WSLCSignalSIGKILL);
567 + VERIFY_ARE_EQUAL(args.CountValidated(ArgType::Signal), static_cast<size_t>(2));
568 + }
569 +
570 + // Test: A validate-only argument (checked during validation but not converted into a cached
571 + // value) is validated on demand when read, so a value added after the up-front pass is checked
572 + // exactly as a command-line value. These arguments have no converted cache, so the earlier
573 + // converted-path tests do not cover them; the read path still runs their range/format checks.
574 + // Network rejects "host" mode and unsupported values.
575 + TEST_METHOD(ArgumentValidate_OnDemandValidateOnlyArgIsChecked)
576 + {
577 + ArgMap labels;
578 + labels.Add(ArgType::BuildLabel, std::wstring(L"foo"));
579 + labels.Add(ArgType::BuildLabel, std::wstring(L"foo="));
580 + auto labelValues = labels.GetAllValues<ArgType::BuildLabel>();
581 + VERIFY_ARE_EQUAL(labelValues.size(), static_cast<size_t>(2));
582 + VERIFY_ARE_EQUAL(labelValues[0], std::wstring(L"foo"));
583 + VERIFY_ARE_EQUAL(labelValues[1], std::wstring(L"foo="));
584 +
585 + ArgMap invalidLabel;
586 + invalidLabel.Add(ArgType::BuildLabel, std::wstring(L"=value"));
587 + VERIFY_THROWS(invalidLabel.GetAllValues<ArgType::BuildLabel>(), wil::ResultException);
588 +
589 + // Valid value, no prior validation pass: the read validates on demand and returns the raw value.
590 + ArgMap valid;
591 + valid.Add(ArgType::Network, std::wstring(L"bridge"));
592 + auto networks = valid.GetAllValues<ArgType::Network>();
593 + VERIFY_ARE_EQUAL(networks.size(), static_cast<size_t>(1));
594 + VERIFY_ARE_EQUAL(networks[0], std::wstring(L"bridge"));
595 +
596 + // Invalid value, no prior validation pass: the read validates on demand and throws, matching
597 + // the failure the up-front pass raises for the same value.
598 + ArgMap invalid;
599 + invalid.Add(ArgType::Network, std::wstring(L"host"));
600 + VERIFY_THROWS(invalid.GetAllValues<ArgType::Network>(), ArgumentException);
601 +
602 + // Valid up-front, then an unsupported value added before the first read: the map-action
603 + // callback clears the validated record, so the read re-validates on demand and throws.
604 + ArgMap added;
605 + added.Add(ArgType::Network, std::wstring(L"bridge"));
606 + Argument::Create(ArgType::Network).Validate(added);
607 + added.Add(ArgType::Network, std::wstring(L"host"));
608 + VERIFY_THROWS(added.GetAllValues<ArgType::Network>(), ArgumentException);
609 + }
610 +
611 + TEST_METHOD(ArgumentValidate_ReadMakesArgumentImmutable)
612 + {
613 + ArgMap args;
614 + args.Add(ArgType::Signal, std::wstring(L"SIGTERM"));
615 + VERIFY_ARE_EQUAL(args.GetValue<ArgType::Signal>(), WSLCSignalSIGTERM);
616 +
617 + const auto verifyImmutableFailure = [](const auto& operation) {
618 + VERIFY_THROWS_SPECIFIC(operation(), wil::ResultException, [](const wil::ResultException& e) {
619 + return e.GetErrorCode() == E_ILLEGAL_METHOD_CALL;
620 + });
621 + };
622 +
623 + verifyImmutableFailure([&] { args.Add(ArgType::Signal, std::wstring(L"SIGKILL")); });
624 + verifyImmutableFailure([&] { args.Remove(ArgType::Signal); });
625 + verifyImmutableFailure([&] { args.InvalidateValidated(ArgType::Signal); });
626 + verifyImmutableFailure([&] { args.AddValidated<ArgType::Signal>(WSLCSignalSIGKILL); });
627 + verifyImmutableFailure([&] { args.MarkValidated(ArgType::Signal); });
628 +
629 + // Immutability is per argument; other arguments remain writable until they are read.
630 + args.Add(ArgType::StopTimeout, std::wstring(L"30"));
631 + VERIFY_ARE_EQUAL(args.GetValue<ArgType::StopTimeout>(), 30);
632 + }
633 +
634 + TEST_METHOD(ArgumentValidate_FlagReadValidatesAndMakesArgumentImmutable)
635 + {
636 + const auto verifyImmutableFailure = [](const auto& operation) {
637 + VERIFY_THROWS_SPECIFIC(operation(), wil::ResultException, [](const wil::ResultException& e) {
638 + return e.GetErrorCode() == E_ILLEGAL_METHOD_CALL;
639 + });
640 + };
641 +
642 + ArgMap absent;
643 + VERIFY_IS_FALSE(absent.GetValue<ArgType::Quiet>());
644 + VERIFY_IS_FALSE(absent.GetValue<ArgType::Quiet>(true));
645 + VERIFY_IS_FALSE(absent.GetValue<ArgType::Quiet>());
646 + VERIFY_IS_FALSE(absent.Contains(ArgType::Quiet));
647 + verifyImmutableFailure([&] { absent.Add(ArgType::Quiet, true); });
648 +
649 + ArgMap present;
650 + present.Add(ArgType::NoHealthcheck, true);
651 + present.Add(ArgType::HealthCmd, std::wstring(L"CMD echo healthy"));
652 + VERIFY_THROWS(present.GetValue<ArgType::NoHealthcheck>(), ArgumentException);
653 +
654 + // A failed read does not freeze the argument, so correcting the conflicting input permits
655 + // a subsequent successful read.
656 + present.Remove(ArgType::HealthCmd);
657 + VERIFY_IS_TRUE(present.GetValue<ArgType::NoHealthcheck>());
658 + verifyImmutableFailure([&] { present.Remove(ArgType::NoHealthcheck); });
659 + }
660 +
661 // Timestamp parsing unit tests (exercises TryParseRfc3339 and integer path via GetTimestampFromString)
662
663 TEST_METHOD(ValidateTimestamp_ValidUnixEpochSeconds)
test/windows/wslc/WSLCCLICommandUnitTests.cpp
+1 -1
@@ -250,7 +250,7 @@ class WSLCCLICommandUnitTests
250 {
251 // Build a lookup table from ArgType -> enum name string using the same X-macro.
252 static constexpr const wchar_t* c_argTypeNames[] = {
253 -#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, Desc) L## #EnumName,
253 +#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, ConvertedType, Desc) L## #EnumName,
254 WSLC_ARGUMENTS(WSLC_ARG_ENUM)
255 #undef WSLC_ARG_ENUM
256 };
test/windows/wslc/WSLCCLIEnvironmentOptionsUnitTests.cpp
+6 -5
@@ -17,7 +17,8 @@ Abstract:
17 #include "WSLCCLITestHelpers.h"
18
19 #include "Argument.h"
20 -#include "ArgumentTypes.h"
20 +#include "ArgumentConvertedTypes.h"
21 +#include "ArgMap.h"
22 #include "EnvironmentOptions.h"
23
24 using namespace wsl::windows::wslc;
@@ -57,7 +58,7 @@ class WSLCCLIEnvironmentOptionsUnitTests
58 ApplyEnvironmentOptions(target, NoColorDefs());
59
60 VERIFY_IS_TRUE(target.Contains(ArgType::NoColor));
60 - VERIFY_IS_TRUE(target.Get<ArgType::NoColor>());
61 + VERIFY_IS_TRUE(target.GetValue<ArgType::NoColor>());
62 }
63
64 // NO_COLOR spec: "0" / "false" / "no" / "off" are not opt-outs.
@@ -72,7 +73,7 @@ class WSLCCLIEnvironmentOptionsUnitTests
73
74 LogComment(std::wstring(L"NO_COLOR=") + value);
75 VERIFY_IS_TRUE(target.Contains(ArgType::NoColor));
75 - VERIFY_IS_TRUE(target.Get<ArgType::NoColor>());
76 + VERIFY_IS_TRUE(target.GetValue<ArgType::NoColor>());
77
78 m_noColor->Clear();
79 }
@@ -86,7 +87,7 @@ class WSLCCLIEnvironmentOptionsUnitTests
87 ApplyEnvironmentOptions(target, NoColorDefs());
88
89 VERIFY_IS_TRUE(target.Contains(ArgType::NoColor));
89 - VERIFY_IS_TRUE(target.Get<ArgType::NoColor>());
90 + VERIFY_IS_TRUE(target.GetValue<ArgType::NoColor>());
91 }
92
93 TEST_METHOD(ApplyEnvironmentOptions_NoColorAbsent_DoesNotSetFlag)
@@ -108,7 +109,7 @@ class WSLCCLIEnvironmentOptionsUnitTests
109 ApplyEnvironmentOptions(target, NoColorDefs());
110
111 VERIFY_ARE_EQUAL(1U, target.Count(ArgType::NoColor));
111 - VERIFY_IS_FALSE(target.Get<ArgType::NoColor>());
112 + VERIFY_IS_FALSE(target.GetValue<ArgType::NoColor>());
113 }
114
115 // Bindings outside definedArgs are ignored even if the env var is set.
test/windows/wslc/WSLCCLIParserUnitTests.cpp
+50 -51
@@ -17,7 +17,8 @@ Abstract:
17 #include "WSLCCLITestHelpers.h"
18
19 #include "Argument.h"
20 -#include "ArgumentTypes.h"
20 +#include "ArgumentConvertedTypes.h"
21 +#include "ArgMap.h"
22 #include "ArgumentParser.h"
23 #include "Invocation.h"
24 #include "ParserTestCases.h"
@@ -102,14 +103,14 @@ class WSLCCLIParserUnitTests
103 if (testCase.commandLine.find(L"image1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
104 {
105 VERIFY_IS_TRUE(args.Contains(ArgType::ImageId));
105 - auto imageId = args.Get<ArgType::ImageId>();
106 + auto imageId = args.GetValue<ArgType::ImageId>();
107 VERIFY_ARE_EQUAL(L"image1", imageId);
108 }
109
110 if (testCase.commandLine.find(L"cont1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::List)
111 {
112 VERIFY_IS_TRUE(args.Contains(ArgType::ContainerId));
112 - auto containerId = args.Get<ArgType::ContainerId>();
113 + auto containerId = args.GetValue<ArgType::ContainerId>();
114 VERIFY_ARE_EQUAL(L"cont1", containerId);
115 }
116
@@ -121,14 +122,14 @@ class WSLCCLIParserUnitTests
122 if (testCase.commandLine.find(L"command") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
123 {
124 VERIFY_IS_TRUE(args.Contains(ArgType::Command));
124 - auto command = args.Get<ArgType::Command>();
125 + auto command = args.GetValue<ArgType::Command>();
126 VERIFY_IS_TRUE(command.find(L"command") != std::wstring::npos);
127 }
128
129 if (testCase.commandLine.find(L"forward") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
130 {
131 VERIFY_IS_TRUE(args.Contains(ArgType::ForwardArgs));
131 - auto forwardArgs = args.Get<ArgType::ForwardArgs>();
132 + auto forwardArgs = args.GetValue<ArgType::ForwardArgs>();
133 std::wstring forwardArgsConcat = wsl::shared::string::Join(forwardArgs, L' ');
134 VERIFY_IS_TRUE(forwardArgsConcat.find(L"hello world") != std::wstring::npos); // Forward args should contain hello world
135 VERIFY_IS_TRUE(forwardArgsConcat.find(L"image1") == std::wstring::npos); // Forward args should not contain the imageId
@@ -139,7 +140,7 @@ class WSLCCLIParserUnitTests
140 if (testCase.commandLine.find(L"443") != std::wstring::npos)
141 {
142 VERIFY_IS_TRUE(args.Contains(ArgType::Publish));
142 - auto publishArgs = args.GetAll<ArgType::Publish>();
143 + auto publishArgs = args.GetAllValues<ArgType::Publish>();
144 VERIFY_ARE_EQUAL(2, publishArgs.size()); // Should have both publish args
145 VERIFY_ARE_NOT_EQUAL(publishArgs[0], publishArgs[1]); // Both publish args should be different
146 }
@@ -159,7 +160,7 @@ class WSLCCLIParserUnitTests
160 if (testCase.commandLine.find(L"--session") != std::wstring::npos)
161 {
162 VERIFY_IS_TRUE(args.Contains(ArgType::Session));
162 - VERIFY_ARE_EQUAL(std::wstring(L"foo"), args.Get<ArgType::Session>());
163 + VERIFY_ARE_EQUAL(std::wstring(L"foo"), args.GetValue<ArgType::Session>());
164 }
165 }
166 }
@@ -266,7 +267,7 @@ class WSLCCLIParserUnitTests
267 }
268
269 VERIFY_IS_TRUE(args.Contains(ArgType::Signal));
269 - VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
270 + VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, args.GetValue<ArgType::Signal>());
271 VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
272 }
273
@@ -284,7 +285,7 @@ class WSLCCLIParserUnitTests
285 }
286
287 VERIFY_IS_TRUE(args.Contains(ArgType::Signal));
287 - VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
288 + VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, args.GetValue<ArgType::Signal>());
289 VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
290 }
291
@@ -369,9 +370,9 @@ class WSLCCLIParserUnitTests
370 }
371
372 VERIFY_IS_TRUE(subArgs.Contains(ArgType::ImageId));
372 - VERIFY_ARE_EQUAL(std::wstring(L"image1"), subArgs.Get<ArgType::ImageId>());
373 + VERIFY_ARE_EQUAL(std::wstring(L"image1"), subArgs.GetValue<ArgType::ImageId>());
374 VERIFY_IS_TRUE(subArgs.Contains(ArgType::Signal));
374 - VERIFY_ARE_EQUAL(std::wstring(L"9"), subArgs.Get<ArgType::Signal>());
375 + VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, subArgs.GetValue<ArgType::Signal>());
376 }
377
378 // stopOnUnknown: unknown -alias / --name / lone '-' / bare '--' tokens
@@ -466,7 +467,7 @@ class WSLCCLIParserUnitTests
467 sm.ThrowIfError();
468
469 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
469 - VERIFY_ARE_EQUAL(std::wstring(L"9"), args.Get<ArgType::Signal>());
470 + VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, args.GetValue<ArgType::Signal>());
471 }
472
473 // A preloaded (env-style) default followed by multiple CLI values collapses to the
@@ -488,7 +489,7 @@ class WSLCCLIParserUnitTests
489 sm.ThrowIfError();
490
491 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
491 - VERIFY_ARE_EQUAL(std::wstring(L"1"), args.Get<ArgType::Signal>());
492 + VERIFY_ARE_EQUAL(WSLCSignalSIGHUP, args.GetValue<ArgType::Signal>());
493 }
494
495 // Preloaded flag default plus CLI mention of the same flag stays a single
@@ -510,7 +511,7 @@ class WSLCCLIParserUnitTests
511 sm.ThrowIfError();
512
513 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
513 - VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
514 + VERIFY_IS_TRUE(args.GetValue<ArgType::Verbose>());
515 }
516
517 // Duplicate flag on the CLI (no env preload) folds to one entry: docker-style.
@@ -529,7 +530,7 @@ class WSLCCLIParserUnitTests
530 sm.ThrowIfError();
531
532 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
532 - VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
533 + VERIFY_IS_TRUE(args.GetValue<ArgType::Verbose>());
534 }
535
536 // Duplicate single-value arg on the CLI (no preload) is last-wins (docker-style):
@@ -549,7 +550,7 @@ class WSLCCLIParserUnitTests
550 sm.ThrowIfError();
551
552 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
552 - VERIFY_ARE_EQUAL(std::wstring(L"1"), args.Get<ArgType::Signal>());
553 + VERIFY_ARE_EQUAL(WSLCSignalSIGHUP, args.GetValue<ArgType::Signal>());
554 }
555
556 // Unlimited value args are exempt from last-wins: every CLI occurrence accumulates.
@@ -561,7 +562,7 @@ class WSLCCLIParserUnitTests
562 }
563
564 // 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 + // flag is specified, absent when it is not. Consumers read them via ArgMap::GetValue(defaultValue),
566 // which returns the stored value if present or a caller-supplied default if absent. The
567 // helper parses a single command line against the supplied defs and returns the resulting
568 // ArgMap so each case can assert the stored flag value.
@@ -596,13 +597,12 @@ class WSLCCLIParserUnitTests
597
598 VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
599 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
599 - VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
600 - VERIFY_IS_TRUE(args.GetFlag<ArgType::Verbose>());
600 + VERIFY_IS_TRUE(args.GetValue<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
605 + // "--flag=false"), so Contains() is true but GetValue() 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 {
@@ -614,8 +614,7 @@ class WSLCCLIParserUnitTests
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>());
617 + VERIFY_IS_FALSE(args.GetValue<ArgType::Verbose>());
618 }
619 }
620
@@ -654,26 +653,26 @@ class WSLCCLIParserUnitTests
653 L"wslc --verbose true", {Argument::Create(ArgType::Verbose), Argument::Create(ArgType::ContainerId, false, Limit::Unlimited)});
654
655 VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
657 - VERIFY_IS_TRUE(args.Get<ArgType::Verbose>());
656 + VERIFY_IS_TRUE(args.GetValue<ArgType::Verbose>());
657 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::ContainerId));
659 - VERIFY_ARE_EQUAL(std::wstring(L"true"), args.Get<ArgType::ContainerId>());
658 + VERIFY_ARE_EQUAL(std::wstring(L"true"), args.GetValue<ArgType::ContainerId>());
659 }
660
661 // Alias forms honor adjoined booleans just like the long name.
662 TEST_METHOD(Flag_AliasAdjoinedBoolean)
663 {
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>());
664 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
665 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=true", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
666 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=false", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
667 }
668
669 // Docker-parity single-letter forms ("t"/"T"/"f"/"F") are honored on the alias form too.
670 TEST_METHOD(Flag_AliasShortBooleanForms)
671 {
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>());
672 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=t", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
673 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=T", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
674 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=f", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
675 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=F", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
676 }
677
678 // An adjoined boolean value may be wrapped in double quotes (e.g. --flag="true"), just like
@@ -681,12 +680,12 @@ class WSLCCLIParserUnitTests
680 // the named and alias forms.
681 TEST_METHOD(Flag_QuotedAdjoinedBoolean)
682 {
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>());
683 + VERIFY_IS_TRUE(ParseFlags(L"wslc --verbose=\"true\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
684 + VERIFY_IS_FALSE(ParseFlags(L"wslc --verbose=\"false\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
685 + VERIFY_IS_TRUE(ParseFlags(L"wslc -q=\"true\"", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
686 + VERIFY_IS_FALSE(ParseFlags(L"wslc -q=\"false\"", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
687 + VERIFY_IS_TRUE(ParseFlags(L"wslc --verbose=\"t\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
688 + VERIFY_IS_FALSE(ParseFlags(L"wslc --verbose=\"f\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
689 }
690
691 // In an alias chain, leading flags are true and a trailing "=false" turns only the
@@ -696,25 +695,25 @@ class WSLCCLIParserUnitTests
695 std::vector<Argument> defs = {Argument::Create(ArgType::Quiet), Argument::Create(ArgType::Interactive)};
696
697 ArgMap all = ParseFlags(L"wslc -qi", defs);
699 - VERIFY_IS_TRUE(all.GetFlag<ArgType::Quiet>());
700 - VERIFY_IS_TRUE(all.GetFlag<ArgType::Interactive>());
698 + VERIFY_IS_TRUE(all.GetValue<ArgType::Quiet>());
699 + VERIFY_IS_TRUE(all.GetValue<ArgType::Interactive>());
700
701 ArgMap trailingFalse = ParseFlags(L"wslc -qi=false", defs);
703 - VERIFY_IS_TRUE(trailingFalse.GetFlag<ArgType::Quiet>());
704 - VERIFY_IS_FALSE(trailingFalse.GetFlag<ArgType::Interactive>());
702 + VERIFY_IS_TRUE(trailingFalse.GetValue<ArgType::Quiet>());
703 + VERIFY_IS_FALSE(trailingFalse.GetValue<ArgType::Interactive>());
704 }
705
706 // Repeated flags are last-wins (matching docker) and never accumulate multiple entries:
707 // "--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.
708 + // way (a single entry), so GetValue() reports the winning value.
709 TEST_METHOD(Flag_Repeated_LastWins)
710 {
711 ArgMap trueThenFalse = ParseFlags(L"wslc --verbose --verbose=false", {Argument::Create(ArgType::Verbose)});
713 - VERIFY_IS_FALSE(trueThenFalse.GetFlag<ArgType::Verbose>());
712 + VERIFY_IS_FALSE(trueThenFalse.GetValue<ArgType::Verbose>());
713 VERIFY_ARE_EQUAL(1u, trueThenFalse.Count(ArgType::Verbose));
714
715 ArgMap falseThenTrue = ParseFlags(L"wslc --verbose=false --verbose", {Argument::Create(ArgType::Verbose)});
717 - VERIFY_IS_TRUE(falseThenTrue.GetFlag<ArgType::Verbose>());
716 + VERIFY_IS_TRUE(falseThenTrue.GetValue<ArgType::Verbose>());
717 VERIFY_ARE_EQUAL(1u, falseThenTrue.Count(ArgType::Verbose));
718
719 ArgMap duplicateTrue = ParseFlags(L"wslc --verbose --verbose=true", {Argument::Create(ArgType::Verbose)});
@@ -722,7 +721,7 @@ class WSLCCLIParserUnitTests
721 }
722
723 // "--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.
724 + // single stored false rather than leaving a lingering true. GetValue() then reports false.
725 TEST_METHOD(Flag_FalseOverridesPreloadedDefault)
726 {
727 auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose=false");
@@ -741,27 +740,27 @@ class WSLCCLIParserUnitTests
740
741 VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
742 VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
744 - VERIFY_IS_FALSE(args.GetFlag<ArgType::Verbose>());
743 + VERIFY_IS_FALSE(args.GetValue<ArgType::Verbose>());
744 }
745
747 - // A flag whose behavior is on by default is read with GetFlag(true): absent yields the
746 + // A flag whose behavior is on by default is read with GetValue(true): absent yields the
747 // default (true), "--flag=false" yields false, and "--flag" yields true. A bare Contains()
748 // cannot express this: it reports true for both "--flag" and "--flag=false" and false when
749 // the flag is absent, so it distinguishes neither the two stored values nor absent-as-default.
751 - TEST_METHOD(Flag_GetFlagDefaultTrue_DefaultOnFlag)
750 + TEST_METHOD(Flag_GetValueDefaultTrue_DefaultOnFlag)
751 {
752 std::vector<Argument> defs = {Argument::Create(ArgType::Remove)};
753
754 ArgMap absent = ParseFlags(L"wslc", defs);
755 VERIFY_IS_FALSE(absent.Contains(ArgType::Remove));
757 - VERIFY_IS_TRUE(absent.GetFlag<ArgType::Remove>(true));
756 + VERIFY_IS_TRUE(absent.GetValue<ArgType::Remove>(true));
757
758 ArgMap disabled = ParseFlags(L"wslc --rm=false", defs);
759 VERIFY_IS_TRUE(disabled.Contains(ArgType::Remove));
761 - VERIFY_IS_FALSE(disabled.GetFlag<ArgType::Remove>(true));
760 + VERIFY_IS_FALSE(disabled.GetValue<ArgType::Remove>(true));
761
762 ArgMap enabled = ParseFlags(L"wslc --rm", defs);
764 - VERIFY_IS_TRUE(enabled.GetFlag<ArgType::Remove>(true));
763 + VERIFY_IS_TRUE(enabled.GetValue<ArgType::Remove>(true));
764 }
765 };
766