diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index a35146ca8f..8754f300ab 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -3038,8 +3038,41 @@ On first run, creates the file with all settings commented out at their defaults
Filter output based on conditions provided
- Invalid value "{}" for the '-f, --filter' option: bad format of filter (expected name=value)
- {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated
+ Invalid value "{}" for the '--filter' option: bad format of filter (expected name=value)
+ {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated
+
+
+ Provide filter values (e.g. "until=<timestamp>")
+ {Locked="until"}Command line arguments, file names and string inserts should not be translated
+
+
+ Provide filter values (e.g. "label=<label>")
+ {Locked="label"}Command line arguments, file names and string inserts should not be translated
+
+
+ Do not prompt for confirmation
+
+
+ Are you sure you want to continue?
+
+
+ WARNING! This will remove all stopped containers.
+
+
+ WARNING! This will remove all dangling images.
+
+
+ WARNING! This will remove all images without at least one container associated to them.
+
+
+ WARNING! This will remove anonymous local volumes not used by at least one container.
+
+
+ WARNING! This will remove all local volumes not used by at least one container.
+
+
+ WARNING! This will remove all custom networks not used by at least one container.
+
Invalid argument "{}" for '--mount' option: {}
{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--mount'"}Command line arguments, file names and string inserts should not be translated
diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp
index ca6580878b..c2399308ab 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.cpp
+++ b/src/windows/wslc/arguments/ArgumentValidation.cpp
@@ -352,16 +352,6 @@ void ValidateWSLCSignalFromString(const std::vector& values, const
}
}
-// Validates that each --filter argument is in the form "key=value". Rejects entries without an '=';
-// the runtime validates the key and value for specific objects.
-void ValidateFilter(const std::vector& values)
-{
- for (const auto& value : values)
- {
- std::ignore = ParseFilter(value);
- }
-}
-
void ValidateTimestamp(const std::vector& values, const std::wstring& argName)
{
for (const auto& value : values)
diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h
index f0b84f6262..4d999b67b1 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.h
+++ b/src/windows/wslc/arguments/ArgumentValidation.h
@@ -78,6 +78,4 @@ void ValidateFormatTypeFromString(const std::vector& values, const
void ValidateGpus(const std::vector& values, const std::wstring& argName);
-void ValidateFilter(const std::vector& values);
-
} // namespace wsl::windows::wslc::validation
diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp
index a68f77d47d..68b91c6f63 100644
--- a/src/windows/wslc/commands/ContainerPruneCommand.cpp
+++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp
@@ -26,7 +26,10 @@ namespace wsl::windows::wslc {
// Container Prune Command
std::vector ContainerPruneCommand::GetArguments() const
{
- return {};
+ return {
+ Argument::Create(ArgType::Filter, {.Alias = NO_ALIAS, .Limit = Limit::Unlimited, .Desc = Localization::WSLCCLI_PruneFilterArgDescription()}),
+ Argument::Create(ArgType::Force, {.Desc = Localization::WSLCCLI_PruneForceArgDescription()}),
+ };
}
std::wstring ContainerPruneCommand::ShortDescription() const
diff --git a/src/windows/wslc/commands/ImagePruneCommand.cpp b/src/windows/wslc/commands/ImagePruneCommand.cpp
index 7c63690daa..7db0b03bf9 100644
--- a/src/windows/wslc/commands/ImagePruneCommand.cpp
+++ b/src/windows/wslc/commands/ImagePruneCommand.cpp
@@ -29,7 +29,8 @@ std::vector ImagePruneCommand::GetArguments() const
{
return {
Argument::Create(ArgType::All, {.Desc = Localization::WSLCCLI_ImagePruneAllArgDescription()}),
- Argument::Create(ArgType::Filter, {.Limit = Limit::Unlimited}),
+ Argument::Create(ArgType::Filter, {.Alias = NO_ALIAS, .Limit = Limit::Unlimited, .Desc = Localization::WSLCCLI_PruneFilterArgDescription()}),
+ Argument::Create(ArgType::Force, {.Desc = Localization::WSLCCLI_PruneForceArgDescription()}),
};
}
diff --git a/src/windows/wslc/commands/NetworkPruneCommand.cpp b/src/windows/wslc/commands/NetworkPruneCommand.cpp
index 4beec98a83..34d56f49b3 100644
--- a/src/windows/wslc/commands/NetworkPruneCommand.cpp
+++ b/src/windows/wslc/commands/NetworkPruneCommand.cpp
@@ -27,7 +27,8 @@ namespace wsl::windows::wslc {
std::vector NetworkPruneCommand::GetArguments() const
{
return {
- Argument::Create(ArgType::Filter, {.Limit = Limit::Unlimited}),
+ Argument::Create(ArgType::Filter, {.Alias = NO_ALIAS, .Limit = Limit::Unlimited, .Desc = Localization::WSLCCLI_PruneFilterArgDescription()}),
+ Argument::Create(ArgType::Force, {.Desc = Localization::WSLCCLI_PruneForceArgDescription()}),
};
}
diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp
index a7b8ed9a35..b391ddf1ce 100644
--- a/src/windows/wslc/commands/VolumePruneCommand.cpp
+++ b/src/windows/wslc/commands/VolumePruneCommand.cpp
@@ -28,7 +28,8 @@ std::vector VolumePruneCommand::GetArguments() const
{
return {
Argument::Create(ArgType::All, {.Desc = Localization::WSLCCLI_VolumePruneAllArgDescription()}),
- Argument::Create(ArgType::Filter, {.Limit = Limit::Unlimited}),
+ Argument::Create(ArgType::Filter, {.Alias = NO_ALIAS, .Limit = Limit::Unlimited, .Desc = Localization::WSLCCLI_VolumePruneFilterArgDescription()}),
+ Argument::Create(ArgType::Force, {.Desc = Localization::WSLCCLI_PruneForceArgDescription()}),
};
}
diff --git a/src/windows/wslc/core/Exceptions.h b/src/windows/wslc/core/Exceptions.h
index 1b341474c7..5bf100fbf9 100644
--- a/src/windows/wslc/core/Exceptions.h
+++ b/src/windows/wslc/core/Exceptions.h
@@ -76,4 +76,13 @@ struct ExecutionException : CLIException
{
}
};
+
+// Specific exception for a user declining a confirmation prompt. The requested action is abandoned
+// and the program exits successfully.
+struct TerminateException : CLIException
+{
+ TerminateException() : CLIException(std::wstring_view{})
+ {
+ }
+};
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/core/ExecutionContextData.h b/src/windows/wslc/core/ExecutionContextData.h
index 09d9529298..ea9f37f095 100644
--- a/src/windows/wslc/core/ExecutionContextData.h
+++ b/src/windows/wslc/core/ExecutionContextData.h
@@ -42,6 +42,8 @@ enum class Data : size_t
Volumes,
Networks,
NetworkEndpointOptions,
+ ConfirmWarning,
+ ConfirmMessage,
Max
};
@@ -59,6 +61,8 @@ namespace details {
DEFINE_DATA_MAPPING(Volumes, std::vector);
DEFINE_DATA_MAPPING(Networks, std::vector);
DEFINE_DATA_MAPPING(NetworkEndpointOptions, wsl::windows::wslc::models::NetworkEndpointOptions);
+ DEFINE_DATA_MAPPING(ConfirmWarning, std::wstring);
+ DEFINE_DATA_MAPPING(ConfirmMessage, std::wstring);
} // namespace details
struct DataMap : wsl::windows::wslc::EnumBasedVariantMap
diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp
index d2cac7e69a..4f4c745a0a 100644
--- a/src/windows/wslc/core/Main.cpp
+++ b/src/windows/wslc/core/Main.cpp
@@ -149,6 +149,11 @@ try
context.Terminal.Error(L"{}\n", ee.Message());
return 1;
}
+ catch (const TerminateException&)
+ {
+ // The user declined a confirmation prompt, so the requested action is not performed.
+ return 0;
+ }
catch (...)
{
LOG_CAUGHT_EXCEPTION();
diff --git a/src/windows/wslc/core/Terminal.cpp b/src/windows/wslc/core/Terminal.cpp
index ef76d0eabb..8ab7873c9e 100644
--- a/src/windows/wslc/core/Terminal.cpp
+++ b/src/windows/wslc/core/Terminal.cpp
@@ -80,4 +80,11 @@ std::wstring Terminal::PromptForLine(Level level, std::wstring_view label, bool
return line.value_or(std::wstring{});
}
+bool Terminal::Confirm(std::wstring_view message)
+{
+ const auto answer = PromptForLine(std::format(L"{} [y/N] ", message));
+
+ return wsl::shared::string::IsEqual(wsl::shared::string::TrimAscii(std::wstring_view{answer}), L"y", true);
+}
+
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/core/Terminal.h b/src/windows/wslc/core/Terminal.h
index 8e1e742ba7..7a540c48a5 100644
--- a/src/windows/wslc/core/Terminal.h
+++ b/src/windows/wslc/core/Terminal.h
@@ -139,6 +139,12 @@ struct Terminal
return PromptForLine(Level::Output, label, mask);
}
+ // Prompts for confirmation by writing message followed by " [y/N] ", then reading one line.
+ // Returns true only when the answer is "y" (case-insensitive, surrounding ASCII whitespace
+ // ignored); every other answer, including end of input, declines. Declining at end of input is
+ // what lets a non-interactive prune abort instead of blocking.
+ bool Confirm(std::wstring_view message);
+
bool IsVTEnabled(Level level) const noexcept;
bool IsColorEnabled(Level level) const noexcept;
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index a8639ca993..784a17389f 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -870,10 +870,18 @@ wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Sess
return wsl::shared::FromJson(output.get());
}
-PruneContainersResult ContainerService::Prune(Session& session)
+PruneContainersResult ContainerService::Prune(Session& session, const std::vector>& filters)
{
+ std::vector filterEntries;
+ filterEntries.reserve(filters.size());
+ for (const auto& [key, value] : filters)
+ {
+ filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
+ }
+
PruneResult result;
- THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, &result.result));
+ THROW_IF_FAILED(session.Get()->PruneContainers(
+ filterEntries.empty() ? nullptr : filterEntries.data(), static_cast(filterEntries.size()), &result.result));
PruneContainersResult pruneResult;
pruneResult.SpaceReclaimed = result.result.SpaceReclaimed;
diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h
index 29b009e678..5718ffa6f8 100644
--- a/src/windows/wslc/services/ContainerService.h
+++ b/src/windows/wslc/services/ContainerService.h
@@ -67,6 +67,6 @@ struct ContainerService
static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail = 0);
static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
- static models::PruneContainersResult Prune(models::Session& session);
+ static models::PruneContainersResult Prune(models::Session& session, const std::vector>& filters);
};
} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/tasks/CommonTasks.cpp b/src/windows/wslc/tasks/CommonTasks.cpp
new file mode 100644
index 0000000000..17cd04c3f8
--- /dev/null
+++ b/src/windows/wslc/tasks/CommonTasks.cpp
@@ -0,0 +1,47 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ CommonTasks.cpp
+
+Abstract:
+
+ Implementation of execution logic shared by multiple commands.
+
+--*/
+#include "Argument.h"
+#include "CLIExecutionContext.h"
+#include "CommonTasks.h"
+#include "Exceptions.h"
+
+using namespace wsl::shared;
+using namespace wsl::windows::wslc::execution;
+
+namespace wsl::windows::wslc::task {
+
+// Prompts the user to confirm the action described by Data::ConfirmMessage, preceded by the
+// Data::ConfirmWarning explaining what the action affects. The prompt is skipped when --force is
+// passed, and declining terminates the invocation without running the remaining tasks.
+void ConfirmAction(CLIExecutionContext& context)
+{
+ WI_ASSERT(context.Data.Contains(Data::ConfirmMessage));
+
+ if (context.Args.GetValue())
+ {
+ return;
+ }
+
+ if (context.Data.Contains(Data::ConfirmWarning))
+ {
+ context.Terminal.Warn(L"{}\n", context.Data.Get());
+ }
+
+ if (!context.Terminal.Confirm(context.Data.Get()))
+ {
+ throw TerminateException{};
+ }
+}
+
+} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslc/tasks/CommonTasks.h b/src/windows/wslc/tasks/CommonTasks.h
new file mode 100644
index 0000000000..b232404312
--- /dev/null
+++ b/src/windows/wslc/tasks/CommonTasks.h
@@ -0,0 +1,21 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ CommonTasks.h
+
+Abstract:
+
+ Declaration of execution tasks shared by multiple commands.
+
+--*/
+#pragma once
+#include "CLIExecutionContext.h"
+
+using wsl::windows::wslc::execution::CLIExecutionContext;
+
+namespace wsl::windows::wslc::task {
+void ConfirmAction(CLIExecutionContext& context);
+} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index edc48b9349..7e87677083 100644
--- a/src/windows/wslc/tasks/ContainerTasks.cpp
+++ b/src/windows/wslc/tasks/ContainerTasks.cpp
@@ -15,6 +15,7 @@ Module Name:
#include "ArgumentConvertedTypes.h"
#include "AsyncExecution.h"
#include "CLIExecutionContext.h"
+#include "CommonTasks.h"
#include "ContainerModel.h"
#include "ContainerService.h"
#include "ContainerTasks.h"
@@ -1103,10 +1104,17 @@ void ViewContainerLogs(CLIExecutionContext& context)
void PruneContainers(CLIExecutionContext& context)
{
+ context.Data.Add(Localization::WSLCCLI_ContainerPruneConfirm());
+ context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt());
+ ConfirmAction(context);
+
WI_ASSERT(context.Data.Contains(Data::Session));
auto& session = context.Data.Get();
- auto result = ContainerService::Prune(session);
+ // Filter values are parsed and cached during argument validation.
+ auto filters = context.Args.GetAllValues();
+
+ auto result = ContainerService::Prune(session, filters);
if (!result.PrunedContainers.empty())
{
diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp
index 5f8e49ee9a..64c9ddb7da 100644
--- a/src/windows/wslc/tasks/ImageTasks.cpp
+++ b/src/windows/wslc/tasks/ImageTasks.cpp
@@ -15,6 +15,7 @@ Module Name:
#include "ArgumentConvertedTypes.h"
#include "BuildImageCallback.h"
#include "CLIExecutionContext.h"
+#include "CommonTasks.h"
#include "ContainerService.h"
#include "ImageModel.h"
#include "ImageService.h"
@@ -423,6 +424,11 @@ void TagImage(CLIExecutionContext& context)
void PruneImages(CLIExecutionContext& context)
{
+ context.Data.Add(
+ context.Args.GetValue() ? Localization::WSLCCLI_ImagePruneAllConfirm() : Localization::WSLCCLI_ImagePruneConfirm());
+ context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt());
+ ConfirmAction(context);
+
WI_ASSERT(context.Data.Contains(Data::Session));
auto& session = context.Data.Get();
diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp
index c59357409f..1c8c13607f 100644
--- a/src/windows/wslc/tasks/NetworkTasks.cpp
+++ b/src/windows/wslc/tasks/NetworkTasks.cpp
@@ -14,6 +14,7 @@ Module Name:
#include "Argument.h"
#include "ArgumentConvertedTypes.h"
#include "CLIExecutionContext.h"
+#include "CommonTasks.h"
#include "NetworkModel.h"
#include "NetworkService.h"
#include "NetworkTasks.h"
@@ -262,6 +263,10 @@ void ListNetworks(CLIExecutionContext& context)
void PruneNetworks(CLIExecutionContext& context)
{
+ context.Data.Add(Localization::WSLCCLI_NetworkPruneConfirm());
+ context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt());
+ ConfirmAction(context);
+
WI_ASSERT(context.Data.Contains(Data::Session));
auto& session = context.Data.Get();
diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp
index b4587f1746..35c4b39430 100644
--- a/src/windows/wslc/tasks/VolumeTasks.cpp
+++ b/src/windows/wslc/tasks/VolumeTasks.cpp
@@ -14,6 +14,7 @@ Module Name:
#include "Argument.h"
#include "ArgumentConvertedTypes.h"
#include "CLIExecutionContext.h"
+#include "CommonTasks.h"
#include "VolumeModel.h"
#include "VolumeService.h"
#include "VolumeTasks.h"
@@ -236,6 +237,11 @@ void ListVolumes(CLIExecutionContext& context)
void PruneVolumes(CLIExecutionContext& context)
{
+ context.Data.Add(
+ context.Args.GetValue() ? Localization::WSLCCLI_VolumePruneAllConfirm() : Localization::WSLCCLI_VolumePruneConfirm());
+ context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt());
+ ConfirmAction(context);
+
WI_ASSERT(context.Data.Contains(Data::Session));
auto& session = context.Data.Get();
diff --git a/test/windows/wslc/WSLCCLICommandUnitTests.cpp b/test/windows/wslc/WSLCCLICommandUnitTests.cpp
index f54c9b7227..b5f141043d 100644
--- a/test/windows/wslc/WSLCCLICommandUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLICommandUnitTests.cpp
@@ -104,6 +104,52 @@ class WSLCCLICommandUnitTests
}
}
+ // Test: Every prune command binds -f to --force and leaves --filter unaliased. Aliases resolve
+ // by first match with no collision detection, so an aliased --filter here would shadow -f.
+ TEST_METHOD(PruneCommands_BindShortFToForce)
+ {
+ const auto verifyPruneArguments = [](const std::vector& args, const std::wstring& command) {
+ LogComment(L"Verifying prune argument aliases for: " + command);
+
+ const auto find = [&args](ArgType type) -> const Argument* {
+ const auto itr = std::find_if(args.begin(), args.end(), [type](const auto& arg) { return arg.Type() == type; });
+ return itr == args.end() ? nullptr : &*itr;
+ };
+
+ const auto* force = find(ArgType::Force);
+ VERIFY_IS_NOT_NULL(force);
+ VERIFY_ARE_EQUAL(std::wstring{L"force"}, force->Name());
+ VERIFY_ARE_EQUAL(std::wstring{L"f"}, force->Alias());
+
+ const auto* filter = find(ArgType::Filter);
+ VERIFY_IS_NOT_NULL(filter);
+ VERIFY_ARE_EQUAL(std::wstring{L"filter"}, filter->Name());
+ VERIFY_ARE_EQUAL(std::wstring{L""}, filter->Alias());
+ };
+
+ verifyPruneArguments(ContainerPruneCommand(L"container").GetArguments(), L"container prune");
+ verifyPruneArguments(ImagePruneCommand(L"image").GetArguments(), L"image prune");
+ verifyPruneArguments(VolumePruneCommand(L"volume").GetArguments(), L"volume prune");
+ verifyPruneArguments(NetworkPruneCommand(L"network").GetArguments(), L"network prune");
+ }
+
+ // Test: List commands keep -f bound to --filter, which is why only prune commands were realigned.
+ TEST_METHOD(ListCommands_KeepShortFOnFilter)
+ {
+ const auto verifyFilterAlias = [](const std::vector& args, const std::wstring& command) {
+ LogComment(L"Verifying filter alias for: " + command);
+
+ const auto itr = std::find_if(args.begin(), args.end(), [](const auto& arg) { return arg.Type() == ArgType::Filter; });
+ VERIFY_IS_TRUE(itr != args.end());
+ VERIFY_ARE_EQUAL(std::wstring{L"f"}, itr->Alias());
+ };
+
+ verifyFilterAlias(ContainerListCommand(L"container").GetArguments(), L"container list");
+ verifyFilterAlias(ImageListCommand(L"image").GetArguments(), L"image list");
+ verifyFilterAlias(VolumeListCommand(L"volume").GetArguments(), L"volume list");
+ verifyFilterAlias(NetworkListCommand(L"network").GetArguments(), L"network list");
+ }
+
// Test: Verify SessionEnterCommand has the expected arguments
TEST_METHOD(SessionEnterCommand_HasExpectedArguments)
{
diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
index 09b72b8280..fa9613beca 100644
--- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
@@ -160,6 +160,16 @@ class WSLCCLIExecutionUnitTests
dataMap.Add(std::move(endpointOptions));
handled = true;
}
+ else if (dataType == Data::ConfirmWarning)
+ {
+ dataMap.Add(std::wstring{L"warning"});
+ handled = true;
+ }
+ else if (dataType == Data::ConfirmMessage)
+ {
+ dataMap.Add(std::wstring{L"message"});
+ handled = true;
+ }
if (!handled)
{
@@ -177,6 +187,9 @@ class WSLCCLIExecutionUnitTests
auto& containers = dataMap.Get();
VERIFY_ARE_EQUAL(0u, containers.size());
+ auto& confirmMessage = dataMap.Get();
+ VERIFY_ARE_EQUAL(std::wstring{L"message"}, confirmMessage);
+
// Other more complex EnumVariantMap tests are in the Args unit tests.
// This one will just verify all the data types in the Data Map work as expected.
}
diff --git a/test/windows/wslc/WSLCCLITerminalUnitTests.cpp b/test/windows/wslc/WSLCCLITerminalUnitTests.cpp
index 555dbf41e9..5b35321c38 100644
--- a/test/windows/wslc/WSLCCLITerminalUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLITerminalUnitTests.cpp
@@ -552,6 +552,44 @@ class WSLCCLITerminalUnitTests
VERIFY_ARE_EQUAL(expected, result);
}
+ TEST_METHOD(Terminal_Confirm)
+ {
+ // The prompt is written inline on stdout with the standard suffix appended, and nothing
+ // goes to stderr.
+ {
+ InputCaptureTerminal cap{L"y\n"};
+ VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?"));
+ VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured());
+ VERIFY_ARE_EQUAL(std::wstring{L""}, cap.errPipe.captured());
+ }
+
+ // Only a bare y accepts, in either case and with surrounding whitespace trimmed.
+ for (const auto* answer : {L"y\n", L"Y\n", L" y \n"})
+ {
+ InputCaptureTerminal cap{answer};
+ VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?"));
+ }
+
+ // Anything else declines, including a spelled-out yes, matching the container CLI ecosystem.
+ // A prune with no input attached must abort rather than block, so end of input declines too.
+ for (const auto* answer : {L"yes\n", L"n\n", L"N\n", L"no\n", L"\n", L"maybe\n", L""})
+ {
+ InputCaptureTerminal cap{answer};
+ VERIFY_IS_FALSE(cap.terminal.Confirm(L"Remove everything?"));
+ VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured());
+ }
+
+ // The message is a formatting argument, not a format string, so braces must not be
+ // interpreted.
+ {
+ InputCaptureTerminal cap{L"y\n"};
+ const std::wstring message = L"Remove {} {0} {name} 100%?";
+
+ VERIFY_IS_TRUE(cap.terminal.Confirm(message));
+ VERIFY_ARE_EQUAL(message + L" [y/N] ", cap.outPipe.captured());
+ }
+ }
+
TEST_METHOD(Terminal_ReadLineMaskDefaultsToUnmasked)
{
// ReadLine(bool mask = false): the default reads without masking and returns
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
index d07d2481df..ce89023357 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
@@ -33,13 +33,14 @@ class WSLCE2EContainerPruneTests
EnsureContainerDoesNotExist(L"prune-running-test");
EnsureContainerDoesNotExist(L"prune-multi-1");
EnsureContainerDoesNotExist(L"prune-multi-2");
+ EnsureContainerDoesNotExist(L"prune-confirm-test");
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
// Clean up any leftover containers
- RunWslc(L"container prune");
+ RunWslc(L"container prune --force");
return true;
}
@@ -53,9 +54,9 @@ class WSLCE2EContainerPruneTests
WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers)
{
// Establish a clean baseline first so this does not depend on what other tests left behind.
- RunWslc(L"container prune").Verify({.Stderr = L"", .ExitCode = 0});
+ RunWslc(L"container prune --force").Verify({.Stderr = L"", .ExitCode = 0});
- const auto result = RunWslc(L"container prune");
+ const auto result = RunWslc(L"container prune --force");
// With nothing pruned docker emits only the reclaimed-space line, with no header or leading blank line.
result.Verify({.Stdout = L"Total reclaimed space: 0B\r\n", .Stderr = L"", .ExitCode = 0});
@@ -68,10 +69,10 @@ class WSLCE2EContainerPruneTests
createResult.Verify({.Stderr = L"", .ExitCode = 0});
auto containerId = createResult.GetStdoutOneLine();
- auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });
+ auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune --force"); });
// The created container is in stopped state, so prune should remove it
- const auto result = RunWslc(L"container prune");
+ const auto result = RunWslc(L"container prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify pruned container ID is in output
@@ -94,7 +95,7 @@ class WSLCE2EContainerPruneTests
});
// Prune should not remove a running container
- const auto pruneResult = RunWslc(L"container prune");
+ const auto pruneResult = RunWslc(L"container prune --force");
pruneResult.Verify({.Stderr = L"", .ExitCode = 0});
// Verify the running container is still present
@@ -112,9 +113,9 @@ class WSLCE2EContainerPruneTests
create2.Verify({.Stderr = L"", .ExitCode = 0});
auto containerId2 = create2.GetStdoutOneLine();
- auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });
+ auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune --force"); });
- const auto result = RunWslc(L"container prune");
+ const auto result = RunWslc(L"container prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify pruned container IDs are in output
@@ -126,7 +127,55 @@ class WSLCE2EContainerPruneTests
VerifyContainerIsNotListed(L"prune-multi-2");
}
+ WSLC_TEST_METHOD(WSLCE2E_Container_Prune_WithoutForcePromptsAndDeclines)
+ {
+ // Without --force the prune asks for confirmation. No input is attached here, so the read hits
+ // end of input and declines: the container survives and the command still succeeds.
+ RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0});
+
+ auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); });
+
+ // Argument validation runs before the confirmation prompt, so a bad filter fails with empty
+ // stdout rather than printing the warning first.
+ const auto malformed = RunWslc(L"container prune --filter label");
+ malformed.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(malformed.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label")));
+
+ const auto result = RunWslc(L"container prune");
+ result.Verify({.ExitCode = 0});
+
+ // The warning goes to stderr; the question goes to stdout.
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm()));
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_PruneConfirmPrompt()));
+
+ // Declining prunes nothing, so no deleted header is emitted and the container is still listed.
+ VERIFY_IS_FALSE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneDeletedHeader()));
+ VerifyContainerIsListed(ConfirmContainerName, L"created");
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Prune_FilterExcludesNonMatchingContainer)
+ {
+ // container prune gained --filter here, so a label filter that matches nothing must leave the
+ // stopped container in place, and an unfiltered prune must still remove it.
+ auto createResult = RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag()));
+ createResult.Verify({.Stderr = L"", .ExitCode = 0});
+ const auto containerId = createResult.GetStdoutOneLine();
+
+ auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); });
+
+ const auto filtered = RunWslc(L"container prune --force --filter label=wslc.test.never=present");
+ filtered.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(filtered.StdoutContainsSubstring(containerId));
+ VerifyContainerIsListed(ConfirmContainerName, L"created");
+
+ const auto unfiltered = RunWslc(L"container prune --force");
+ unfiltered.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_TRUE(unfiltered.StdoutContainsSubstring(containerId));
+ VerifyContainerIsNotListed(ConfirmContainerName);
+ }
+
private:
+ const std::wstring ConfirmContainerName = L"prune-confirm-test";
const TestImage& DebianImage = DebianTestImage();
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
index 024c62cb61..eb4d9b3642 100644
--- a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
@@ -46,7 +46,7 @@ class WSLCE2EImagePruneTests
WSLC_TEST_METHOD(WSLCE2E_Image_Prune_NoDanglingImages)
{
// Prune when no dangling images exist should succeed with zero reclaimed space
- const auto result = RunWslc(L"image prune");
+ const auto result = RunWslc(L"image prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
VerifyStdoutContains(result, L"Total reclaimed space:");
@@ -60,7 +60,7 @@ class WSLCE2EImagePruneTests
// 3. Tag alpine as prune-target:v1, overwriting it — debian image is now dangling
TestImageRegistry::Instance().EnsureLoaded(AlpineImage);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
- RunWslc(L"image prune");
+ RunWslc(L"image prune --force");
RunWslc(L"image delete prune-target:v1");
TestImageRegistry::Instance().Delete(AlpineImage);
TestImageRegistry::Instance().Restore(DebianImage);
@@ -71,7 +71,7 @@ class WSLCE2EImagePruneTests
RunWslc(std::format(L"image tag {} prune-target:v1", AlpineImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0});
// Now prune should remove the dangling (original debian) image
- const auto result = RunWslc(L"image prune");
+ const auto result = RunWslc(L"image prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
bool foundDeleted = false;
@@ -99,7 +99,7 @@ class WSLCE2EImagePruneTests
});
// --all should prune unused images (not just dangling)
- const auto result = RunWslc(L"image prune --all");
+ const auto result = RunWslc(L"image prune --force --all");
result.Verify({.Stderr = L"", .ExitCode = 0});
VerifyStdoutContains(result, L"Total reclaimed space:");
@@ -126,7 +126,7 @@ class WSLCE2EImagePruneTests
WSLC_TEST_METHOD(WSLCE2E_Image_Prune_Filter_InvalidKey)
{
// Filter keys are validated by the Docker daemon, which rejects unknown keys.
- const auto result = RunWslc(L"image prune --filter color=blue");
+ const auto result = RunWslc(L"image prune --force --filter color=blue");
VERIFY_ARE_EQUAL(1, result.ExitCode);
VERIFY_IS_TRUE(result.Stderr.has_value());
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter"));
@@ -137,7 +137,7 @@ class WSLCE2EImagePruneTests
// Create a dangling debian image (same trick as WSLCE2E_Image_Prune_DanglingImage).
TestImageRegistry::Instance().EnsureLoaded(AlpineImage);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
- RunWslc(L"image prune");
+ RunWslc(L"image prune --force");
RunWslc(L"image delete prune-target:v1");
TestImageRegistry::Instance().Delete(AlpineImage);
TestImageRegistry::Instance().Restore(DebianImage);
@@ -150,7 +150,8 @@ class WSLCE2EImagePruneTests
// Prune only dangling images carrying a label the dangling image does NOT have.
// Multiple --filter label= values are AND'd by the daemon; the dangling image
// matches neither, so it must survive this prune.
- auto filteredPrune = RunWslc(L"image prune --filter label=wslc.test.never=present --filter label=wslc.test.also=missing");
+ auto filteredPrune =
+ RunWslc(L"image prune --force --filter label=wslc.test.never=present --filter label=wslc.test.also=missing");
filteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
for (const auto& line : filteredPrune.GetStdoutLines())
{
@@ -163,7 +164,7 @@ class WSLCE2EImagePruneTests
// A subsequent unfiltered prune should still find and remove the dangling image,
// proving the filter — not the absence of dangling images — was the reason nothing
// was pruned above.
- auto unfilteredPrune = RunWslc(L"image prune");
+ auto unfilteredPrune = RunWslc(L"image prune --force");
unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
bool foundDeleted = false;
for (const auto& line : unfilteredPrune.GetStdoutLines())
diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp
index 966e92adf3..bf49eab972 100644
--- a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp
@@ -53,7 +53,7 @@ class WSLCE2ENetworkPruneTests
WSLC_TEST_METHOD(WSLCE2E_Network_Prune_NoNetworks)
{
// Prune when no unused networks exist should succeed without reporting any of our test networks.
- const auto result = RunWslc(L"network prune");
+ const auto result = RunWslc(L"network prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName));
@@ -67,7 +67,7 @@ class WSLCE2ENetworkPruneTests
auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(TestNetworkName); });
- const auto result = RunWslc(L"network prune");
+ const auto result = RunWslc(L"network prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
auto output = result.GetStdoutLines();
@@ -91,7 +91,7 @@ class WSLCE2ENetworkPruneTests
EnsureNetworkDoesNotExist(TestNetworkName2);
});
- const auto result = RunWslc(L"network prune");
+ const auto result = RunWslc(L"network prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName));
@@ -116,7 +116,7 @@ class WSLCE2ENetworkPruneTests
EnsureNetworkDoesNotExist(TestNetworkName);
});
- const auto result = RunWslc(L"network prune");
+ const auto result = RunWslc(L"network prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName), L"Network in use by a running container must not be pruned");
@@ -132,7 +132,7 @@ class WSLCE2ENetworkPruneTests
auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(TestNetworkName); });
// A label filter that does not match the network should preserve it.
- const auto filteredPrune = RunWslc(L"network prune --filter label=wslc.test.never=present");
+ const auto filteredPrune = RunWslc(L"network prune --force --filter label=wslc.test.never=present");
filteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_FALSE(
filteredPrune.StdoutContainsLine(TestNetworkName), L"Filtered prune should not have deleted the non-matching network");
@@ -140,7 +140,7 @@ class WSLCE2ENetworkPruneTests
// A subsequent unfiltered prune should still remove it, proving the filter
// was the reason it survived.
- const auto unfilteredPrune = RunWslc(L"network prune");
+ const auto unfilteredPrune = RunWslc(L"network prune --force");
unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(TestNetworkName));
VerifyNetworkIsNotListed(TestNetworkName);
@@ -158,7 +158,7 @@ class WSLCE2ENetworkPruneTests
EnsureNetworkDoesNotExist(TestNetworkName2);
});
- const auto result = RunWslc(L"network prune --filter label=wslc.test.prune=keep");
+ const auto result = RunWslc(L"network prune --force --filter label=wslc.test.prune=keep");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName));
@@ -180,7 +180,7 @@ class WSLCE2ENetworkPruneTests
EnsureNetworkDoesNotExist(TestNetworkName2);
});
- const auto result = RunWslc(L"network prune --filter label!=wslc.test.keep");
+ const auto result = RunWslc(L"network prune --force --filter label!=wslc.test.keep");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName2));
@@ -200,7 +200,7 @@ class WSLCE2ENetworkPruneTests
WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_InvalidKey)
{
- const auto result = RunWslc(L"network prune --filter color=red");
+ const auto result = RunWslc(L"network prune --force --filter color=red");
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG"));
}
diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp
index 7dc2bee337..612172e7d6 100644
--- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp
@@ -53,7 +53,7 @@ class WSLCE2EVolumePruneTests
WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NoVolumes)
{
// Prune when no volumes exist should succeed and report a reclaimed-space line.
- const auto result = RunWslc(L"volume prune");
+ const auto result = RunWslc(L"volume prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
// The deleted-volume block, and the blank line that follows it, are only written when
@@ -68,7 +68,7 @@ class WSLCE2EVolumePruneTests
auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); });
- const auto result = RunWslc(L"volume prune");
+ const auto result = RunWslc(L"volume prune --force");
result.Verify({.Stderr = L"", .ExitCode = 0});
auto output = result.GetStdoutLines();
@@ -85,7 +85,7 @@ class WSLCE2EVolumePruneTests
auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); });
- const auto result = RunWslc(L"volume prune --all");
+ const auto result = RunWslc(L"volume prune --force --all");
result.Verify({.Stderr = L"", .ExitCode = 0});
auto output = result.GetStdoutLines();
@@ -110,7 +110,7 @@ class WSLCE2EVolumePruneTests
EnsureVolumeDoesNotExist(TestVolumeName2);
});
- const auto result = RunWslc(L"volume prune --all");
+ const auto result = RunWslc(L"volume prune --force --all");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.StdoutContainsLine(L"Deleted Volumes:"));
@@ -136,7 +136,7 @@ class WSLCE2EVolumePruneTests
EnsureVolumeDoesNotExist(TestVolumeName);
});
- const auto result = RunWslc(L"volume prune --all");
+ const auto result = RunWslc(L"volume prune --force --all");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_FALSE(result.StdoutContainsLine(TestVolumeName), L"Volume in use by a running container must not be pruned");
@@ -152,7 +152,7 @@ class WSLCE2EVolumePruneTests
auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); });
// A label filter that does not match the volume should preserve it
- const auto filteredPrune = RunWslc(L"volume prune --all --filter label=wslc.test.never=present");
+ const auto filteredPrune = RunWslc(L"volume prune --force --all --filter label=wslc.test.never=present");
filteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_FALSE(
filteredPrune.StdoutContainsLine(TestVolumeName), L"Filtered prune should not have deleted the non-matching volume");
@@ -160,7 +160,7 @@ class WSLCE2EVolumePruneTests
// Subsequent unfiltered prune --all should still remove it, proving
// the filter was the reason it survived.
- const auto unfilteredPrune = RunWslc(L"volume prune --all");
+ const auto unfilteredPrune = RunWslc(L"volume prune --force --all");
unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(TestVolumeName));
VerifyVolumeIsNotListed(TestVolumeName);
@@ -178,7 +178,7 @@ class WSLCE2EVolumePruneTests
EnsureVolumeDoesNotExist(TestVolumeName2);
});
- const auto result = RunWslc(L"volume prune --all --filter label=wslc.test.prune=keep");
+ const auto result = RunWslc(L"volume prune --force --all --filter label=wslc.test.prune=keep");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.StdoutContainsLine(TestVolumeName));
@@ -200,7 +200,7 @@ class WSLCE2EVolumePruneTests
EnsureVolumeDoesNotExist(TestVolumeName2);
});
- const auto result = RunWslc(L"volume prune --all --filter label!=wslc.test.keep");
+ const auto result = RunWslc(L"volume prune --force --all --filter label!=wslc.test.keep");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.StdoutContainsLine(TestVolumeName2));
@@ -220,7 +220,7 @@ class WSLCE2EVolumePruneTests
WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey)
{
- const auto result = RunWslc(L"volume prune --filter color=red");
+ const auto result = RunWslc(L"volume prune --force --filter color=red");
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG"));
}