CLI: Fix forwarded args beginning with '-' from being a parser error (#40131)

This commit is contained in:
David Bennett
2026-04-08 11:03:09 -07:00
committed by GitHub
parent fcd425932b
commit 01706c9cdc
4 changed files with 95 additions and 42 deletions

View File

@@ -173,7 +173,10 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPos
if ((m_executionArgs.Count(m_anchorPositional.value().Type()) < m_anchorPositional.value().Limit()) ||
(m_anchorPositional.value().Limit() == NO_LIMIT))
{
// validate that we dont have any invalid argument specifiers.
// Validate that we don't have any invalid argument specifiers.
// Anchor positionals with multiple values should be order-independent, which means a
// '-' at the start of the first one would be invalid, so it should also be invalid for
// all other anchor positionals of the same type.
if (!currArg.empty() && currArg[0] == WSLC_CLI_ARG_ID_CHAR)
{
return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
@@ -192,12 +195,6 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPos
const Argument* nextPositional = NextPositional();
if (nextPositional)
{
// validate that we dont have any invalid argument specifiers.
if (!currArg.empty() && currArg[0] == WSLC_CLI_ARG_ID_CHAR)
{
return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
}
m_executionArgs.Add(nextPositional->Type(), std::wstring{currArg});
return {};
}

View File

@@ -55,6 +55,19 @@ COMMAND_LINE_TEST_CASE(L"container list --format json", L"list", true)
COMMAND_LINE_TEST_CASE(L"container list --format table", L"list", true)
COMMAND_LINE_TEST_CASE(L"container list --format badformat", L"list", false)
COMMAND_LINE_TEST_CASE(L"run ubuntu", L"run", true)
COMMAND_LINE_TEST_CASE(L"run --rm -it --entrypoint bash archlinux:latest -c \"echo 123\"", L"run", true)
COMMAND_LINE_TEST_CASE(L"run --rm --entrypoint /bin/bash debian:latest -c ls", L"run", true)
COMMAND_LINE_TEST_CASE(L"run jrottenberg/ffmpeg:4.4-alpine -i http://url/to/media.mp4 -stats", L"run", true)
COMMAND_LINE_TEST_CASE(
L"run -v ${PWD}:/data jrottenberg/ffmpeg:4.4-scratch -stats -i http://www.hevc-10bit.mkv -c:v libx265 -pix_fmt yuv420p10 -t "
L"5 -f mp4 test.mp4",
L"run",
true)
COMMAND_LINE_TEST_CASE(
L"run -v ${PWD}:/data -it jrottenberg/ffmpeg:4.4-scratch -stats -i https://file-examples/file_example_MP4_480_1_5MG.mp4 -c:v "
L"libx265 -pix_fmt yuv420p10 -t 5 -f mp4 /dataout.mp4",
L"run",
true)
COMMAND_LINE_TEST_CASE(L"container run ubuntu bash -c 'echo Hello World'", L"run", true)
COMMAND_LINE_TEST_CASE(L"container run ubuntu", L"run", true)
COMMAND_LINE_TEST_CASE(L"container run -it --name foo ubuntu", L"run", true)
@@ -77,7 +90,6 @@ COMMAND_LINE_TEST_CASE(L"exec --workdir /app cont1 echo Hello", L"exec", true)
COMMAND_LINE_TEST_CASE(L"exec -w /app cont1 echo Hello", L"exec", true)
COMMAND_LINE_TEST_CASE(L"container exec --workdir /app cont1 sh", L"exec", true)
COMMAND_LINE_TEST_CASE(L"exec --workdir", L"exec", false) // Missing value for --workdir
COMMAND_LINE_TEST_CASE(L"exec cont1 --workdir", L"exec", false) // Invalid argument specifier after container id
COMMAND_LINE_TEST_CASE(L"exec --workdir \"\" cont1 echo Hello", L"exec", false) // Empty working directory
COMMAND_LINE_TEST_CASE(L"kill cont1 --signal sigkill", L"kill", true)
COMMAND_LINE_TEST_CASE(L"container kill cont1 -s KILL", L"kill", true)

View File

@@ -44,8 +44,8 @@ inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet
{
case ArgumentSet::Run:
return {
Argument::Create(ArgType::ContainerId, true), // Required positional argument
Argument::Create(ArgType::Command, false), // Optional positional argument
Argument::Create(ArgType::ImageId, true), // Required positional argument
Argument::Create(ArgType::Command, false), // Optional positional argument
Argument::Create(ArgType::ForwardArgs, false),
Argument::Create(ArgType::Help),
Argument::Create(ArgType::Interactive),
@@ -76,42 +76,50 @@ inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet
#define WSLC_PARSER_TEST_CASES \
/* Simple case with required arg and simple other args */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -h)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose image1)") \
\
/* Value tests, flag and non-flag, multi-value */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --publish=80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --publish 80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 -p 443:443 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 -p=443:443 cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --verbose --verbose cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --publish=80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --publish 80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p 80:80 -p 443:443 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 -p=443:443 image1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --verbose --verbose image1)") \
\
/* Flag parse tests */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -h cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -hi cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -ihp- cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih=80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih 80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ihp 80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ihp=80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -h image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -hi image1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -ihp- image1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih image1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih=80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih 80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ihp 80:80 image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ihp=80:80 image1)") \
\
/* Validation tests */ \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --signal FOO cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --signal 9 cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -t blah)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -t 5)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --signal FOO image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --signal 9 image1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -t blah image1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -t 5 image1)") \
\
/* Multi-positional tests */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1 command)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1 command --f -z forward hello world)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1 command forward hello world)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1 command forward"hello world")") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1 command f="hello world" forward echo)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc cont1 -v command f="hello world" forward echo)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1 \\command\\?"" --f -z forward hello world)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command --f -z forward hello world)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command forward hello world)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command forward"hello world")") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 command f="hello world" forward echo)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose image1 command f="hello world" forward echo)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc image1 \\command\\?"" --f -z forward hello world)") \
\
/* Once the image name is parsed, the next token becomes the optional <command> positional \
* and everything after that goes into ForwardArgs. Neither <command> nor ForwardArgs are \
* interpreted as wslc options. The second case uses '\' + newline between tokens, which \
* CommandLineToArgvW passes through as literal '\' tokens that the container shell \
* handles correctly. */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc jrottenberg/ffmpeg:4.4-alpine ffmpeg -i http://url/to/media.mp4 -stats)") \
WSLC_PARSER_TEST_CASE(Run, true, L"wslc jrottenberg/ffmpeg:4.4-alpine \\\nffmpeg \\\n-i http://url/to/media.mp4 \\\n-stats") \
\
/* List cases with multiple args and flags that can come after the optional multi-positional. */ \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc)") \

