Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
090934b
[WSLC] Add --workdir / -w option to 'wslc exec'
Mar 27, 2026
4a2218d
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/exec-workingdir
Mar 30, 2026
84851fd
Update test/windows/wslc/CommandLineTestCases.h
ptrivedi Mar 30, 2026
9add70a
Update test/windows/wslc/CommandLineTestCases.h
ptrivedi Mar 30, 2026
53c56a1
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/exec-workingdir
ptrivedi Apr 2, 2026
ce1e458
Fix clang formatting issues
Apr 2, 2026
e6b90dd
Update test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
ptrivedi Apr 2, 2026
1699085
Add E2E tests for wslc container exec, including --workdir option
Apr 2, 2026
d54748e
Fix clang formatting in WSLCE2EContainerExecTests.cpp
Apr 2, 2026
9e966e3
Validate --workdir is non-empty; add unit and parse test cases
Apr 2, 2026
6b8fcbc
Fix clang formatting in CommandLineTestCases.h
Apr 2, 2026
6267e6d
Trim exec E2E tests to --workdir coverage only
Apr 2, 2026
70d3f3a
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/exec-workingdir
ptrivedi Apr 3, 2026
6eacc0b
Missed change from merge conflict resolution
Apr 3, 2026
0979c36
Fix --workdir whitespace validation to use std::iswspace for full Uni…
Copilot Apr 3, 2026
7c16de1
Use lambda with wint_t cast in iswspace call to avoid potential UB
Copilot Apr 3, 2026
a923420
Missed change from merge conflict resolution
Apr 3, 2026
a1e710e
Merge branch 'user/ptrivedi/exec-workingdir' of https://github.com/mi…
Apr 3, 2026
53b0bb9
Address Copilot PR feedback
Apr 4, 2026
edb9743
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/exec-workingdir
ptrivedi Apr 4, 2026
2942cb4
Update src/windows/wslc/services/ContainerService.cpp
ptrivedi Apr 4, 2026
7e7fbfe
Fix ParserTest_StateMachine_PositionalForward: replace -v with -h in …
Apr 6, 2026
00e3420
Merge remote-tracking branch 'origin/feature/wsl-for-apps' into user/…
Apr 6, 2026
f7cda40
Fix E2E exec help test: add --user option after base branch merge
Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2394,6 +2394,9 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
<value>Bind mount a volume to the container</value>
</data>
<data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
<value>Working directory inside the container</value>
</data>
<data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
<value>Write the container ID to the provided path.</value>
</data>
Expand Down
3 changes: 2 additions & 1 deletion src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ _(Time, "time", L"t", Kind::Value, L
_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
/*_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription())*/ \
_(Verbose, "verbose", L"v", Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
_(Version, "version", L"v", Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \
_(Version, "version", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \
/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \
_(Volume, "volume", L"v", Kind::Value, Localization::WSLCCLI_VolumeArgDescription()) \
_(WorkDir, "workdir", L"w", Kind::Value, Localization::WSLCCLI_WorkDirArgDescription()) \
// clang-format on
10 changes: 10 additions & 0 deletions src/windows/wslc/arguments/ArgumentValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ void Argument::Validate(const ArgMap& execArgs) const
validation::ValidateVolumeMount(execArgs.GetAll<ArgType::Volume>());
break;

case ArgType::WorkDir:
{
const auto& value = execArgs.Get<ArgType::WorkDir>();
if (value.empty() || std::all_of(value.begin(), value.end(), std::iswspace))
{
throw ArgumentException(std::format(L"Invalid {} argument value: working directory cannot be empty or whitespace", m_name));
}
break;
}

default:
break;
}
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerExecCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ std::vector<Argument> ContainerExecCommand::GetArguments() const
Argument::Create(ArgType::Session),
Argument::Create(ArgType::TTY),
// Argument::Create(ArgType::User),
Argument::Create(ArgType::WorkDir),
};
}

Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ struct ContainerOptions
bool TTY = false;
std::vector<std::string> Ports;
std::vector<std::wstring> Volumes;
std::string WorkingDirectory;
std::vector<std::string> Entrypoint;
};

Expand Down
9 changes: 7 additions & 2 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,14 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt
WI_SetFlagIf(execFlags, WSLCProcessFlagsStdin, options.Interactive);
WI_SetFlagIf(execFlags, WSLCProcessFlagsTty, options.TTY);

wsl::windows::common::WSLCProcessLauncher launcher({}, options.Arguments, options.EnvironmentVariables, execFlags);
if (!options.WorkingDirectory.empty())
{
launcher.SetWorkingDirectory(std::move(options.WorkingDirectory));
}

ConsoleService consoleService;
return consoleService.AttachToCurrentConsole(
wsl::windows::common::WSLCProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags).Launch(*container));
return consoleService.AttachToCurrentConsole(launcher.Launch(*container));
}

InspectContainer ContainerService::Inspect(Session& session, const std::string& id)
Expand Down
5 changes: 5 additions & 0 deletions src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
}
}

if (context.Args.Contains(ArgType::WorkDir))
{
options.WorkingDirectory = WideToMultiByte(context.Args.Get<ArgType::WorkDir>());
}

context.Data.Add<Data::ContainerOptions>(std::move(options));
}

Expand Down
6 changes: 6 additions & 0 deletions test/windows/wslc/CommandLineTestCases.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ COMMAND_LINE_TEST_CASE(L"container create --name foo ubuntu", L"create", true)
COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true)
COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument
COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example
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)
COMMAND_LINE_TEST_CASE(L"inspect cont1", L"inspect", true)
Expand Down
72 changes: 72 additions & 0 deletions test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Module Name:

#include "Command.h"
#include "RootCommand.h"
#include "ContainerCommand.h"
#include "ContainerTasks.h"

using namespace wsl::windows::wslc;
using namespace WSLCTestHelpers;
Expand Down Expand Up @@ -116,6 +118,76 @@ class WSLCCLIExecutionUnitTests
// This one will just verify all the data types in the Data Map work as expected.
}

// Test: SetContainerOptionsFromArgs sets WorkingDirectory when --workdir is provided
TEST_METHOD(SetContainerOptionsFromArgs_WithWorkDir_SetsWorkingDirectory)
{
CLIExecutionContext context;
context.Args.Add<ArgType::WorkDir>(std::wstring{L"/app"});

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
}

// Test: SetContainerOptionsFromArgs leaves WorkingDirectory empty when --workdir is not provided
TEST_METHOD(SetContainerOptionsFromArgs_WithoutWorkDir_WorkingDirectoryIsEmpty)
{
CLIExecutionContext context;

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_IS_TRUE(options.WorkingDirectory.empty());
}

// Test: Full parse of 'exec --workdir "" cont1 cmd' rejects empty working directory
TEST_METHOD(ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
{
// Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" cont1 sh");

ContainerExecCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);

VERIFY_THROWS_SPECIFIC(
command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
}

// Test: Full parse of 'exec --workdir /path cont1 cmd' sets WorkingDirectory
TEST_METHOD(ExecCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
{
// Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir cont1 sh");

ContainerExecCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);
command.ValidateArguments(context.Args);

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
}

// Test: Full parse of 'exec -w /path cont1 cmd' (short alias) sets WorkingDirectory
TEST_METHOD(ExecCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app cont1 sh");

ContainerExecCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);
command.ValidateArguments(context.Args);

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
}

// Test: Command Line test parsing all cases defined in CommandLineTestCases.h
// This test verifies the command line parsing logic used by the CLI and executes the same
// code as the CLI up to the point of command execution, including parsing and argument validtion.
Expand Down
126 changes: 126 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*++

Copyright (c) Microsoft. All rights reserved.

Module Name:

WSLCE2EContainerExecTests.cpp

Abstract:

This file contains end-to-end tests for WSLC container exec command.
--*/

#include "precomp.h"
#include "windows/Common.h"
#include "WSLCExecutor.h"
#include "WSLCE2EHelpers.h"

namespace WSLCE2ETests {

class WSLCE2EContainerExecTests
{
WSLC_TEST_CLASS(WSLCE2EContainerExecTests)

TEST_CLASS_SETUP(ClassSetup)
{
EnsureImageIsLoaded(DebianImage);
return true;
}

TEST_CLASS_CLEANUP(ClassCleanup)
{
EnsureContainerDoesNotExist(WslcContainerName);
EnsureImageIsDeleted(DebianImage);
return true;
}

TEST_METHOD_SETUP(TestMethodSetup)
{
EnsureContainerDoesNotExist(WslcContainerName);
return true;
}

TEST_METHOD(WSLCE2E_Container_Exec_HelpCommand)
{
WSL2_TEST_ONLY();

auto result = RunWslc(L"container exec --help");
result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
}

TEST_METHOD(WSLCE2E_Container_Exec_WorkDir)
{
WSL2_TEST_ONLY();

auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

result = RunWslc(std::format(L"container exec --workdir /tmp {} pwd", WslcContainerName));
result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
}

TEST_METHOD(WSLCE2E_Container_Exec_WorkDir_ShortAlias)
{
WSL2_TEST_ONLY();

auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

result = RunWslc(std::format(L"container exec -w /tmp {} pwd", WslcContainerName));
result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
}

private:
const std::wstring WslcContainerName = L"wslc-test-container";
const TestImage& DebianImage = DebianTestImage();

std::wstring GetHelpMessage() const
{
std::wstringstream output;
output << GetWslcHeader() //
<< GetDescription() //
<< GetUsage() //
<< GetAvailableCommands() //
<< GetAvailableOptions();
return output.str();
}

std::wstring GetDescription() const
{
return L"Executes a command in a running container.\r\n\r\n";
}

std::wstring GetUsage() const
{
return L"Usage: wslc container exec [<options>] <container-id> <command> [<arguments>...]\r\n\r\n";
}

std::wstring GetAvailableCommands() const
{
std::wstringstream commands;
commands << L"The following arguments are available:\r\n"
<< L" container-id Container ID\r\n"
<< L" command The command to run\r\n"
<< L" arguments Arguments to pass to the command being executed inside the container\r\n"
<< L"\r\n";
return commands.str();
}

std::wstring GetAvailableOptions() const
{
std::wstringstream options;
options << L"The following options are available:\r\n"
<< L" -d,--detach Run container in detached mode\r\n"
<< L" -e,--env Key=Value pairs for environment variables\r\n"
<< L" --env-file File containing key=value pairs of env variables\r\n"
<< L" -i,--interactive Attach to stdin and keep it open\r\n"
<< L" --session Specify the session to use\r\n"
<< L" -t,--tty Open a TTY with the container process.\r\n"
<< L" -w,--workdir Working directory inside the container\r\n"
<< L" -h,--help Shows help about the selected command\r\n"
<< L"\r\n";
return options.str();
}
};
} // namespace WSLCE2ETests
Loading