CLI: Fix parser treating stdin positional ('-') as an argument specifier error. (#40527)

David Bennett committed May 13, 2026 at 23:19 UTC 7c3394fe67f3e6f3bcd4cee09fd31287cc711e69
4 files changed +39 -26
src/windows/wslc/arguments/ArgumentParser.cpp
+24 -22
@@ -67,21 +67,25 @@ void ParseArgumentsStateMachine::ThrowIfError() const
67 }
68 }
69
70 -const Argument* ParseArgumentsStateMachine::NextPositional()
70 +void ParseArgumentsStateMachine::AdvanceToNextPositional(std::vector<Argument>::iterator& itr) const
71 {
72 - // Find the next appropriate positional arg if the current itr isn't one or has hit its limit.
73 - while (m_positionalSearchItr != m_positionalArgs.end() &&
74 - (m_executionArgs.Count(m_positionalSearchItr->Type()) == m_positionalSearchItr->Limit()))
72 + while (itr != m_positionalArgs.end() && (m_executionArgs.Count(itr->Type()) == itr->Limit()))
73 {
76 - ++m_positionalSearchItr;
74 + ++itr;
75 }
76 +}
77
79 - if (m_positionalSearchItr == m_positionalArgs.end())
80 - {
81 - return nullptr;
82 - }
78 +const Argument* ParseArgumentsStateMachine::NextPositional()
79 +{
80 + AdvanceToNextPositional(m_positionalSearchItr);
81 + return m_positionalSearchItr != m_positionalArgs.end() ? &*m_positionalSearchItr : nullptr;
82 +}
83
84 - return &*m_positionalSearchItr;
84 +bool ParseArgumentsStateMachine::HasNextPositional() const
85 +{
86 + auto itr = m_positionalSearchItr;
87 + AdvanceToNextPositional(itr);
88 + return itr != m_positionalArgs.end();
89 }
90
91 // Parse arguments as such:
@@ -127,7 +131,14 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
131 // The currentArg is non-empty, and starts with a -.
132 if (currArg.length() == 1)
133 {
130 - // If it is only one character, then it is an error since it is neither an alias nor a named argument.
134 + if (HasNextPositional())
135 + {
136 + // The '-' character may be a valid positional argument value (ex: stdin), so treat this
137 + // as a positional argument if there are any positionals left to fill.
138 + return ProcessPositionalArgument(currArg);
139 + }
140 +
141 + // No positional argument remaining means this is an invalid argument.
142 return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
143 }
144
@@ -141,10 +152,10 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
152 return ProcessNamedArgument(currArg);
153 }
154
144 -// Assumes non-empty and does not begin with '-'.
155 +// Assumes non-empty.
156 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessPositionalArgument(const std::wstring_view& currArg)
157 {
147 - WI_ASSERT(!currArg.empty() && currArg[0] != WSLC_CLI_ARG_ID_CHAR);
158 + WI_ASSERT(!currArg.empty());
159
160 const Argument* nextPositional = NextPositional();
161 if (!nextPositional)
@@ -173,15 +184,6 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPos
184 if ((m_executionArgs.Count(m_anchorPositional.value().Type()) < m_anchorPositional.value().Limit()) ||
185 (m_anchorPositional.value().Limit() == NO_LIMIT))
186 {
176 - // Validate that we don't have any invalid argument specifiers.
177 - // Anchor positionals with multiple values should be order-independent, which means a
178 - // '-' at the start of the first one would be invalid, so it should also be invalid for
179 - // all other anchor positionals of the same type.
180 - if (!currArg.empty() && currArg[0] == WSLC_CLI_ARG_ID_CHAR)
181 - {
182 - return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
183 - }
184 -
187 m_executionArgs.Add(m_anchorPositional.value().Type(), std::wstring{currArg});
188 return {};
189 }
src/windows/wslc/arguments/ArgumentParser.h
+6
@@ -89,6 +89,9 @@ struct ParseArgumentsStateMachine
89 // Gets the next positional argument, or nullptr if there is not one.
90 const Argument* NextPositional();
91
92 + // Returns true if there is a next positional argument available, without advancing the iterator.
93 + bool HasNextPositional() const;
94 +
95 const std::vector<Argument>& Arguments() const
96 {
97 return m_arguments;
@@ -102,6 +105,9 @@ private:
105 State ProcessNamedArgument(const std::wstring_view& currArg);
106 void ProcessAdjoinedValue(ArgType type, std::wstring_view value);
107
108 + // Advances the given iterator past any positionals that have reached their limit.
109 + void AdvanceToNextPositional(std::vector<Argument>::iterator& itr) const;
110 +
111 Invocation& m_invocation;
112 ArgMap& m_executionArgs;
113 std::vector<Argument> m_arguments;
test/windows/wslc/ParserTestCases.h
+7 -1
@@ -121,6 +121,12 @@ WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 \\command\\?"" --f -z forward h
121 WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc jrottenberg/ffmpeg:4.4-alpine ffmpeg -i http://url/to/media.mp4 -stats)") \
122 WSLC_PARSER_TEST_CASE(Run, true, L"wslc jrottenberg/ffmpeg:4.4-alpine \\\nffmpeg \\\n-i http://url/to/media.mp4 \\\n-stats") \
123 \
124 +/* Stdin dash ('-') as a positional value. A lone '-' conventionally means stdin and must \
125 + * be accepted as a valid positional rather than treated as an invalid flag specifier. */ \
126 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -)") \
127 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc - command)") \
128 +WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose -)") \
129 +\
130 /* List cases with multiple args and flags that can come after the optional multi-positional. */ \
131 WSLC_PARSER_TEST_CASE(List, true, LR"(wslc)") \
132 WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1)") \
@@ -138,4 +144,4 @@ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -vp cont1)") \
144 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 -v cont2 -12)") \
145 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=false cont2)") \
146 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)")
141 -// clang-format on
\ No newline at end of file
147 +// clang-format on
test/windows/wslc/WSLCCLIParserUnitTests.cpp
+2 -3
@@ -46,8 +46,7 @@ class WSLCCLIParserUnitTests
46 return true;
47 }
48
49 - // Test: Verify command line to argv mapping and GetRemainingRawCommandLineFromIndex
50 - TEST_METHOD(ParserTest_StateMachine_PositionalForward)
49 + TEST_METHOD(ParserTest_ParserCases)
50 {
51 // Build test cases from x-macro
52 std::vector<ParserTestCase> testCases = {
@@ -173,4 +172,4 @@ class WSLCCLIParserUnitTests
172 }
173 }
174 };
176 -} // namespace WSLCCLIParserUnitTests
\ No newline at end of file
175 +} // namespace WSLCCLIParserUnitTests