Skip to content

Commit 2d8c8cf

Browse files
ptrivediPooja TrivediCopilotclaudeCopilot
authored
[WSLC] Add --workdir / -w option to 'wslc exec' (#40041)
* [WSLC] Add --workdir / -w option to 'wslc exec' Adds a --workdir (-w) argument to the exec command that sets the working directory inside the container for the executed process. Wires the value through ContainerOptions into WSLAProcessLauncher::SetWorkingDirectory. Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-Authored-By: Claude Sonnet 4.6 * Update test/windows/wslc/CommandLineTestCases.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/windows/wslc/CommandLineTestCases.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix clang formatting issues * Update test/windows/wslc/WSLCCLIExecutionUnitTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add E2E tests for wslc container exec, including --workdir option - Port existing exec E2E tests from feature branch - Add WSLCE2E_Container_Exec_WorkDir and WSLCE2E_Container_Exec_WorkDir_ShortAlias tests - Update help message in GetAvailableOptions to include -w,--workdir Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix clang formatting in WSLCE2EContainerExecTests.cpp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Validate --workdir is non-empty; add unit and parse test cases - Reject empty or whitespace-only --workdir in Argument::Validate - Add ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException unit test - Add empty-workdir failing case to CommandLineTestCases.h Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix clang formatting in CommandLineTestCases.h Co-Authored-By: Claude Sonnet 4.6 * Trim exec E2E tests to --workdir coverage only Remove tests that duplicate existing coverage in WSLCE2EContainerCreateTests.cpp. Keep only the help message test (validates --workdir appears in output) and the two workdir-specific E2E tests. Co-Authored-By: Claude Sonnet 4.6 * Missed change from merge conflict resolution * Fix --workdir whitespace validation to use std::iswspace for full Unicode coverage Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/b21d1a57-bb3f-4a12-84cf-8e414a453890 Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> * Use lambda with wint_t cast in iswspace call to avoid potential UB Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/b21d1a57-bb3f-4a12-84cf-8e414a453890 Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> * Missed change from merge conflict resolution * Address Copilot PR feedback - Revert Version ArgType alias from NO_ALIAS back to L"v" to preserve existing -v short option - Restore WSLCE2EContainerExecTests.cpp lost in merge conflict resolution Co-Authored-By: Pooja Trivedi * Update src/windows/wslc/services/ContainerService.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix ParserTest_StateMachine_PositionalForward: replace -v with -h in flag parse tests The -v short alias was removed from --verbose (changed to NO_ALIAS) to resolve a triple alias conflict with --version and --volume. The parser test cases in the Run argument set still used -v expecting it to resolve to --verbose, but since neither Version nor Volume are in the Run set, -v became unresolvable and caused unexpected parse failures. Replace -v with -h (help flag) in the flag parse test cases to preserve the same combined-flag parsing coverage with a valid short alias. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix E2E exec help test: add --user option after base branch merge After merging feature/wsl-for-apps, the --user argument is now active in ContainerExecCommand (from PR #40101). Update the expected exec help output to include -u,--user. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a8ee043 commit 2d8c8cf

12 files changed

Lines changed: 247 additions & 18 deletions

File tree

localization/strings/en-US/Resources.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2394,6 +2394,9 @@ On first run, creates the file with all settings commented out at their defaults
23942394
<data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
23952395
<value>Bind mount a volume to the container</value>
23962396
</data>
2397+
<data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
2398+
<value>Working directory inside the container</value>
2399+
</data>
23972400
<data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
23982401
<value>Write the container ID to the provided path.</value>
23992402
</data>

src/windows/wslc/arguments/ArgumentDefinitions.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ _(Time, "time", L"t", Kind::Value, L
7878
/*_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription())*/ \
7979
_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
8080
_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
81-
_(Verbose, "verbose", L"v", Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
81+
_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
8282
_(Version, "version", L"v", Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \
8383
/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \
8484
_(Volume, "volume", L"v", Kind::Value, Localization::WSLCCLI_VolumeArgDescription()) \
85+
_(WorkDir, "workdir", L"w", Kind::Value, Localization::WSLCCLI_WorkingDirArgDescription()) \
8586
// clang-format on

src/windows/wslc/arguments/ArgumentValidation.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ void Argument::Validate(const ArgMap& execArgs) const
4747
validation::ValidateVolumeMount(execArgs.GetAll<ArgType::Volume>());
4848
break;
4949

50+
case ArgType::WorkDir:
51+
{
52+
const auto& value = execArgs.Get<ArgType::WorkDir>();
53+
if (value.empty() ||
54+
std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
55+
{
56+
throw ArgumentException(std::format(L"Invalid {} argument value: working directory cannot be empty or whitespace", m_name));
57+
}
58+
break;
59+
}
60+
5061
default:
5162
break;
5263
}

src/windows/wslc/commands/ContainerExecCommand.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ std::vector<Argument> ContainerExecCommand::GetArguments() const
3737
Argument::Create(ArgType::Session),
3838
Argument::Create(ArgType::TTY),
3939
Argument::Create(ArgType::User),
40+
Argument::Create(ArgType::WorkDir),
4041
};
4142
}
4243

src/windows/wslc/services/ContainerModel.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ struct ContainerOptions
3838
bool TTY = false;
3939
std::vector<std::string> Ports;
4040
std::vector<std::wstring> Volumes;
41+
std::string WorkingDirectory;
4142
std::vector<std::string> Entrypoint;
4243
std::optional<std::string> User{};
4344
};

src/windows/wslc/services/ContainerService.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt
408408
auto user = options.User.value();
409409
processLauncher.SetUser(std::move(user));
410410
}
411+
if (!options.WorkingDirectory.empty())
412+
{
413+
processLauncher.SetWorkingDirectory(std::move(options.WorkingDirectory));
414+
}
411415

412416
return ConsoleService::AttachToCurrentConsole(processLauncher.Launch(*container));
413417
}