View File

@@ -58,6 +58,8 @@ class WSLCCLIParserUnitTests
for (const auto& testCase : testCases)
{
bool succeeded = false;
try
{
Log::Comment(String().Format(L"Testing: %ls", testCase.commandLine.c_str()));
@@ -73,16 +75,48 @@ class WSLCCLIParserUnitTests
stateMachine.ThrowIfError();
}
if (testCase.commandLine.find(L"cont1") != std::wstring::npos)
// Validate count limits and required arguments, mirroring Command::ValidateArguments.
// Skip all validation if --help is present, as Command::ValidateArguments does.
if (!args.Contains(ArgType::Help))
{
for (const auto& arg : GetArgumentsForSet(testCase.argumentSet))
{
if (arg.Required() && !args.Contains(arg.Type()))
{
throw ArgumentException(std::wstring(L"Required argument missing: ") + arg.Name());
}
if ((arg.Limit() > 0) && (arg.Limit() < args.Count(arg.Type())))
{
throw ArgumentException(std::wstring(L"Too many values for argument: ") + arg.Name());
}
if (args.Contains(arg.Type()))
{
arg.Validate(args);
}
}
}
succeeded = true;
if (testCase.commandLine.find(L"image1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
{
VERIFY_IS_TRUE(args.Contains(ArgType::ImageId));
auto imageId = args.Get<ArgType::ImageId>();
VERIFY_ARE_EQUAL(L"image1", imageId);
}
if (testCase.commandLine.find(L"cont1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::List)
{
VERIFY_IS_TRUE(args.Contains(ArgType::ContainerId));
auto containerId = args.Get<ArgType::ContainerId>();
VERIFY_ARE_EQUAL(L"cont1", containerId);
}
if (testCase.commandLine.find(L"rm") != std::wstring::npos)
if (testCase.commandLine.find(L"--rm") != std::wstring::npos)
{
// Ensure 'rm' was parsed wherever it was found.
// Ensure '--rm' was parsed wherever it was found.
VERIFY_IS_TRUE(args.Contains(ArgType::Remove));
}
@@ -99,7 +133,7 @@ class WSLCCLIParserUnitTests
auto forwardArgs = args.Get<ArgType::ForwardArgs>();
std::wstring forwardArgsConcat = wsl::shared::string::Join(forwardArgs, L' ');
VERIFY_IS_TRUE(forwardArgsConcat.find(L"hello world") != std::wstring::npos); // Forward args should contain hello world
VERIFY_IS_TRUE(forwardArgsConcat.find(L"cont1") == std::wstring::npos); // Forward args should not contain the containerId
VERIFY_IS_TRUE(forwardArgsConcat.find(L"image1") == std::wstring::npos); // Forward args should not contain the imageId
VERIFY_IS_TRUE(forwardArgsConcat.find(L"command") == std::wstring::npos); // Forward args should not contain the command
LogComment(L"Forwarded Args: " + forwardArgsConcat);
}
@@ -134,6 +168,8 @@ class WSLCCLIParserUnitTests
Log::Comment(String().Format(L"Test case threw expected exception: %hs", ex.what()));
}
}
VERIFY_ARE_EQUAL(testCase.expectedResult, succeeded, String().Format(L"Command line: %ls", testCase.commandLine.c_str()));
}
}
};