master
cpp 541 lines 17.9 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 ArgumentParser.cpp
8
9 Abstract:
10
11 Implementation of the ArgumentParser class.
12
13 --*/
14 #include "ArgumentParser.h"
15 #include "Localization.h"
16
17 using namespace wsl::shared;
18
19 namespace wsl::windows::wslc {
20
21 ParseArgumentsStateMachine::ParseArgumentsStateMachine(
22 Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments, bool optionsOnly, bool stopOnUnknown, const std::vector<Argument>& overridableDefaults) :
23 m_invocation(inv),
24 m_executionArgs(execArgs),
25 m_arguments(std::move(arguments)),
26 m_invocationItr(m_invocation.begin()),
27 m_optionsOnly(optionsOnly),
28 m_stopOnUnknown(stopOnUnknown)
29 {
30 for (const auto& arg : m_arguments)
31 {
32 switch (arg.Kind())
33 {
34 case Kind::Value:
35 m_standardArgs.emplace_back(arg);
36 break;
37 case Kind::Flag:
38 m_standardArgs.emplace_back(arg);
39 break;
40 case Kind::Positional:
41 m_positionalArgs.emplace_back(arg);
42 break;
43 case Kind::Forward:
44 m_forwardArgs.emplace_back(arg);
45 break;
46 }
47 }
48
49 m_positionalSearchItr = m_positionalArgs.begin();
50
51 m_overridableDefaults.reserve(overridableDefaults.size());
52 for (const auto& arg : overridableDefaults)
53 {
54 m_overridableDefaults.push_back(arg.Type());
55 }
56 }
57
58 bool ParseArgumentsStateMachine::Step()
59 {
60 if (m_stopped || m_invocationItr == m_invocation.end())
61 {
62 return false;
63 }
64
65 m_state = StepInternal();
66 return true;
67 }
68
69 void ParseArgumentsStateMachine::ThrowIfError() const
70 {
71 if (m_state.Exception())
72 {
73 throw m_state.Exception().value();
74 }
75 // If the next argument was to be a value, but none was provided, convert it to an exception.
76 else if (m_state.Type() && m_invocationItr == m_invocation.end())
77 {
78 const auto* argument = FindArgument(m_state.Type().value());
79 const auto message = Localization::WSLCCLI_MissingArgumentError(m_state.Arg());
80 throw argument != nullptr ? ArgumentException(message, *argument) : ArgumentException(message);
81 }
82 }
83
84 void ParseArgumentsStateMachine::AdvanceToNextPositional(std::vector<Argument>::iterator& itr) const
85 {
86 // Skip positionals that are already full. A single-value positional is full once it
87 // holds one value; an unlimited positional is never full.
88 while (itr != m_positionalArgs.end() && itr->IsSingle() && m_executionArgs.Count(itr->Type()) >= 1)
89 {
90 ++itr;
91 }
92 }
93
94 const Argument* ParseArgumentsStateMachine::NextPositional()
95 {
96 AdvanceToNextPositional(m_positionalSearchItr);
97 return m_positionalSearchItr != m_positionalArgs.end() ? &*m_positionalSearchItr : nullptr;
98 }
99
100 bool ParseArgumentsStateMachine::HasNextPositional() const
101 {
102 auto itr = m_positionalSearchItr;
103 AdvanceToNextPositional(itr);
104 return itr != m_positionalArgs.end();
105 }
106
107 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::BackUpAndStop()
108 {
109 --m_invocationItr;
110 m_stopped = true;
111 return {};
112 }
113
114 bool ParseArgumentsStateMachine::ConsumeOverrideIfPresent(ArgType type)
115 {
116 auto it = std::find(m_overridableDefaults.begin(), m_overridableDefaults.end(), type);
117 if (it == m_overridableDefaults.end())
118 {
119 return false;
120 }
121
122 m_executionArgs.Remove(type);
123 m_overridableDefaults.erase(it);
124 return true;
125 }
126
127 void ParseArgumentsStateMachine::ClearArgument(ArgType type)
128 {
129 // Drop any preloaded overridable default and remove previously parsed entries so the
130 // argument is left absent. This is the single-value/last-wins primitive shared by
131 // SetFlag (which then stores the flag's explicit value) and AddValue (single-value args).
132 ConsumeOverrideIfPresent(type);
133 m_executionArgs.Remove(type);
134 }
135
136 void ParseArgumentsStateMachine::SetFlag(ArgType type, bool value)
137 {
138 // Boolean flags store their explicit parsed value (true or false) so a flag whose behavior
139 // is on by default can be turned off with "--flag=false". Clearing first collapses CLI
140 // duplicates to a single entry and gives docker's last-wins behavior for repeated flags
141 // (e.g. "--flag --flag=false" ends up false). Read flags back via ArgMap::GetValue(defaultValue), which
142 // folds the presence check and the stored value into one test, rather than a bare Contains().
143 ClearArgument(type);
144 m_executionArgs.Add(type, value);
145 }
146
147 std::wstring_view ParseArgumentsStateMachine::StripSurroundingQuotes(std::wstring_view value)
148 {
149 if (value.length() >= 2 && value.front() == L'"' && value.back() == L'"')
150 {
151 value = value.substr(1, value.length() - 2);
152 }
153
154 return value;
155 }
156
157 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ApplyFlagValue(ArgType type, std::wstring_view value, const std::wstring_view& currArg)
158 {
159 const auto unquoted = StripSurroundingQuotes(value);
160 const auto boolVal = string::ParseBool(std::wstring(unquoted).c_str(), /*AllowExtendedForms*/ true);
161 if (!boolVal.has_value())
162 {
163 const auto* argument = FindArgument(type);
164 const auto message = Localization::WSLCCLI_FlagInvalidBooleanError(currArg);
165 return argument != nullptr ? ArgumentException(message, *argument) : ArgumentException(message);
166 }
167
168 SetFlag(type, boolVal.value());
169 return {};
170 }
171
172 const Argument* ParseArgumentsStateMachine::FindArgument(ArgType type) const
173 {
174 for (const auto& arg : m_arguments)
175 {
176 if (arg.Type() == type)
177 {
178 return &arg;
179 }
180 }
181
182 return nullptr;
183 }
184
185 void ParseArgumentsStateMachine::AddValue(ArgType type, std::wstring value)
186 {
187 const Argument* arg = FindArgument(type);
188 WI_ASSERT(arg != nullptr);
189
190 // Unlimited value args accumulate; single-value args are last-wins. In both cases the
191 // first CLI value must displace a preloaded overridable default, which ClearArgument
192 // (single) and ConsumeOverrideIfPresent (unlimited) each handle.
193 if (arg != nullptr && arg->IsUnlimited())
194 {
195 ConsumeOverrideIfPresent(type);
196 }
197 else
198 {
199 ClearArgument(type);
200 }
201
202 m_executionArgs.Add(type, std::move(value));
203 }
204
205 // Parse rules:
206 // 1. Token starting with a single '-' is an alias (1-2 chars):
207 // a. Value: '-a=VALUE' / '-ab=VALUE' / '-a VALUE' / '-ab VALUE'
208 // b. Flag: trailing chars are additional flags; fails if any is non-flag.
209 // 2. Token starting with '--' is the full name: '--arg=VALUE' or '--arg VALUE'.
210 // 3. Anything else is the next positional.
211 // 4. Once a positional is seen, everything after stays positional.
212 // 5. If only one positional is defined, everything after it is forwarded.
213 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
214 {
215 auto currArg = std::wstring_view{*m_invocationItr};
216 ++m_invocationItr;
217
218 // Pending value from the previous token.
219 if (m_state.Type())
220 {
221 AddValue(m_state.Type().value(), std::wstring{currArg});
222 return {};
223 }
224
225 // Anchored: remaining tokens are positional or forwarded.
226 if (!m_forwardArgs.empty() && m_anchorPositional.has_value())
227 {
228 return ProcessAnchoredPositionals(currArg);
229 }
230
231 // Arg does not begin with '-' so it is neither an alias nor a named value, must be positional.
232 if (currArg.empty() || currArg[0] != WSLC_CLI_ARG_ID_CHAR)
233 {
234 if (m_optionsOnly)
235 {
236 // Options-only mode: stop cleanly at the first positional token without
237 // consuming it so the caller can resume parsing (e.g. subcommand resolution).
238 return BackUpAndStop();
239 }
240
241 return ProcessPositionalArgument(currArg);
242 }
243
244 // The currentArg is non-empty, and starts with a -.
245 if (currArg.length() == 1)
246 {
247 if (HasNextPositional())
248 {
249 // The '-' character may be a valid positional argument value (ex: stdin), so treat this
250 // as a positional argument if there are any positionals left to fill.
251 return ProcessPositionalArgument(currArg);
252 }
253
254 // No positional argument remaining. In stopOnUnknown mode this token isn't ours;
255 // back up and let the next pass deal with it.
256 if (m_stopOnUnknown)
257 {
258 return BackUpAndStop();
259 }
260
261 return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
262 }
263
264 // Single '-' that is 2 characters or more means this must be an alias or collection of alias flags.
265 if (currArg[1] != WSLC_CLI_ARG_ID_CHAR)
266 {
267 return ProcessAliasArgument(currArg);
268 }
269
270 // The currentArg must be a named argument.
271 return ProcessNamedArgument(currArg);
272 }
273
274 // Assumes non-empty.
275 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessPositionalArgument(const std::wstring_view& currArg)
276 {
277 WI_ASSERT(!currArg.empty());
278
279 const Argument* nextPositional = NextPositional();
280 if (!nextPositional)
281 {
282 return ArgumentException(Localization::WSLCCLI_ExtraPositionalError(currArg));
283 }
284
285 // First positional found is the anchor positional.
286 if (!m_anchorPositional.has_value())
287 {
288 m_anchorPositional = Argument(*nextPositional);
289 }
290
291 m_executionArgs.Add(nextPositional->Type(), std::wstring{currArg});
292 return {};
293 }
294
295 // Assumes one positional has already been found and therefore there are no remaining Kind Value/Flag arguments.
296 // Only Kind::Positional or Kind::Forward arguments should remain.
297 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPositionals(const std::wstring_view& currArg)
298 {
299 WI_ASSERT(m_anchorPositional.has_value());
300
301 // If we haven't reached the limit for the anchor positional, treat this as another anchor positional.
302 // Unlimited anchors are never full and therefore always treat subsequent positionals as anchors.
303 if (m_anchorPositional.value().IsUnlimited() || (m_executionArgs.Count(m_anchorPositional.value().Type()) < 1))
304 {
305 m_executionArgs.Add(m_anchorPositional.value().Type(), std::wstring{currArg});
306 return {};
307 }
308
309 // There are three possibilities for this argument:
310 // 1) It is another positional argument (ex: run <imagename> <command>)
311 // 2) It is a forwarded argument set that could be anything (most likely)
312 // 3) It is an input error and there should be no such argument.
313
314 // Check next positional.
315 const Argument* nextPositional = NextPositional();
316 if (nextPositional)
317 {
318 m_executionArgs.Add(nextPositional->Type(), std::wstring{currArg});
319 return {};
320 }
321
322 // Handle case where we expect a positional but don't find one - check forwarded args.
323
324 // Check for forwarded arg existence.
325 if (m_forwardArgs.empty())
326 {
327 return ArgumentException(Localization::WSLCCLI_CommandHasNoForwardArgumentsError(currArg));
328 }
329
330 // currArg is the first forwarded argument
331 // All the rest of the args are forward args.
332 std::vector<std::wstring> forwardedArgs;
333 forwardedArgs.emplace_back(std::wstring{currArg});
334 while (m_invocationItr != m_invocation.end())
335 {
336 forwardedArgs.emplace_back(std::wstring{*m_invocationItr});
337 ++m_invocationItr;
338 }
339
340 m_executionArgs.Add(m_forwardArgs.front().Type(), std::move(forwardedArgs));
341 return {};
342 }
343
344 // Assumes argument begins with '-' and is at least 2 characters.
345 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgument(const std::wstring_view& currArg)
346 {
347 WI_ASSERT(currArg.length() >= 2 && currArg[0] == WSLC_CLI_ARG_ID_CHAR && currArg[1] != WSLC_CLI_ARG_ID_CHAR);
348
349 // This may be a collection of boolean alias flags.
350 // Helper to find an argument by alias starting at a specific position.
351 auto findArgumentByAlias = [this](const std::wstring_view& str, size_t startPos, size_t& aliasLength) -> const Argument* {
352 for (const auto& arg : m_standardArgs)
353 {
354 const auto& alias = arg.Alias();
355 if (alias.empty())
356 {
357 continue;
358 }
359
360 if (startPos + alias.length() <= str.length() && str.compare(startPos, alias.length(), alias) == 0)
361 {
362 aliasLength = alias.length();
363 return &arg;
364 }
365 }
366
367 return nullptr;
368 };
369
370 // Find the first alias starting at position 1 (after the '-')
371 size_t aliasLength = 0;
372 const Argument* firstArg = findArgumentByAlias(currArg, 1, aliasLength);
373 if (!firstArg)
374 {
375 // Leading alias is unknown. In stopOnUnknown mode nothing has been added
376 // to m_executionArgs for this token yet, so it is safe to back up and stop.
377 if (m_stopOnUnknown)
378 {
379 return BackUpAndStop();
380 }
381
382 return ArgumentException(Localization::WSLCCLI_InvalidAliasError(currArg));
383 }
384
385 // Position after the first alias
386 size_t currentPos = 1 + aliasLength;
387
388 // Check if this argument expects a value
389 if (firstArg->Kind() == Kind::Value)
390 {
391 // Kind::Value is only allowed if it's the last flag (no more characters after it, or '=' follows)
392 if (currentPos >= currArg.length())
393 {
394 // No more characters - value should be in next argument
395 return {firstArg->Type(), currArg};
396 }
397
398 if (currArg[currentPos] != WSLC_CLI_ARG_SPLIT_CHAR)
399 {
400 // There are more characters but it's not '=' - this is invalid
401 return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg), *firstArg);
402 }
403
404 // Value is adjoined after '='
405 ProcessAdjoinedValue(firstArg->Type(), currArg.substr(currentPos + 1));
406 return {};
407 }
408
409 // Boolean flag - check for adjoined boolean value (e.g., -a=true or -a=false).
410 if (currentPos < currArg.length() && currArg[currentPos] == WSLC_CLI_ARG_SPLIT_CHAR)
411 {
412 return ApplyFlagValue(firstArg->Type(), currArg.substr(currentPos + 1), currArg);
413 }
414
415 // No adjoined value — add the flag as true.
416 SetFlag(firstArg->Type(), true);
417
418 // Process remaining adjoined flags
419 while (currentPos < currArg.length())
420 {
421 const Argument* nextArg = findArgumentByAlias(currArg, currentPos, aliasLength);
422
423 if (!nextArg)
424 {
425 return ArgumentException(Localization::WSLCCLI_AdjoinedNotFoundError(currArg));
426 }
427
428 // Update position before checking Kind
429 size_t nextPos = currentPos + aliasLength;
430
431 if (nextArg->Kind() == Kind::Value)
432 {
433 // Kind::Value is only allowed if it's the last flag
434 if (nextPos >= currArg.length())
435 {
436 // No more characters - value should be in next argument
437 return {nextArg->Type(), currArg};
438 }
439
440 if (currArg[nextPos] != WSLC_CLI_ARG_SPLIT_CHAR)
441 {
442 // There are more characters but it's not '=' - this is invalid
443 return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg), *nextArg);
444 }
445
446 // Value is adjoined after '='
447 ProcessAdjoinedValue(nextArg->Type(), currArg.substr(nextPos + 1));
448 return {};
449 }
450
451 // Boolean flag in chain — check for adjoined boolean value.
452 if (nextPos < currArg.length() && currArg[nextPos] == WSLC_CLI_ARG_SPLIT_CHAR)
453 {
454 return ApplyFlagValue(nextArg->Type(), currArg.substr(nextPos + 1), currArg);
455 }
456
457 SetFlag(nextArg->Type(), true);
458 currentPos = nextPos;
459 }
460
461 return {};
462 }
463
464 // Assumes the arg value begins with -- and is at least 2 characters long.
465 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgument(const std::wstring_view& currArg)
466 {
467 WI_ASSERT(currArg.starts_with(L"--"));
468
469 if (currArg.length() == 2)
470 {
471 // Bare '--': not a name we recognize. In stopOnUnknown mode hand it off
472 // to the next pass; otherwise it's a malformed token at this level.
473 if (m_stopOnUnknown)
474 {
475 return BackUpAndStop();
476 }
477
478 return ArgumentException(Localization::WSLCCLI_MissingArgumentNameError(currArg));
479 }
480
481 // This is an arg name, find it and process its value if needed.
482 // Skip the double arg identifier chars.
483 size_t argStart = currArg.find_first_not_of(WSLC_CLI_ARG_ID_CHAR);
484 std::wstring_view argName = currArg.substr(argStart);
485 bool argFound = false;
486
487 bool hasAdjoinedValue = false;
488 std::wstring_view argValue;
489 size_t splitChar = argName.find_first_of(WSLC_CLI_ARG_SPLIT_CHAR);
490 if (splitChar != std::string::npos)
491 {
492 // There is an '=' in this arg, it has an adjoined value, split it out.
493 hasAdjoinedValue = true;
494 argValue = argName.substr(splitChar + 1);
495 argName = argName.substr(0, splitChar);
496 }
497
498 // Find a matching standard arg with this name.
499 for (const auto& arg : m_standardArgs)
500 {
501 if (string::IsEqual(argName, arg.Name()))
502 {
503 // Found a match, process by kind.
504 if (arg.Kind() == Kind::Flag)
505 {
506 if (hasAdjoinedValue)
507 {
508 return ApplyFlagValue(arg.Type(), argValue, currArg);
509 }
510
511 SetFlag(arg.Type(), true);
512 return {};
513 }
514
515 // Not a Flag, must be a Value, and therefore must have a value provided.
516 if (hasAdjoinedValue)
517 {
518 ProcessAdjoinedValue(arg.Type(), argValue);
519 return {};
520 }
521
522 // The value should be the next argument.
523 return {arg.Type(), currArg};
524 }
525 }
526
527 // Unknown name. In stopOnUnknown mode hand it off to the next pass.
528 if (m_stopOnUnknown)
529 {
530 return BackUpAndStop();
531 }
532
533 return ArgumentException(Localization::WSLCCLI_InvalidNameError(currArg));
534 }
535
536 void ParseArgumentsStateMachine::ProcessAdjoinedValue(ArgType type, std::wstring_view value)
537 {
538 // If the adjoined value is wrapped in quotes, strip them off.
539 AddValue(type, std::wstring{StripSurroundingQuotes(value)});
540 }
541 } // namespace wsl::windows::wslc