src/windows/wslc/tasks/ContainerTasks.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
293293
}
294294
}
295295

296+
if (context.Args.Contains(ArgType::WorkDir))
297+
{
298+
options.WorkingDirectory = WideToMultiByte(context.Args.Get<ArgType::WorkDir>());
299+
}
300+
296301
context.Data.Add<Data::ContainerOptions>(std::move(options));
297302
}
298303

test/windows/wslc/CommandLineTestCases.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ COMMAND_LINE_TEST_CASE(L"-v", L"root", true)
2626

2727
// Session command tests
2828
COMMAND_LINE_TEST_CASE(L"session list", L"list", true)
29-
COMMAND_LINE_TEST_CASE(L"session list -v", L"list", true)
3029
COMMAND_LINE_TEST_CASE(L"session list --verbose", L"list", true)
3130
COMMAND_LINE_TEST_CASE(L"session list --verbose --help", L"list", true)
3231
COMMAND_LINE_TEST_CASE(L"session list --notanarg", L"list", false)
@@ -68,6 +67,12 @@ COMMAND_LINE_TEST_CASE(L"container create --name foo ubuntu", L"create", true)
6867
COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true)
6968
COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument
7069
COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example
70+
COMMAND_LINE_TEST_CASE(L"exec --workdir /app cont1 echo Hello", L"exec", true)
71+
COMMAND_LINE_TEST_CASE(L"exec -w /app cont1 echo Hello", L"exec", true)
72+
COMMAND_LINE_TEST_CASE(L"container exec --workdir /app cont1 sh", L"exec", true)
73+
COMMAND_LINE_TEST_CASE(L"exec --workdir", L"exec", false) // Missing value for --workdir
74+
COMMAND_LINE_TEST_CASE(L"exec cont1 --workdir", L"exec", false) // Invalid argument specifier after container id
75+
COMMAND_LINE_TEST_CASE(L"exec --workdir \"\" cont1 echo Hello", L"exec", false) // Empty working directory
7176
COMMAND_LINE_TEST_CASE(L"kill cont1 --signal sigkill", L"kill", true)
7277
COMMAND_LINE_TEST_CASE(L"container kill cont1 -s KILL", L"kill", true)
7378
COMMAND_LINE_TEST_CASE(L"inspect cont1", L"inspect", true)
@@ -98,7 +103,6 @@ COMMAND_LINE_TEST_CASE(L"image build C:\\context --tag tag1 --tag tag2 --tag tag
98103
COMMAND_LINE_TEST_CASE(L"image build C:\\context --build-arg KEY=VALUE", L"build", true)
99104
COMMAND_LINE_TEST_CASE(L"image build C:\\context --build-arg A=1 --build-arg B=2", L"build", true)
100105
COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test:latest --build-arg KEY=VALUE -f Dockerfile.custom", L"build", true)
101-
COMMAND_LINE_TEST_CASE(L"image build C:\\context -v", L"build", true)
102106
COMMAND_LINE_TEST_CASE(L"image build C:\\context --verbose", L"build", true)
103107
COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test --build-arg KEY=VALUE --verbose", L"build", true)
104108
COMMAND_LINE_TEST_CASE(L"image build", L"build", false)
@@ -110,7 +114,7 @@ COMMAND_LINE_TEST_CASE(L"images", L"images", true) // Aliased off the root chang
110114
COMMAND_LINE_TEST_CASE(L"image ls", L"list", true)
111115
COMMAND_LINE_TEST_CASE(L"image list --format json", L"list", true)
112116
COMMAND_LINE_TEST_CASE(L"image list --format badformat", L"list", false)
113-
COMMAND_LINE_TEST_CASE(L"image list -v", L"list", true)
117+
COMMAND_LINE_TEST_CASE(L"image list --verbose", L"list", true)
114118
COMMAND_LINE_TEST_CASE(L"image list -q", L"list", true)
115119
COMMAND_LINE_TEST_CASE(L"image pull ubuntu", L"pull", true)
116120
COMMAND_LINE_TEST_CASE(L"pull ubuntu", L"pull", true)

test/windows/wslc/ParserTestCases.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,14 @@ WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -p=80:80 -p=443:443 cont1)") \
8989
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --verbose --verbose cont1)") \
9090
\
9191
/* Flag parse tests */ \
92-
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -v cont1)") \
93-
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -vi cont1)") \
94-
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -ivp- cont1)") \
95-
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -piv cont1)") \
96-
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -piv=80:80 cont1)") \
97-
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -piv 80:80 cont1)") \
98-
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ivp 80:80 cont1)") \
99-
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ivp=80:80 cont1)") \
92+
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -h cont1)") \
93+
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -hi cont1)") \
94+
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -ihp- cont1)") \
95+
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih cont1)") \
96+
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih=80:80 cont1)") \
97+
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -pih 80:80 cont1)") \
98+
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ihp 80:80 cont1)") \
99+
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -ihp=80:80 cont1)") \
100100
\
101101
/* Validation tests */ \
102102
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc --signal FOO cont1)") \

test/windows/wslc/WSLCCLIExecutionUnitTests.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Module Name:
2020

2121
#include "Command.h"
2222
#include "RootCommand.h"
23+
#include "ContainerCommand.h"
24+
#include "ContainerTasks.h"
2325

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

121+
// Test: SetContainerOptionsFromArgs sets WorkingDirectory when --workdir is provided
122+
TEST_METHOD(SetContainerOptionsFromArgs_WithWorkDir_SetsWorkingDirectory)
123+
{
124+
CLIExecutionContext context;
125+
context.Args.Add<ArgType::WorkDir>(std::wstring{L"/app"});
126+
127+
wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
128+
129+
const auto& options = context.Data.Get<Data::ContainerOptions>();
130+
VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
131+
}
132+
133+
// Test: SetContainerOptionsFromArgs leaves WorkingDirectory empty when --workdir is not provided
134+
TEST_METHOD(SetContainerOptionsFromArgs_WithoutWorkDir_WorkingDirectoryIsEmpty)
135+
{
136+
CLIExecutionContext context;
137+
138+
wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
139+
140+
const auto& options = context.Data.Get<Data::ContainerOptions>();
141+
VERIFY_IS_TRUE(options.WorkingDirectory.empty());
142+
}
143+
144+
// Test: Full parse of 'exec --workdir "" cont1 cmd' rejects empty working directory
145+
TEST_METHOD(ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
146+
{
147+
// Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
148+
auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" cont1 sh");
149+
150+
ContainerExecCommand command{L""};
151+
CLIExecutionContext context;
152+
command.ParseArguments(invocation, context.Args);
153+
154+
VERIFY_THROWS_SPECIFIC(
155+
command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
156+
}
157+
158+
// Test: Full parse of 'exec --workdir /path cont1 cmd' sets WorkingDirectory
159+
TEST_METHOD(ExecCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
160+
{
161+
// Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
162+
auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir cont1 sh");
163+
164+
ContainerExecCommand command{L""};
165+
CLIExecutionContext context;
166+
command.ParseArguments(invocation, context.Args);
167+
command.ValidateArguments(context.Args);
168+
169+
wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
170+
171+
const auto& options = context.Data.Get<Data::ContainerOptions>();
172+
VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
173+
}
174+
175+
// Test: Full parse of 'exec -w /path cont1 cmd' (short alias) sets WorkingDirectory
176+
TEST_METHOD(ExecCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
177+
{
178+
auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app cont1 sh");
179+
180+
ContainerExecCommand command{L""};
181+
CLIExecutionContext context;
182+
command.ParseArguments(invocation, context.Args);
183+
command.ValidateArguments(context.Args);
184+
185+
wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
186+
187+
const auto& options = context.Data.Get<Data::ContainerOptions>();
188+
VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
189+
}
190+
119191
// Test: Command Line test parsing all cases defined in CommandLineTestCases.h
120192
// This test verifies the command line parsing logic used by the CLI and executes the same
121193
// code as the CLI up to the point of command execution, including parsing and argument validtion.

0 commit comments

Comments
 (0)