Add base CLI implementation (#14216)

This commit is contained in:
David Bennett
2026-02-18 14:00:29 -08:00
committed by GitHub
parent b8d1505318
commit 465f3f093e
44 changed files with 3438 additions and 82 deletions

View File

@@ -1924,6 +1924,130 @@ Usage:
<value>Unknown command: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
<value>Unrecognized command: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_RequiredArgumentError" xml:space="preserve">
<value>Required argument not provided: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_TooManyArgumentsError" xml:space="preserve">
<value>Argument provided more times than allowed: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_HelpArgDescription" xml:space="preserve">
<value>Shows help about the selected command</value>
</data>
<data name="WSLCCLI_InfoArgDescription" xml:space="preserve">
<value>Shows information about this tool and its environment</value>
</data>
<data name="WSLCCLI_MultipleExclusiveArgumentsProvided" xml:space="preserve">
<value>Multiple mutually exclusive arguments provided: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_DependencyArgumentMissing" xml:space="preserve">
<value>Argument {} can only be used with {}</value>
<comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_Usage" xml:space="preserve">
<value>Usage: {} {}</value>
<comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_Command" xml:space="preserve">
<value>Command</value>
</data>
<data name="WSLCCLI_Options" xml:space="preserve">
<value>options</value>
</data>
<data name="WSLCCLI_AvailableCommandAliases" xml:space="preserve">
<value>The following command aliases are available:</value>
</data>
<data name="WSLCCLI_AvailableCommands" xml:space="preserve">
<value>The following commands are available:</value>
</data>
<data name="WSLCCLI_AvailableSubcommands" xml:space="preserve">
<value>The following sub-commands are available:</value>
</data>
<data name="WSLCCLI_AvailableOptions" xml:space="preserve">
<value>The following options are available:</value>
</data>
<data name="WSLCCLI_AvailableArguments" xml:space="preserve">
<value>The following arguments are available:</value>
</data>
<data name="WSLCCLI_HelpForDetails" xml:space="preserve">
<value>For more details on a specific command, pass it the help argument.</value>
</data>
<data name="WSLCCLI_InvalidNameError" xml:space="preserve">
<value>Argument name was not recognized for the current command: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_CommandRequiresAdmin" xml:space="preserve">
<value>This command requires administrator privileges to execute.</value>
</data>
<data name="WSLCCLI_SessionIdArgDescription" xml:space="preserve">
<value>Specify the session to use</value>
</data>
<data name="WSLCCLI_AttachArgDescription" xml:space="preserve">
<value>Attach to stdout/stderr of the container</value>
</data>
<data name="WSLCCLI_InteractiveArgDescription" xml:space="preserve">
<value>Attach to stdin and keep it open</value>
</data>
<data name="WSLCCLI_PortArgDescription" xml:space="preserve">
<value>Specify the port to use</value>
</data>
<data name="WSLCCLI_ContainerIdArgDescription" xml:space="preserve">
<value>Container ID</value>
</data>
<data name="WSLCCLI_MissingArgumentError" xml:space="preserve">
<value>Missing argument value: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_InvalidAliasError" xml:space="preserve">
<value>Argument alias was not recognized for the current command: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_InvalidArgumentSpecifierError" xml:space="preserve">
<value>Invalid argument specifier: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_AdjoinedNotFoundError" xml:space="preserve">
<value>Adjoined flag alias not found: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_AdjoinedNotFlagError" xml:space="preserve">
<value>Adjoined alias is not a flag: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_SingleCharAfterDashError" xml:space="preserve">
<value>Invalid argument specifier: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_FlagContainAdjoinedError" xml:space="preserve">
<value>Flag argument cannot contain adjoined value: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
<value>Found a positional argument when none was expected: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_MissingArgumentNameError" xml:space="preserve">
<value>Missing argument name at: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_FailedResolvingForwardError" xml:space="preserve">
<value>Failed to resolve forwarded arguments starting at argument: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_CommandHasNoForwardArgumentsError" xml:space="preserve">
<value>Invalid extra argument encountered: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ValueMustBeLastInAliasChainError" xml:space="preserve">
<value>Alias arguments with a value must be last in the alias chain: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name = "MessageWslaInvalidImage" xml:space = "preserve" >
<value>Invalid image: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>

View File

@@ -42,6 +42,16 @@
<File Id="system.vhd" Source="${WSLG_SOURCE_DIR}/${TARGET_PLATFORM}/system.vhd"/>
<?endif?>
<!-- Add INSTALLDIR to system PATH to make wslc.exe and other CLI tools accessible. -->
<!-- NOTE:
- Permanent="no" ensures this PATH entry is removed when the MSI is uninstalled.
- If INSTALLDIR is manually moved or deleted after installation (outside of MSI),
the PATH entry will become stale until uninstall or manual correction.
- This behavior is intentional and currently implemented as a convenience until
we can deploy these CLI tools (e.g. wslc.exe) to a stable system location
such as System32, or offer an installer option to skip PATH modification. -->
<Environment Id="PATH" Name="PATH" Value="[INSTALLDIR]" Permanent="no" Part="last" Action="set" System="yes" />
<!-- Installation folder -->
<RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI">
<RegistryValue Name="InstallLocation" Value="[INSTALLDIR]" Type="string" />

View File

@@ -1,84 +1,64 @@
set(SOURCES
main.cpp
# Commands
ShellCommand.cpp
ImageCommand.cpp
ContainerCommand.cpp
ICommand.cpp
# Services
ContainerService.cpp
ImageService.cpp
ShellService.cpp
SessionService.cpp
ConsoleService.cpp
# Models
SessionModel.cpp
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/core
${CMAKE_CURRENT_SOURCE_DIR}/commands
${CMAKE_CURRENT_SOURCE_DIR}/arguments
${CMAKE_CURRENT_SOURCE_DIR}/tasks
)
set(HEADERS
# Commands
ShellCommand.h
ImageCommand.h
ContainerCommand.h
ICommand.h
# Others
TablePrinter.h
# Services
ContainerService.h
ImageService.h
ShellService.h
SessionService.h
ConsoleService.h
# Models
ImageModel.h
ContainerModel.h
SessionModel.h
core/Exceptions.h
core/EnumVariantMap.h
arguments/ArgumentDefinitions.h
arguments/ArgumentTypes.h
arguments/Argument.h
arguments/ArgumentParser.h
core/Command.h
core/CLIExecutionContext.h
core/ExecutionContextData.h
core/Invocation.h
commands/RootCommand.h
commands/DiagCommand.h
tasks/Task.h
tasks/DiagTasks.h
)
source_group("Services" FILES
ContainerService.h
ContainerService.cpp
ImageService.h
ImageService.cpp
ShellService.h
ShellService.cpp
SessionService.h
SessionService.cpp
ConsoleService.h
ConsoleService.cpp
set(SOURCES
core/Command.cpp
core/Main.cpp
arguments/Argument.cpp
arguments/ArgumentParser.cpp
commands/RootCommand.cpp
commands/DiagCommand.cpp
commands/DiagListCommand.cpp
tasks/DiagTasks.cpp
)
source_group("Commands" FILES
ShellCommand.h
ShellCommand.cpp
ImageCommand.h
ImageCommand.cpp
ContainerCommand.h
ContainerCommand.cpp
ICommand.h
ICommand.cpp
# Object library for WSLC components.
# Used to build the executable and also unit testing components.
add_library(wslclib OBJECT ${SOURCES} ${HEADERS})
set_target_properties(wslclib PROPERTIES FOLDER windows)
target_include_directories(wslclib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/core
${CMAKE_CURRENT_SOURCE_DIR}/commands
${CMAKE_CURRENT_SOURCE_DIR}/arguments
${CMAKE_CURRENT_SOURCE_DIR}/tasks
)
source_group("Models" FILES
ImageModel.h
ContainerModel.h
SessionModel.h
SessionModel.cpp
)
add_executable(wslc ${SOURCES} ${HEADERS})
target_link_libraries(wslc
target_precompile_headers(wslclib REUSE_FROM common)
target_link_libraries(wslclib
PRIVATE
${COMMON_LINK_LIBRARIES}
common
)
target_precompile_headers(wslc REUSE_FROM common)
# Create wslc.exe
add_executable(wslc $<TARGET_OBJECTS:wslclib>)
target_link_libraries(wslc
PRIVATE
${COMMON_LINK_LIBRARIES}
common
)
set_target_properties(wslc PROPERTIES FOLDER windows)
set_target_properties(wslc PROPERTIES FOLDER windows)
# For prettier source tree browsing
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS})

View File

@@ -123,4 +123,4 @@ int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ClientRunningWS
return process.Wait();
}
} // namespace wsl::windows::wslc::services
} // namespace wsl::windows::wslc::services

View File

@@ -29,4 +29,4 @@ public:
int Exec(models::Session& session, const std::string& id, models::ExecContainerOptions options);
wsl::windows::common::docker_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
};
} // namespace wsl::windows::wslc::services
} // namespace wsl::windows::wslc::services

View File

@@ -39,4 +39,4 @@ void ICommand::PrintHelp() const
{
wslutil::PrintMessage(wsl::shared::string::MultiByteToWide(GetFullDescription()));
}
} // namespace wsl::windows::wslc::commands
} // namespace wsl::windows::wslc::commands

View File

@@ -23,4 +23,4 @@ struct ImageInformation
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ImageInformation, Name, Size);
};
} // namespace wsl::windows::wslc::models
} // namespace wsl::windows::wslc::models

View File

@@ -0,0 +1,2 @@
### WSL Container CLI
This is the WSL Container CLI README

View File

@@ -34,4 +34,4 @@ const WSLA_SESSION_SETTINGS* SessionOptions::Get() const
{
return &m_sessionSettings;
}
} // namespace wsl::windows::wslc::models
} // namespace wsl::windows::wslc::models

View File

@@ -33,4 +33,4 @@ Session SessionService::CreateSession(const std::optional<SessionOptions>& optio
wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
return Session(std::move(session));
}
} // namespace wsl::windows::wslc::services
} // namespace wsl::windows::wslc::services

View File

@@ -126,4 +126,4 @@ std::vector<SessionInformation> ShellService::List()
return result;
}
} // namespace wsl::windows::wslc::services
} // namespace wsl::windows::wslc::services

View File

@@ -30,4 +30,4 @@ public:
int Attach(const std::wstring& name);
std::vector<SessionInformation> List();
};
} // namespace wsl::windows::wslc::services
} // namespace wsl::windows::wslc::services

View File

@@ -0,0 +1,75 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Argument.cpp
Abstract:
Implementation of the Argument class.
--*/
#include "Argument.h"
#include "Command.h"
#include "Exceptions.h"
#include "ArgumentDefinitions.h"
#include <algorithm>
#include <iterator>
#include <sstream>
#include <string>
using namespace wsl::shared;
using namespace wsl::windows::wslc::argument;
using namespace std::literals;
namespace wsl::windows::wslc {
using namespace wsl::windows::wslc::execution;
// This is the main Argument creation method, allowing overrides of the default properties of arguments.
// The ArgType has some core characteristic, such as the Kind, Name, and Alias. If these
// need to be changed, it is recommended to create a new ArgType in ArgumentDefinitions.h. If the argument
// just needs a different description, it can be overridden in the desc, or if you need it to be required,
// or to allow multiple uses within a command, then those properties can be set using the Create
// function below inside the command. In this way all arguments default to "1" use and not required, and
// this can only be changed in the command's GetArguments function, so the defaults are always clear and
// consistent. Visibility can also be overridden and is defaulted to "Help".
Argument Argument::Create(ArgType type, std::optional<bool> required, std::optional<int> countLimit, std::optional<std::wstring> desc)
{
switch (type)
{
#define WSLC_ARG_CREATE_CASE(EnumName, Name, Alias, ArgumentKind, Desc) \
case ArgType::EnumName: \
return Argument{ \
type, \
L##Name, \
Alias, \
desc.has_value() ? std::move(desc.value()) : std::wstring(Desc), \
ArgumentKind, \
required.value_or(DefaultRequired), \
countLimit.value_or(DefaultCountLimit)};
WSLC_ARGUMENTS(WSLC_ARG_CREATE_CASE)
#undef WSLC_ARG_CREATE_CASE
default:
THROW_HR(E_UNEXPECTED);
}
}
// Retrieves the usage string of the Argument, based on its Alias and Name.
// The format is "-alias,--name" or just "--name" if no alias.
std::wstring Argument::GetUsageString() const
{
std::wostringstream strstr;
if (!m_alias.empty())
{
strstr << WSLC_CLI_ARG_ID_CHAR << m_alias << L',';
}
strstr << WSLC_CLI_ARG_ID_CHAR << WSLC_CLI_ARG_ID_CHAR << m_name;
return strstr.str();
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,105 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Argument.h
Abstract:
Declaration of the Argument class for command-line argument handling.
--*/
#pragma once
#include "ArgumentTypes.h"
#include <string>
#define WSLC_CLI_ARG_ID_CHAR L'-'
#define WSLC_CLI_ARG_ID_STRING L"-"
#define WSLC_CLI_ARG_SPLIT_CHAR L'='
#define WSLC_CLI_HELP_ARG L"?"
#define WSLC_CLI_HELP_ARG_STRING WSLC_CLI_ARG_ID_STRING WSLC_CLI_HELP_ARG
#define NO_ALIAS L""
using namespace wsl::windows::wslc::argument;
namespace wsl::windows::wslc {
// An argument to a command.
struct Argument
{
// Default argument configuration constants
static constexpr Kind DefaultKind = Kind::Flag;
static constexpr bool DefaultRequired = false;
static constexpr int DefaultCountLimit = 1;
// Full constructor with all parameters
Argument(
ArgType argType,
const std::wstring& name,
const std::wstring& alias,
const std::wstring& desc,
argument::Kind kind = DefaultKind,
bool required = DefaultRequired,
int countLimit = DefaultCountLimit) :
m_argType(argType), m_name(name), m_alias(alias), m_desc(desc), m_type(kind), m_required(required), m_countLimit(countLimit)
{
}
Argument(const Argument&) = default;
Argument& operator=(const Argument&) = default;
Argument(Argument&&) = default;
Argument& operator=(Argument&&) = default;
// Creates an argument with optional overrides for table defaults
static Argument Create(
ArgType type,
std::optional<bool> required = std::nullopt,
std::optional<int> countLimit = std::nullopt,
std::optional<std::wstring> desc = std::nullopt);
// Gets the argument usage string in the format of "-alias,--name" or just "--name" if no alias.
std::wstring GetUsageString() const;
// Arguments are not localized, but the description is.
const std::wstring& Name() const
{
return m_name;
}
const std::wstring& Alias() const
{
return m_alias;
}
const std::wstring& Description() const
{
return m_desc;
}
bool Required() const
{
return m_required;
}
ArgType Type() const
{
return m_argType;
}
Kind Kind() const
{
return m_type;
}
int Limit() const
{
return m_countLimit;
}
private:
ArgType m_argType;
std::wstring m_name;
std::wstring m_desc;
std::wstring m_alias;
bool m_required = DefaultRequired;
argument::Kind m_type = DefaultKind;
int m_countLimit = DefaultCountLimit;
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,45 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
ArgumentDefinitions.h
Abstract:
Declaration of the available Arguments with their base properties.
--*/
#pragma once
// Here is where base argument types are defined, with their name, alias, kind, and default description.
// The description can be overridden by commands if a particular command needs a different description but otherwise the
// same argument type definition. The ArgType enum and the mapping of ArgType to data type are generated from this X-Macro, so all
// arguments must be defined here to be used in the system. The arguments defined here are the basis for all commands,
// but not all arguments need to be used by all commands, and additional properties of the arguments can be set in the command's
// GetArguments function when creating the Argument with Argument::Create.
// The Kind determines the data type:
// - Kind::Flag -> bool
// - Kind::Value -> std::wstring
// - Kind::Positional -> std::wstring
// - Kind::Forward -> std::vector<std::wstring>
// No other files other than ArgumentValidation need to be changed when adding a new argument, and that is only
// if you wish to add validation for the new argument or have it use existing validation.
// X-Macro for defining all arguments in one place
// Format: ARGUMENT(EnumName, Name, Alias, Kind, Desc)
// clang-format off
#define WSLC_ARGUMENTS(_) \
_(Command, "command", NO_ALIAS, Kind::Positional, L"The command to run") \
_(ContainerId, "container-id", NO_ALIAS, Kind::Positional, L"Specify the target container by its ID") \
_(ForwardArgs, "forwardargs", NO_ALIAS, Kind::Forward, L"Args to pass along") \
_(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, Localization::WSLCCLI_HelpArgDescription()) \
_(Info, "info", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_InfoArgDescription()) \
_(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
_(Publish, "publish", L"p", Kind::Value, L"Publish port") \
_(Remove, "remove", L"rm", Kind::Flag, L"Remove the container after execution") \
_(Verbose, "verbose", L"v", Kind::Flag, L"Output verbose details")
// clang-format on

View File

@@ -0,0 +1,409 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
ArgumentParser.cpp
Abstract:
Implementation of the ArgumentParser class.
--*/
#include "ArgumentParser.h"
#include "Localization.h"
using namespace wsl::shared;
namespace wsl::windows::wslc {
ParseArgumentsStateMachine::ParseArgumentsStateMachine(Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments) :
m_invocation(inv), m_executionArgs(execArgs), m_arguments(std::move(arguments)), m_invocationItr(m_invocation.begin())
{
// Create sublists by Kind for easier processing in the state machine.
for (const auto& arg : m_arguments)
{
switch (arg.Kind())
{
case Kind::Value:
m_standardArgs.emplace_back(arg);
break;
case Kind::Flag:
m_standardArgs.emplace_back(arg);
break;
case Kind::Positional:
m_positionalArgs.emplace_back(arg);
break;
case Kind::Forward:
m_forwardArgs.emplace_back(arg);
break;
}
}
m_positionalSearchItr = m_positionalArgs.begin();
}
bool ParseArgumentsStateMachine::Step()
{
if (m_invocationItr == m_invocation.end())
{
return false;
}
m_state = StepInternal();
return true;
}
void ParseArgumentsStateMachine::ThrowIfError() const
{
if (m_state.Exception())
{
throw m_state.Exception().value();
}
// If the next argument was to be a value, but none was provided, convert it to an exception.
else if (m_state.Type() && m_invocationItr == m_invocation.end())
{
throw ArgumentException(Localization::WSLCCLI_MissingArgumentError(m_state.Arg()));
}
}
const Argument* ParseArgumentsStateMachine::NextPositional()
{
// Find the next appropriate positional arg if the current itr isn't one or has hit its limit.
while (m_positionalSearchItr != m_positionalArgs.end() &&
(m_executionArgs.Count(m_positionalSearchItr->Type()) == m_positionalSearchItr->Limit()))
{
++m_positionalSearchItr;
}
if (m_positionalSearchItr == m_positionalArgs.end())
{
return nullptr;
}
return &*m_positionalSearchItr;
}
// Parse arguments as such:
// 1. If argument starts with a single -, the alias is considered (can be 1-2 characters).
// a. If the named argument alias (a or ab) needs a VALUE, it can be provided in these ways:
// -a=VALUE or -ab=VALUE
// -a VALUE or -ab VALUE
// b. If the argument is a flag, additional characters after are treated as if they start
// with a -, repeatedly until the end of the argument is reached. Fails if non-flags hit.
// 2. If the argument starts with a double --, only the full name is considered.
// a. If the named argument (arg) needs a VALUE, it can be provided in these ways:
// --arg=VALUE
// --arg VALUE
// 3. If the argument does not start with any -, it is considered the next positional argument.
// 4. Once a positional argument is encountered, all subsequent arguments are considered positional
// 5. If the command only has 1 positional argument, all subsequent arguments are considered forwarded.
ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal()
{
// Get the next argument from the invocation.
auto currArg = std::wstring_view{*m_invocationItr};
++m_invocationItr;
// If current state has a type, then that means this must be a value for the previous argument.
if (m_state.Type())
{
m_executionArgs.Add(m_state.Type().value(), std::wstring{currArg});
return {};
}
// If this command has forwarded args present and we have found a positional argument,
// the all remaining args are considered positional or forwarded.
if (!m_forwardArgs.empty() && m_anchorPositional.has_value())
{
return ProcessAnchoredPositionals(currArg);
}
// Arg does not begin with '-' so it is neither an alias nor a named value, must be positional.
if (currArg.empty() || currArg[0] != WSLC_CLI_ARG_ID_CHAR)
{
return ProcessPositionalArgument(currArg);
}
// The currentArg is non-empty, and starts with a -.
if (currArg.length() == 1)
{
// If it is only one character, then it is an error since it is neither an alias nor a named argument.
return ArgumentException(Localization::WSLCCLI_InvalidArgumentSpecifierError(currArg));
}
// Single '-' that is 2 characters or more means this must be an alias or collection of alias flags.
if (currArg[1] != WSLC_CLI_ARG_ID_CHAR)
{
return ProcessAliasArgument(currArg);
}
// The currentArg must be a named argument.
return ProcessNamedArgument(currArg);
}
// Assumes non-empty and does not begin with '-'.
ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessPositionalArgument(const std::wstring_view& currArg)
{
if (currArg.empty() || currArg[0] == WSLC_CLI_ARG_ID_CHAR)
{
// Assumption invalid, there is a bug in the logic.
THROW_HR(E_UNEXPECTED);
}
const Argument* nextPositional = NextPositional();
if (!nextPositional)
{
return ArgumentException(Localization::WSLCCLI_ExtraPositionalError(currArg));
}
// First positional found is the anchor positional.
if (!m_anchorPositional.has_value())
{
m_anchorPositional = Argument(*nextPositional);
}
m_executionArgs.Add(nextPositional->Type(), std::wstring{currArg});
return {};
}
// Assumes one positional has already been found and therefore there are no remaining Kind Value/Flag arguments.
// Only Kind::Positional or Kind::Forward arguments should remain.
ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAnchoredPositionals(const std::wstring_view& currArg)
{
if (!m_anchorPositional.has_value())
{
// Invalid state, this is a programmer error.
THROW_HR(E_UNEXPECTED);
}
// If we haven't reached the limit for the anchor positional, treat this as another anchor positional.
if (m_executionArgs.Count(m_anchorPositional.value().Type()) < m_anchorPositional.value().Limit())
{
// 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(m_anchorPositional.value().Type(), std::wstring{currArg});
return {};
}
// There are three possibilities for this argument:
// 1) It is another positional argument (ex: run <imagename> <command>)
// 2) It is a forwarded argument set that could be anything (most likely)
// 3) It is an input error and there should be no such argument.
// Check next positional.
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 {};
}
// Handle case where we expect a positional but
// Check for forwarded arg existence.
if (m_forwardArgs.empty())
{
return ArgumentException(Localization::WSLCCLI_CommandHasNoForwardArgumentsError(currArg));
}
// currArg is the first forwarded argument
// All the rest of the args are forward args.
std::vector<std::wstring> forwardedArgs;
forwardedArgs.push_back(std::wstring{currArg});
while (m_invocationItr != m_invocation.end())
{
forwardedArgs.push_back(std::wstring{*m_invocationItr});
++m_invocationItr;
}
m_executionArgs.Add(m_forwardArgs.front().Type(), std::move(forwardedArgs));
return {};
}
// Assumes argument begins with '-' and is at least 2 characters.
ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgument(const std::wstring_view& currArg)
{
if (currArg.length() < 2 || currArg[0] != WSLC_CLI_ARG_ID_CHAR || currArg[1] == WSLC_CLI_ARG_ID_CHAR)
{
// Assumption invalid, this is a programmer error.
THROW_HR(E_UNEXPECTED);
}
// This may be a collection of boolean alias flags.
// Helper to find an argument by alias starting at a specific position.
auto findArgumentByAlias = [this](const std::wstring_view& str, size_t startPos, size_t& aliasLength) -> const Argument* {
for (const auto& arg : m_standardArgs)
{
const auto& alias = arg.Alias();
if (alias.empty())
{
continue;
}
if (startPos + alias.length() <= str.length() && str.compare(startPos, alias.length(), alias) == 0)
{
aliasLength = alias.length();
return &arg;
}
}
return nullptr;
};
// Find the first alias starting at position 1 (after the '-')
size_t aliasLength = 0;
const Argument* firstArg = findArgumentByAlias(currArg, 1, aliasLength);
if (!firstArg)
{
return ArgumentException(Localization::WSLCCLI_InvalidAliasError(currArg));
}
// Position after the first alias
size_t currentPos = 1 + aliasLength;
// Check if this argument expects a value
if (firstArg->Kind() == Kind::Value)
{
// Kind::Value is only allowed if it's the last flag (no more characters after it, or '=' follows)
if (currentPos >= currArg.length())
{
// No more characters - value should be in next argument
return {firstArg->Type(), currArg};
}
if (currArg[currentPos] != WSLC_CLI_ARG_SPLIT_CHAR)
{
// There are more characters but it's not '=' - this is invalid
return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg));
}
// Value is adjoined after '='
ProcessAdjoinedValue(firstArg->Type(), currArg.substr(currentPos + 1));
return {};
}
// Boolean flag - add it and process any adjoined flags
m_executionArgs.Add(firstArg->Type(), true);
// Process remaining adjoined flags
while (currentPos < currArg.length())
{
const Argument* nextArg = findArgumentByAlias(currArg, currentPos, aliasLength);
if (!nextArg)
{
return ArgumentException(Localization::WSLCCLI_AdjoinedNotFoundError(currArg));
}
// Update position before checking Kind
size_t nextPos = currentPos + aliasLength;
if (nextArg->Kind() == Kind::Value)
{
// Kind::Value is only allowed if it's the last flag
if (nextPos >= currArg.length())
{
// No more characters - value should be in next argument
return {nextArg->Type(), currArg};
}
if (currArg[nextPos] != WSLC_CLI_ARG_SPLIT_CHAR)
{
// There are more characters but it's not '=' - this is invalid
return ArgumentException(Localization::WSLCCLI_ValueMustBeLastInAliasChainError(currArg));
}
// Value is adjoined after '='
ProcessAdjoinedValue(nextArg->Type(), currArg.substr(nextPos + 1));
return {};
}
m_executionArgs.Add(nextArg->Type(), true);
currentPos = nextPos;
}
return {};
}
// Assumes the arg value begins with -- and is at least 2 characters long.
ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgument(const std::wstring_view& currArg)
{
THROW_HR_IF(E_UNEXPECTED, !currArg.starts_with(L"--"));
if (currArg.length() == 2)
{
// Missing argument name after double dash, this is an error.
return ArgumentException(Localization::WSLCCLI_MissingArgumentNameError(currArg));
}
// This is an arg name, find it and process its value if needed.
// Skip the double arg identifier chars.
size_t argStart = currArg.find_first_not_of(WSLC_CLI_ARG_ID_CHAR);
std::wstring_view argName = currArg.substr(argStart);
bool argFound = false;
bool hasAdjoinedValue = false;
std::wstring_view argValue;
size_t splitChar = argName.find_first_of(WSLC_CLI_ARG_SPLIT_CHAR);
if (splitChar != std::string::npos)
{
// There is an '=' in this arg, it has an adjoined value, split it out.
hasAdjoinedValue = true;
argValue = argName.substr(splitChar + 1);
argName = argName.substr(0, splitChar);
}
// Find a matching standard arg with this name.
for (const auto& arg : m_standardArgs)
{
if (string::IsEqual(argName, arg.Name()))
{
// Found a match, process by kind.
if (arg.Kind() == Kind::Flag)
{
// TODO: Consider supporting --flag and --flag=true or --flag=false for bool args.
if (hasAdjoinedValue)
{
return ArgumentException(Localization::WSLCCLI_FlagContainAdjoinedError(currArg));
}
m_executionArgs.Add(arg.Type(), true);
return {};
}
// Not a Flag, must be a Value, and therefore must have a value provided.
if (hasAdjoinedValue)
{
ProcessAdjoinedValue(arg.Type(), argValue);
return {};
}
// The value should be the next argument.
return {arg.Type(), currArg};
}
}
// We found no matching argument for this name, this is an invalid argument name.
return ArgumentException(Localization::WSLCCLI_InvalidNameError(currArg));
}
void ParseArgumentsStateMachine::ProcessAdjoinedValue(ArgType type, std::wstring_view value)
{
// If the adjoined value is wrapped in quotes, strip them off.
if (value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"')
{
value = value.substr(1, value.length() - 2);
}
m_executionArgs.Add(type, std::wstring{value});
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,122 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
ArgumentParser.h
Abstract:
Declaration of the ArgumentParser class for command-line argument parsing.
--*/
#pragma once
#include "Argument.h"
#include "Exceptions.h"
#include "Invocation.h"
#include "ArgumentTypes.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <type_traits>
namespace wsl::windows::wslc {
// The argument parsing state machine.
// It is broken out to enable completion to process arguments, ignore errors,
// and determine the likely state of the word to be completed.
struct ParseArgumentsStateMachine
{
ParseArgumentsStateMachine(Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments);
ParseArgumentsStateMachine(const ParseArgumentsStateMachine&) = delete;
ParseArgumentsStateMachine& operator=(const ParseArgumentsStateMachine&) = delete;
ParseArgumentsStateMachine(ParseArgumentsStateMachine&&) = default;
ParseArgumentsStateMachine& operator=(ParseArgumentsStateMachine&&) = default;
// Processes the next argument from the invocation.
// Returns true if there was an argument to process;
// returns false if there were none.
bool Step();
// Throws if there was an error during the prior step.
void ThrowIfError() const;
// The current state of the state machine.
// An empty state indicates that the next argument can be anything.
struct State
{
State() = default;
State(ArgType type, std::wstring_view arg) : m_type(type), m_arg(arg)
{
}
State(ArgumentException ce) : m_exception(std::move(ce))
{
}
// If set, indicates that the next argument is a value for this type.
const std::optional<ArgType>& Type() const
{
return m_type;
}
// The actual argument string associated with Type.
const std::wstring& Arg() const
{
return m_arg;
}
// If set, indicates that the last argument produced an error.
const std::optional<ArgumentException>& Exception() const
{
return m_exception;
}
private:
std::optional<ArgType> m_type;
std::wstring m_arg;
std::optional<ArgumentException> m_exception;
};
const State& GetState() const
{
return m_state;
}
// Gets the next positional argument, or nullptr if there is not one.
const Argument* NextPositional();
const std::vector<Argument>& Arguments() const
{
return m_arguments;
}
private:
State StepInternal();
State ProcessPositionalArgument(const std::wstring_view& currArg);
State ProcessAnchoredPositionals(const std::wstring_view& currArg);
State ProcessAliasArgument(const std::wstring_view& currArg);
State ProcessNamedArgument(const std::wstring_view& currArg);
void ProcessAdjoinedValue(ArgType type, std::wstring_view value);
Invocation& m_invocation;
ArgMap& m_executionArgs;
std::vector<Argument> m_arguments;
Invocation::iterator m_invocationItr;
std::vector<Argument>::iterator m_positionalSearchItr;
// The anchor positional is the first positional argument processed.
std::optional<Argument> m_anchorPositional = std::nullopt;
// Separate arguments by Kind
std::vector<Argument> m_standardArgs = {};
std::vector<Argument> m_positionalArgs = {};
std::vector<Argument> m_forwardArgs = {};
State m_state;
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,103 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
ArgumentTypes.h
Abstract:
Declaration of the ArgumentTypes, which includes all ArgTypes and their properties.
--*/
#pragma once
#include "ArgumentDefinitions.h"
#include "EnumVariantMap.h"
#include <string>
#include <vector>
#include <array>
#include <type_traits>
namespace wsl::windows::wslc::argument {
// General format: commandname [Flag | Value]* [Positional]* [Forward]
// Argument Kind, which determines both parsing behavior and data type.
enum class Kind
{
// Boolean flag argument (--flag or -f). Data type: bool
Flag,
// String value argument (--option value or -o value). Data type: std::wstring
Value,
// Positional argument (implied by position, no flag). Data type: std::wstring
Positional,
// Forward arguments (remaining args passed through). Data type: std::wstring
Forward,
};
// Generate ArgType enum from X-macro
enum class ArgType : size_t
{
#define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, Desc) EnumName,
WSLC_ARGUMENTS(WSLC_ARG_ENUM)
#undef WSLC_ARG_ENUM
// This should always be at the end
Max,
};
namespace details {
// Map Kind to data type
template <Kind K>
struct KindToType;
template <>
struct KindToType<Kind::Flag>
{
using type = bool;
};
template <>
struct KindToType<Kind::Value>
{
using type = std::wstring;
};
template <>
struct KindToType<Kind::Positional>
{
using type = std::wstring;
};
template <>
struct KindToType<Kind::Forward>
{
using type = std::vector<std::wstring>;
};
template <ArgType D>
struct ArgDataMapping
{
};
// Generate data mappings from X-macro - Kind determines the type
#define WSLC_ARG_MAPPING(EnumName, Name, Alias, ArgumentKind, Desc) \
template <> \
struct ArgDataMapping<ArgType::EnumName> \
{ \
using value_t = typename KindToType<ArgumentKind>::type; \
};
WSLC_ARGUMENTS(WSLC_ARG_MAPPING)
#undef WSLC_ARG_MAPPING
} // namespace details
// This is the main ArgType map used for storing parsed arguments.
struct ArgMap : wsl::windows::wslc::EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping>
{
};
} // namespace wsl::windows::wslc::argument

View File

@@ -0,0 +1,49 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
DiagCommand.cpp
Abstract:
Implementation of DiagCommand command tree.
--*/
#include "CLIExecutionContext.h"
#include "ExecutionContextData.h"
#include "DiagCommand.h"
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc {
// Diag Root Command
std::vector<std::unique_ptr<Command>> DiagCommand::GetCommands() const
{
std::vector<std::unique_ptr<Command>> commands;
commands.reserve(1);
commands.push_back(std::make_unique<DiagListCommand>(FullName()));
return commands;
}
std::vector<Argument> DiagCommand::GetArguments() const
{
return {};
}
std::wstring DiagCommand::ShortDescription() const
{
return {L"Diag command"};
}
std::wstring DiagCommand::LongDescription() const
{
return {L"Diag command for demonstration purposes."};
}
void DiagCommand::ExecuteInternal(CLIExecutionContext& context) const
{
OutputHelp();
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,49 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
DiagCommand.h
Abstract:
Declaration of DiagCommand command tree.
--*/
#pragma once
#include "Command.h"
namespace wsl::windows::wslc {
// Root Diag Command
struct DiagCommand final : public Command
{
constexpr static std::wstring_view CommandName = L"diag";
DiagCommand(std::wstring parent) : Command(CommandName, parent)
{
}
std::vector<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;
std::vector<std::unique_ptr<Command>> GetCommands() const override;
protected:
void ExecuteInternal(CLIExecutionContext& context) const override;
};
// List Command
struct DiagListCommand final : public Command
{
constexpr static std::wstring_view CommandName = L"list";
DiagListCommand(std::wstring parent) : Command(CommandName, parent)
{
}
std::vector<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;
protected:
void ExecuteInternal(CLIExecutionContext& context) const override;
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,47 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
DiagListCommand.cpp
Abstract:
Implementation of the diag list command.
--*/
#include "CLIExecutionContext.h"
#include "ExecutionContextData.h"
#include "DiagCommand.h"
#include "DiagTasks.h"
#include "Task.h"
using namespace wsl::windows::common::wslutil;
using namespace wsl::windows::wslc::execution;
using namespace wsl::windows::wslc::task;
namespace wsl::windows::wslc {
// Diag List Command
std::vector<Argument> DiagListCommand::GetArguments() const
{
return {
Argument::Create(ArgType::Verbose, std::nullopt, std::nullopt, L"Show detailed information about the listed containers."),
};
}
std::wstring DiagListCommand::ShortDescription() const
{
return {L"List containers."};
}
std::wstring DiagListCommand::LongDescription() const
{
return {L"Lists specified container(s)."};
}
void DiagListCommand::ExecuteInternal(CLIExecutionContext& context) const
{
context << task::ListContainers;
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,56 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
RootCommand.cpp
Abstract:
Implementation of the RootCommand, which is the root of all commands in the CLI.
--*/
#include "RootCommand.h"
// Include all commands that parent to the root.
#include "DiagCommand.h"
using namespace wsl::shared;
using namespace wsl::windows::common::wslutil;
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc {
std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
{
std::vector<std::unique_ptr<Command>> commands;
commands.reserve(2);
commands.push_back(std::make_unique<DiagCommand>(FullName()));
commands.push_back(std::make_unique<DiagListCommand>(FullName()));
return commands;
}
std::vector<Argument> RootCommand::GetArguments() const
{
return {
Argument::Create(ArgType::Info),
};
}
std::wstring RootCommand::ShortDescription() const
{
return {L"WSLC is the Windows Subsystem for Linux Container CLI tool."};
}
std::wstring RootCommand::LongDescription() const
{
return {
L"WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL containers "
L"from the command line."};
}
void RootCommand::ExecuteInternal(CLIExecutionContext& context) const
{
OutputHelp();
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,34 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
RootCommand.h
Abstract:
Declaration of the RootCommand, which is the root of all commands in the CLI.
--*/
#pragma once
#include "Command.h"
namespace wsl::windows::wslc {
struct RootCommand final : public Command
{
constexpr static std::wstring_view CommandName = L"root";
RootCommand() : Command(CommandName, {})
{
}
std::vector<std::unique_ptr<Command>> GetCommands() const override;
std::vector<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;
protected:
virtual void ExecuteInternal(CLIExecutionContext& context) const;
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,39 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
CLIExecutionContext.h
Abstract:
Declaration of CLI execution context.
--*/
#pragma once
#include "ArgumentTypes.h"
#include "ExecutionContextData.h"
namespace wsl::windows::wslc::execution {
// The context within which all commands execute.
// Contains arguments via Args.
struct CLIExecutionContext : public wsl::windows::common::ExecutionContext
{
CLIExecutionContext() : wsl::windows::common::ExecutionContext(wsl::windows::common::Context::WslC)
{
}
~CLIExecutionContext() override = default;
CLIExecutionContext(const CLIExecutionContext&) = default;
CLIExecutionContext& operator=(const CLIExecutionContext&) = default;
CLIExecutionContext(CLIExecutionContext&&) = default;
CLIExecutionContext& operator=(CLIExecutionContext&&) = default;
argument::ArgMap Args;
// Map of data stored in the context.
DataMap Data;
};
} // namespace wsl::windows::wslc::execution

View File

@@ -0,0 +1,360 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Command.cpp
Abstract:
Implementation of command execution logic.
--*/
#include "Argument.h"
#include "Command.h"
#include "Invocation.h"
#include "ArgumentParser.h"
using namespace wsl::shared;
using namespace wsl::windows::common::wslutil;
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc {
constexpr std::wstring_view s_ExecutableName = L"wslc";
Command::Command(std::wstring_view name, std::wstring parent) : m_name(name)
{
if (!parent.empty())
{
m_fullName.reserve(parent.length() + 1 + name.length());
m_fullName = parent;
m_fullName += ParentSplitChar;
m_fullName += name;
}
else
{
m_fullName = name;
}
}
// This is the header applied before every help output, for product and copyright information.
// It is separate in case we need to show it in other contexts, such as error messages, or
// during specific command executions.
void Command::OutputIntroHeader() const
{
// Placeholder header.
// TODO: Get better product version information dynamically instead of hardcoding it here.
// TODO: Strings should be in resources.
std::wostringstream infoOut;
infoOut << L"Windows Subsystem for Linux Container CLI (Preview) v1.0.0" << std::endl;
infoOut << L"Copyright (c) Microsoft Corporation. All rights reserved." << std::endl;
PrintMessage(infoOut.str(), stdout);
}
void Command::OutputHelp(const CommandException* exception) const
{
// Header
OutputIntroHeader();
// Error if given
if (exception)
{
PrintMessage(exception->Message(), stderr);
}
// Description
std::wostringstream infoOut;
infoOut << LongDescription() << std::endl << std::endl;
// Example usage for this command
// First create the command chain for output
std::wstring commandChain = FullName();
size_t firstSplit = commandChain.find_first_of(ParentSplitChar);
if (firstSplit == std::wstring::npos)
{
commandChain.clear();
}
else
{
commandChain = commandChain.substr(firstSplit + 1);
for (wchar_t& c : commandChain)
{
if (c == ParentSplitChar)
{
c = L' ';
}
}
}
// Usage follows the Microsoft convention:
// https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key
// Output the command preamble and command chain
infoOut << Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
auto commands = GetCommands();
auto arguments = GetAllArguments();
// Separate arguments by Kind
std::vector<Argument> standardArgs;
std::vector<Argument> positionalArgs;
std::vector<Argument> forwardArgs;
bool requiredPositionalArgsExist = false;
for (const auto& arg : arguments)
{
switch (arg.Kind())
{
case Kind::Flag:
standardArgs.emplace_back(arg);
break;
case Kind::Value:
standardArgs.emplace_back(arg);
break;
case Kind::Positional:
positionalArgs.emplace_back(arg);
if (arg.Required())
{
requiredPositionalArgsExist = true;
}
break;
case Kind::Forward:
forwardArgs.emplace_back(arg);
break;
}
}
bool hasArguments = !positionalArgs.empty();
bool hasOptions = !standardArgs.empty();
bool hasForwardArgs = !forwardArgs.empty();
// Output the command token, made optional if arguments are present.
if (!commands.empty())
{
infoOut << ' ';
if (!arguments.empty())
{
infoOut << L'[';
}
infoOut << L'<' << Localization::WSLCCLI_Command() << L'>';
if (!arguments.empty())
{
infoOut << L']';
}
}
// For WSLC format of command [<options>] <positional> <args | positional2..>
// Add options to the usage if there are options present.
if (hasOptions)
{
infoOut << L" [<" << Localization::WSLCCLI_Options() << L">]";
}
// Add arguments to the usage if there are arguments present. Positional come after
// options and may be optional or required.
for (const auto& arg : positionalArgs)
{
infoOut << L' ';
if (!arg.Required())
{
infoOut << L'[';
}
infoOut << L'<' << arg.Name() << L'>';
if (arg.Limit() > 1)
{
infoOut << L"...";
}
if (!arg.Required())
{
infoOut << L']';
}
}
if (hasForwardArgs)
{
// Assume only one forward arg is present, as multiple forwards would be
// ambiguous in usage. Revisit if this becomes a scenario.
infoOut << L" [<" << forwardArgs.front().Name() << L">...]";
}
infoOut << std::endl << std::endl;
if (!commands.empty())
{
if (Name() == FullName())
{
infoOut << Localization::WSLCCLI_AvailableCommands() << std::endl;
}
else
{
infoOut << Localization::WSLCCLI_AvailableSubcommands() << std::endl;
}
size_t maxCommandNameLength = 0;
for (const auto& command : commands)
{
maxCommandNameLength = std::max(maxCommandNameLength, command->Name().length());
}
for (const auto& command : commands)
{
size_t fillChars = (maxCommandNameLength - command->Name().length()) + 2;
infoOut << L" " << command->Name() << std::wstring(fillChars, L' ') << command->ShortDescription() << std::endl;
}
infoOut << std::endl << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L']' << std::endl;
}
if (!arguments.empty())
{
if (!commands.empty())
{
infoOut << std::endl;
}
size_t maxArgNameLength = 0;
for (const auto& arg : arguments)
{
auto argLength = arg.GetUsageString().length();
maxArgNameLength = std::max(maxArgNameLength, argLength);
}
if (hasArguments)
{
infoOut << Localization::WSLCCLI_AvailableArguments() << std::endl;
for (const auto& arg : positionalArgs)
{
size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
}
}
if (hasForwardArgs)
{
for (const auto& arg : forwardArgs)
{
size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
}
}
if (hasOptions)
{
if (hasArguments || hasForwardArgs)
{
infoOut << std::endl;
}
infoOut << Localization::WSLCCLI_AvailableOptions() << std::endl;
for (const auto& arg : standardArgs)
{
auto usage = arg.GetUsageString();
size_t fillChars = (maxArgNameLength - usage.length()) + 2;
infoOut << L" " << usage << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
}
}
}
PrintMessage(infoOut.str(), stdout);
}
std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const
{
auto itr = inv.begin();
if (itr == inv.end() || (*itr)[0] == WSLC_CLI_ARG_ID_CHAR)
{
// No more command arguments to check, so no command to find
return {};
}
auto commands = GetCommands();
if (commands.empty())
{
return {};
}
for (auto& command : commands)
{
if (string::IsEqual(*itr, command->Name()))
{
inv.consume(itr);
return std::move(command);
}
}
throw CommandException(Localization::WSLCCLI_UnrecognizedCommandError(std::wstring_view{*itr}));
}
// Convert the invocation vector into a map of argument types and their associated values.
// Argument map is based on the arguments that the command defines and are stored as
// an enum -> variant multimap. This is parsing and value storage only, not validation of
// the argument data.
void Command::ParseArguments(Invocation& inv, ArgMap& execArgs) const
{
auto definedArgs = GetAllArguments();
ParseArgumentsStateMachine stateMachine{inv, execArgs, std::move(definedArgs)};
while (stateMachine.Step())
{
stateMachine.ThrowIfError();
}
}
// Validates the ArgMap produced by ParseArguments. ArgMap is assumed to have
// been populated and parsed successfully from the invocation and now we are validating
// that the arguments provided meet the requirements of the command. This includes checking
// that all required arguments are present and no arguments exceed their count limits.
// Any defined validation for specific ArgTypes are also run.
void Command::ValidateArguments(ArgMap& execArgs) const
{
// If help is asked for, don't bother validating anything else.
if (execArgs.Contains(ArgType::Help))
{
return;
}
auto allArgs = GetAllArguments();
for (const auto& arg : allArgs)
{
if (arg.Required() && !execArgs.Contains(arg.Type()))
{
throw CommandException(Localization::WSLCCLI_RequiredArgumentError(arg.Name()));
}
if (arg.Limit() < execArgs.Count(arg.Type()))
{
throw CommandException(Localization::WSLCCLI_TooManyArgumentsError(arg.Name()));
}
}
}
void Command::Execute(CLIExecutionContext& context) const
{
// If Help was part of the validated argument set, we will output help instead of executing.
if (context.Args.Contains(ArgType::Help))
{
OutputHelp();
}
else
{
// Execute internal has the actual command execution path.
ExecuteInternal(context);
}
}
// External execution entry point called by the core execution flow.
void Execute(CLIExecutionContext& context, std::unique_ptr<Command>& command)
{
command->Execute(context);
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,93 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Command.h
Abstract:
Declaration of command class.
--*/
#pragma once
#include "Argument.h"
#include "Exceptions.h"
#include "ArgumentTypes.h"
#include "CLIExecutionContext.h"
#include "Invocation.h"
#include "ArgumentParser.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
using namespace wsl::windows::wslc::execution;
using namespace wsl::windows::wslc::argument;
namespace wsl::windows::wslc {
struct Command
{
// The character used to split between commands and their parents in FullName.
constexpr static wchar_t ParentSplitChar = L':';
Command(std::wstring_view name, std::wstring parent);
virtual ~Command() = default;
Command(const Command&) = default;
Command& operator=(const Command&) = default;
Command(Command&&) = default;
Command& operator=(Command&&) = default;
std::wstring_view Name() const
{
return m_name;
}
const std::wstring& FullName() const
{
return m_fullName;
}
virtual std::vector<std::unique_ptr<Command>> GetCommands() const
{
return {};
}
virtual std::vector<Argument> GetArguments() const
{
return {};
}
virtual std::vector<Argument> GetAllArguments() const
{
auto args = GetArguments();
args.emplace_back(Argument::Create(ArgType::Help));
return args;
}
virtual std::wstring ShortDescription() const = 0;
virtual std::wstring LongDescription() const = 0;
void OutputIntroHeader() const;
void OutputHelp(const CommandException* exception = nullptr) const;
std::unique_ptr<Command> FindSubCommand(Invocation& inv) const;
void ParseArguments(Invocation& inv, ArgMap& execArgs) const;
void ValidateArguments(ArgMap& execArgs) const;
virtual void Execute(CLIExecutionContext& context) const;
protected:
virtual void ExecuteInternal(CLIExecutionContext& context) const = 0;
private:
std::wstring_view m_name;
std::wstring m_fullName;
};
void Execute(CLIExecutionContext& context, std::unique_ptr<Command>& command);
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,339 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
EnumVariantMap.h
Abstract:
Template for enum-based variant maps.
--*/
#pragma once
#include <map>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
// This template set is used for Arg storage and Context Data storage by enum type.
// The backing storage is a std::multimap of the enum to a variant of types.
// This enables strongly typed storage and retrieval of values based on an enum key.
namespace wsl::windows::wslc {
// Enum based variant helper.
// Enum must be an enum whose first member has the value 0, each subsequent member increases by 1, and the final member is named Max.
// Mapping is a template type that takes one template parameter of type Enum, and whose members define value_t as the type for that enum value.
template <typename Enum, template <Enum> typename Mapping>
struct EnumBasedVariant
{
private:
// Used to deduce the variant type; making a variant that includes std::monostate and all Mapping types.
template <size_t... I>
static inline auto Deduce(std::index_sequence<I...>)
{
return std::variant<std::monostate, typename Mapping<static_cast<Enum>(I)>::value_t...>{};
}
public:
// Holds data of any type listed in Mapping.
using variant_t = decltype(Deduce(std::make_index_sequence<static_cast<size_t>(Enum::Max)>()));
// Gets the index into the variant for the given Data.
static constexpr inline size_t Index(Enum e)
{
return static_cast<size_t>(e) + 1;
}
};
// An action that can be taken on an EnumBasedVariantMap.
enum class EnumBasedVariantMapAction
{
Add,
Contains,
Get,
GetAll,
Count,
Remove,
};
// A callback function that can be used for logging map actions.
template <typename Enum>
using EnumBasedVariantMapActionCallback = void (*)(const void* map, Enum value, EnumBasedVariantMapAction action);
// Forward declaration for EnumBasedVariantMapEmplacer
template <typename Enum, template <Enum> typename Mapping, typename V>
struct EnumBasedVariantMapEmplacer;
// Provides a multimap of the Enum to the mapped types (allows multiple values per key).
template <typename Enum, template <Enum> typename Mapping, EnumBasedVariantMapActionCallback<Enum> Callback = nullptr>
struct EnumBasedVariantMap
{
using Variant = EnumBasedVariant<Enum, Mapping>;
template <Enum E>
using mapping_t = typename Mapping<E>::value_t;
// Adds a value to the map. With multimap, this always adds a new entry (doesn't overwrite).
template <Enum E>
void Add(mapping_t<E>&& v)
{
if constexpr (Callback)
{
Callback(this, E, EnumBasedVariantMapAction::Add);
}
// Compile-time type checking - this should always pass since mapping_t<E> is the correct type
using CleanV = std::remove_cvref_t<mapping_t<E>>;
static_assert(
std::is_same_v<CleanV, mapping_t<E>>,
"Type mismatch in Add: provided type does not match the expected type for this enum value");
typename Variant::variant_t variant;
variant.template emplace<Variant::Index(E)>(std::move(v));
m_data.emplace(E, std::move(variant));
}
template <Enum E>
void Add(const mapping_t<E>& v)
{
if constexpr (Callback)
{
Callback(this, E, EnumBasedVariantMapAction::Add);
}
// Compile-time type checking - this should always pass since mapping_t<E> is the correct type
using CleanV = std::remove_cvref_t<mapping_t<E>>;
static_assert(
std::is_same_v<CleanV, mapping_t<E>>,
"Type mismatch in Add: provided type does not match the expected type for this enum value");
typename Variant::variant_t variant;
variant.template emplace<Variant::Index(E)>(v);
m_data.emplace(E, std::move(variant));
}
// Runtime version of Add that takes the enum as a parameter.
template <typename V>
void Add(Enum e, V&& v)
{
if constexpr (Callback)
{
Callback(this, e, EnumBasedVariantMapAction::Add);
}
// Check if the type matches the SPECIFIC enum value at compile time if possible
using CleanV = std::remove_cvref_t<V>;
// Pre-check if this type matches the specific enum value being added to
if (!IsMatchingType<CleanV>(e))
{
THROW_HR_MSG(E_INVALIDARG, "Type mismatch: provided type does not match the expected type for enum value %d", static_cast<int>(e));
}
typename Variant::variant_t variant;
EmplaceAtRuntimeIndex(variant, e, std::forward<V>(v), std::make_index_sequence<static_cast<size_t>(Enum::Max)>());
m_data.emplace(e, std::move(variant));
}
// Runtime method to check if value V matches the mapped type for an enum value.
template <typename V>
bool IsMatchingType(Enum e) const
{
return IsMatchingTypeImpl<V>(e, std::make_index_sequence<static_cast<size_t>(Enum::Max)>());
}
// Return a value indicating whether the given enum has at least one entry.
bool Contains(Enum e) const
{
if constexpr (Callback)
{
Callback(this, e, EnumBasedVariantMapAction::Contains);
}
return (m_data.find(e) != m_data.end());
}
// Gets the count of values for a specific enum key.
size_t Count(Enum e) const
{
if constexpr (Callback)
{
Callback(this, e, EnumBasedVariantMapAction::Count);
}
return m_data.count(e);
}
// Gets the FIRST value for the enum key (for backward compatibility).
// Non-const version returns a reference that can be modified.
template <Enum E>
mapping_t<E>& Get()
{
if constexpr (Callback)
{
Callback(this, E, EnumBasedVariantMapAction::Get);
}
auto itr = m_data.find(E);
THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.end(), "Get(%d): key not found", static_cast<int>(E));
// Validate that the variant holds the expected type at the expected index
constexpr size_t expectedIndex = Variant::Index(E);
if (itr->second.index() != expectedIndex)
{
THROW_HR_MSG(
E_UNEXPECTED,
"Get(%d): variant type mismatch - expected index %zu, got %zu",
static_cast<int>(E),
expectedIndex,
itr->second.index());
}
return std::get<expectedIndex>(itr->second);
}
// Const overload of Get, cannot be modified.
template <Enum E>
const mapping_t<E>& Get() const
{
if constexpr (Callback)
{
Callback(this, E, EnumBasedVariantMapAction::Get);
}
auto itr = m_data.find(E);
THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.cend(), "Get(%d): key not found", static_cast<int>(E));
// Validate that the variant holds the expected type at the expected index
constexpr size_t expectedIndex = Variant::Index(E);
if (itr->second.index() != expectedIndex)
{
THROW_HR_MSG(
E_UNEXPECTED,
"Get(%d): variant type mismatch - expected index %zu, got %zu",
static_cast<int>(E),
expectedIndex,
itr->second.index());
}
return std::get<expectedIndex>(itr->second);
}
// Gets ALL values for a specific enum key as a vector.
template <Enum E>
std::vector<mapping_t<E>> GetAll() const
{
if constexpr (Callback)
{
Callback(this, E, EnumBasedVariantMapAction::GetAll);
}
std::vector<mapping_t<E>> results;
auto range = m_data.equal_range(E);
for (auto it = range.first; it != range.second; ++it)
{
results.push_back(std::get<Variant::Index(E)>(it->second));
}
return results;
}
// Removes ALL entries for a specific enum key.
void Remove(Enum e)
{
if constexpr (Callback)
{
Callback(this, e, EnumBasedVariantMapAction::Remove);
}
m_data.erase(e);
}
// Gets the total number of items stored (across all keys).
size_t GetCount() const
{
return m_data.size();
}
// Gets a vector of all UNIQUE enum keys stored in the map.
std::vector<Enum> GetKeys() const
{
std::vector<Enum> keys;
Enum lastKey = static_cast<Enum>(-1);
bool first = true;
for (const auto& pair : m_data)
{
if (first || pair.first != lastKey)
{
keys.push_back(pair.first);
lastKey = pair.first;
first = false;
}
}
return keys;
}
private:
// Helper to implement runtime type checking.
template <typename V, size_t... I>
bool IsMatchingTypeImpl(Enum e, std::index_sequence<I...>) const
{
bool result = false;
((static_cast<size_t>(e) == I ? (result = std::is_same_v<std::remove_cvref_t<V>, mapping_t<static_cast<Enum>(I)>>, true) : false) || ...);
return result;
}
// Helper to emplace at runtime-determined index
template <typename V, size_t... I>
void EmplaceAtRuntimeIndex(typename Variant::variant_t& variant, Enum e, V&& v, std::index_sequence<I...>)
{
size_t index = static_cast<size_t>(e) + 1;
bool handled = false;
(
[&] {
if (index == I + 1 && !handled)
{
using Emplacer = wsl::windows::wslc::EnumBasedVariantMapEmplacer<Enum, Mapping, V>;
Emplacer::template Emplace<I + 1>(variant, std::forward<V>(v));
handled = true;
}
}(),
...);
if (!handled)
{
using CleanV = std::remove_cvref_t<V>;
THROW_HR_MSG(E_INVALIDARG, "Invalid enum value: %d", static_cast<int>(e));
}
}
std::multimap<Enum, typename Variant::variant_t> m_data;
};
// Helper for runtime emplacement into std::variant for EnumBasedVariantMap
template <typename Enum, template <Enum> typename Mapping, typename V>
struct EnumBasedVariantMapEmplacer
{
template <size_t Index>
static void Emplace(typename EnumBasedVariant<Enum, Mapping>::variant_t& variant, V&& value)
{
using TargetType = typename Mapping<static_cast<Enum>(Index - 1)>::value_t;
using CleanV = std::remove_cvref_t<V>;
constexpr bool is_same_type = std::is_same_v<CleanV, TargetType>;
constexpr bool is_convertible = std::is_convertible_v<CleanV, TargetType>;
constexpr bool is_constructible = std::is_constructible_v<TargetType, CleanV>;
if constexpr (is_same_type || is_convertible || is_constructible)
{
variant.template emplace<Index>(std::forward<V>(value));
}
else
{
throw std::runtime_error("Runtime type mismatch: cannot convert value to target type for this enum value");
}
}
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,40 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Exceptions.h
Abstract:
Header file for Exceptions.
--*/
#pragma once
namespace wsl::windows::wslc {
// Base exception for all command-related errors
struct CommandException
{
CommandException(std::wstring_view message) : m_message(message)
{
}
const std::wstring& Message() const
{
return m_message;
}
protected:
std::wstring m_message;
};
// Specific exception for argument parsing errors
struct ArgumentException : CommandException
{
ArgumentException(std::wstring_view message) : CommandException(message)
{
}
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,49 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
ExecutionContextData.h
Abstract:
Header file for defining execution context data mappings.
--*/
#pragma once
#include "EnumVariantMap.h"
#include <string>
#define DEFINE_DATA_MAPPING(_typeName_, _valueType_) \
template <> \
struct DataMapping<Data::_typeName_> \
{ \
using value_t = _valueType_; \
};
namespace wsl::windows::wslc::execution {
// Names a piece of data stored in the context by a task step.
// Must start at 0 to enable direct access to variant in Context.
// Max must be last and unused.
enum class Data : size_t
{
SessionId,
Max
};
namespace details {
template <Data D>
struct DataMapping
{
};
DEFINE_DATA_MAPPING(SessionId, std::wstring);
} // namespace details
struct DataMap : wsl::windows::wslc::EnumBasedVariantMap<Data, wsl::windows::wslc::execution::details::DataMapping>
{
};
} // namespace wsl::windows::wslc::execution

View File

@@ -0,0 +1,100 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Invocation.h
Abstract:
Header file for walking through and processing a command line invocation.
--*/
#pragma once
#include <string>
#include <vector>
namespace wsl::windows::wslc {
struct Invocation
{
Invocation(std::vector<std::wstring>&& args) : m_args(std::move(args))
{
}
struct iterator
{
iterator(size_t arg, std::vector<std::wstring>& args) : m_arg(arg), m_args(args)
{
}
iterator(const iterator&) = default;
iterator& operator=(const iterator&) = default;
iterator operator++()
{
return {++m_arg, m_args};
}
iterator operator++(int)
{
return {m_arg++, m_args};
}
iterator operator--()
{
return {--m_arg, m_args};
}
iterator operator--(int)
{
return {m_arg--, m_args};
}
bool operator==(const iterator& other) const
{
return m_arg == other.m_arg;
}
bool operator!=(const iterator& other) const
{
return m_arg != other.m_arg;
}
const std::wstring& operator*() const
{
return m_args[m_arg];
}
const std::wstring* operator->() const
{
return &(m_args[m_arg]);
}
size_t index() const
{
return m_arg;
}
private:
size_t m_arg;
std::vector<std::wstring>& m_args;
};
size_t size() const
{
return m_args.size();
}
iterator begin()
{
return {m_currentFirstArg, m_args};
}
iterator end()
{
return {m_args.size(), m_args};
}
void consume(const iterator& i)
{
m_currentFirstArg = i.index() + 1;
}
private:
std::vector<std::wstring> m_args;
size_t m_currentFirstArg = 0;
};
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,116 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Main.cpp
Abstract:
Main program entry point.
--*/
#define WIN32_LEAN_AND_MEAN
#pragma once
#include <Windows.h>
#include "precomp.h"
#include "wslutil.h"
#include "Errors.h"
#include "CLIExecutionContext.h"
#include "Invocation.h"
#include "RootCommand.h"
using namespace wsl::shared;
using namespace wsl::windows::common;
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc {
int CoreMain(int argc, wchar_t const** argv)
try
{
EnableContextualizedErrors(false, true);
CLIExecutionContext context;
HRESULT result = S_OK;
// Initialize runtime and COM.
wslutil::ConfigureCrt();
wslutil::InitializeWil();
WslTraceLoggingInitialize(WslaTelemetryProvider, !wsl::shared::OfficialBuild);
auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); });
wslutil::SetCrtEncoding(_O_U8TEXT);
auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
wslutil::CoInitializeSecurity();
WSADATA data{};
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &data));
auto wsaCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WSACleanup(); });
std::unique_ptr<Command> command = std::make_unique<RootCommand>();
try
{
std::vector<std::wstring> args;
for (int i = 1; i < argc; ++i)
{
args.emplace_back(argv[i]);
}
Invocation invocation{std::move(args)};
std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
while (subCommand)
{
command = std::move(subCommand);
subCommand = command->FindSubCommand(invocation);
}
command->ParseArguments(invocation, context.Args);
command->ValidateArguments(context.Args);
command->Execute(context);
}
// Exceptions specific to parsing the arguments of a command
catch (const CommandException& ce)
{
// A command exception means there was an input failure. Display the help
// along with the error message to help the user correct their input.
command->OutputHelp(&ce);
return E_INVALIDARG;
}
// Any other type of error unrelated to the command parsing.
catch (...)
{
LOG_CAUGHT_EXCEPTION();
// Using WSL shared utility to get the HRESULT from the caught exception.
// CLIExecutionContext is a derived class of wsl::windows::common::ExecutionContext.
result = wil::ResultFromCaughtException();
if (FAILED(result))
{
if (const auto& reported = context.ReportedError())
{
auto strings = wslutil::ErrorToString(*reported);
auto errorMessage = strings.Message.empty() ? strings.Code : strings.Message;
wslutil::PrintMessage(Localization::MessageErrorCode(errorMessage, wslutil::ErrorCodeToString(result)), stderr);
}
else
{
// Fallback for errors without context
wslutil::PrintMessage(Localization::MessageErrorCode("", wslutil::ErrorCodeToString(result)), stderr);
}
}
}
return result;
}
catch (...)
{
return E_UNEXPECTED;
}
} // namespace wsl::windows::wslc
int wmain(int argc, wchar_t const** argv)
{
return wsl::windows::wslc::CoreMain(argc, argv);
}

View File

@@ -143,4 +143,4 @@ int wmain(int, wchar_t**)
}
return exitCode;
}
}

View File

@@ -0,0 +1,105 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
DiagTasks.cpp
Abstract:
Implementation of diag command related execution logic.
--*/
#include "Argument.h"
#include "CLIExecutionContext.h"
#include "Task.h"
#include "DiagTasks.h"
using namespace wsl::shared;
using namespace wsl::windows::common;
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc::task {
// Sample execution task using wsladiag's List implementation.
void ListContainers(CLIExecutionContext& context)
{
// This would probably be in another task or wrapper, as working with sessions is common code, and
// there is a common --session argument to reuse sessions. But including it here for simplicity of the sample.
wil::com_ptr<IWSLASessionManager> manager;
THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLASessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&manager)));
wsl::windows::common::security::ConfigureForCOMImpersonation(manager.get());
wil::unique_cotaskmem_array_ptr<WSLA_SESSION_INFORMATION> sessions;
THROW_IF_FAILED(manager->ListSessions(&sessions, sessions.size_address<ULONG>()));
// For flag args, just its presence is equivalent to testing the value, so simple arg containment check.
if (context.Args.Contains(ArgType::Verbose))
{
const wchar_t* plural = sessions.size() == 1 ? L"" : L"s";
wslutil::PrintMessage(std::format(L"[diag] Found {} session{}", sessions.size(), plural), stdout);
}
if (sessions.size() == 0)
{
wslutil::PrintMessage(Localization::MessageWslaNoSessionsFound(), stdout);
return;
}
wslutil::PrintMessage(Localization::MessageWslaSessionsFound(sessions.size(), sessions.size() == 1 ? L"" : L"s"), stdout);
// Use localized headers
const auto idHeader = Localization::MessageWslaHeaderId();
const auto pidHeader = Localization::MessageWslaHeaderCreatorPid();
const auto nameHeader = Localization::MessageWslaHeaderDisplayName();
size_t idWidth = idHeader.size();
size_t pidWidth = pidHeader.size();
size_t nameWidth = nameHeader.size();
for (const auto& s : sessions)
{
idWidth = std::max(idWidth, std::to_wstring(s.SessionId).size());
pidWidth = std::max(pidWidth, std::to_wstring(s.CreatorPid).size());
nameWidth = std::max(nameWidth, static_cast<size_t>(s.DisplayName ? wcslen(s.DisplayName) : 0));
}
// Header
wprintf(
L"%-*ls %-*ls %-*ls\n",
static_cast<int>(idWidth),
idHeader.c_str(),
static_cast<int>(pidWidth),
pidHeader.c_str(),
static_cast<int>(nameWidth),
nameHeader.c_str());
// Underline
std::wstring idDash(idWidth, L'-');
std::wstring pidDash(pidWidth, L'-');
std::wstring nameDash(nameWidth, L'-');
wprintf(
L"%-*ls %-*ls %-*ls\n",
static_cast<int>(idWidth),
idDash.c_str(),
static_cast<int>(pidWidth),
pidDash.c_str(),
static_cast<int>(nameWidth),
nameDash.c_str());
// Rows
for (const auto& s : sessions)
{
const wchar_t* displayName = s.DisplayName ? s.DisplayName : L"";
wprintf(
L"%-*lu %-*lu %-*ls\n",
static_cast<int>(idWidth),
static_cast<unsigned long>(s.SessionId),
static_cast<int>(pidWidth),
static_cast<unsigned long>(s.CreatorPid),
static_cast<int>(nameWidth),
displayName);
}
}
} // namespace wsl::windows::wslc::task

View File

@@ -0,0 +1,21 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
DiagTasks.h
Abstract:
Declaration of diag command execution tasks.
--*/
#pragma once
#include "CLIExecutionContext.h"
using wsl::windows::wslc::execution::CLIExecutionContext;
namespace wsl::windows::wslc::task {
void ListContainers(CLIExecutionContext& context);
} // namespace wsl::windows::wslc::task

View File

@@ -0,0 +1,56 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Task.h
Abstract:
Declaration of a task for function composition and chaining.
--*/
#pragma once
#include "CLIExecutionContext.h"
#include <functional>
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc::task {
struct Task
{
using Func = std::function<void(CLIExecutionContext&)>;
Task(void (*f)(CLIExecutionContext&)) : m_func(f)
{
}
Task(Func f) : m_func(std::move(f))
{
}
Task(const Task&) = default;
Task& operator=(const Task&) = default;
void operator()(CLIExecutionContext& context) const
{
m_func(context);
}
private:
Func m_func;
};
inline CLIExecutionContext& operator<<(CLIExecutionContext& context, const Task& task)
{
return task(context), context;
}
inline CLIExecutionContext& operator<<(CLIExecutionContext& context, void (*f)(CLIExecutionContext&))
{
return context << Task(f);
}
} // namespace wsl::windows::wslc::task

View File

@@ -24,6 +24,7 @@ target_link_directories(wsltests PRIVATE ${BIN})
target_precompile_headers(wsltests REUSE_FROM common)
target_link_libraries(wsltests
common
wslclib
${TAEF_LINK_LIBRARIES}
${COMMON_LINK_LIBRARIES}
VirtDisk.lib
@@ -31,5 +32,9 @@ target_link_libraries(wsltests
Dbghelp.lib
sfc.lib)
add_dependencies(wsltests wslserviceidl)
add_subdirectory(testplugin)
add_dependencies(wsltests wslserviceidl wslclib wslc)
add_subdirectory(testplugin)
add_subdirectory(wslc)
# For prettier source tree browsing
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS})

View File

@@ -0,0 +1,34 @@
# WSLC CLI Unit Tests
set(WSLC_TEST_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/WSLCCLIParserUnitTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WSLCCLIArgumentUnitTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WSLCCLICommandUnitTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WSLCCLIExecutionUnitTests.cpp
)
set(WSLC_TEST_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/WSLCCLITestHelpers.h
${CMAKE_CURRENT_SOURCE_DIR}/CommandLineTestCases.h
${CMAKE_CURRENT_SOURCE_DIR}/ParserTestCases.h
)
# Add the sources to the parent wsltests target.
# This ensures they're compiled into the main test binary.
target_sources(wsltests PRIVATE ${WSLC_TEST_SOURCES})
# Ensure the file uses the precompiled header from the parent target.
set_source_files_properties(${WSLC_TEST_SOURCES}
PROPERTIES
COMPILE_FLAGS "/Yuprecomp.h"
)
# Add include directories needed for WSLC tests.
target_include_directories(wsltests PRIVATE
${CMAKE_SOURCE_DIR}/test
${CMAKE_SOURCE_DIR}/test/windows
${CMAKE_SOURCE_DIR}/src/windows/wslc/core
${CMAKE_SOURCE_DIR}/src/windows/wslc/commands
${CMAKE_SOURCE_DIR}/src/windows/wslc/arguments
${CMAKE_SOURCE_DIR}/src/windows/wslc/tasks
)

View File

@@ -0,0 +1,34 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
CommandLineTestCases.h
Abstract:
Test case data for command-line parsing tests.
--*/
// These cases should be for testing valid command lines against the defined commands.
// This executes the command line parsing logic and verifies that the command line is valid
// for the defined commands. It does not actually execute the command.
// X-Macro definition: COMMAND_LINE_TEST_CASE(commandLine, expectedCommand, shouldSucceed)
// Root command tests
COMMAND_LINE_TEST_CASE(L"", L"root", true)
COMMAND_LINE_TEST_CASE(L"--help", L"root", true)
// Diag command tests
COMMAND_LINE_TEST_CASE(L"diag list", L"list", true)
COMMAND_LINE_TEST_CASE(L"diag list -v", L"list", true)
COMMAND_LINE_TEST_CASE(L"diag list --verbose", L"list", true)
COMMAND_LINE_TEST_CASE(L"diag list --verbose --help", L"list", true)
COMMAND_LINE_TEST_CASE(L"diag list --notanarg", L"list", false)
COMMAND_LINE_TEST_CASE(L"diag list extraarg", L"list", false)
// Error cases
COMMAND_LINE_TEST_CASE(L"invalid command", L"", false)

View File

@@ -0,0 +1,129 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
ParserTestCases.h
Abstract:
X-macro definitions for WSLC CLI parser test cases.
--*/
#pragma once
#include "Argument.h"
#include "ArgumentTypes.h"
#include <string>
#include <vector>
// ArgumentSet enum - defines which set of arguments to use for parsing
enum class ArgumentSet
{
Run,
List,
};
// ParserTestCase - represents a single test case
struct ParserTestCase
{
ArgumentSet argumentSet;
bool expectedResult;
std::wstring commandLine;
};
// Function to get the argument definitions for a given ArgumentSet
inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet argumentSet)
{
using namespace wsl::windows::wslc;
using namespace wsl::windows::wslc::argument;
switch (argumentSet)
{
case ArgumentSet::Run:
return {
Argument::Create(ArgType::ContainerId, 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),
Argument::Create(ArgType::Verbose),
Argument::Create(ArgType::Remove),
Argument::Create(ArgType::Publish, false, 3), // Not required, up to 3 values.
};
case ArgumentSet::List:
return {
Argument::Create(ArgType::ContainerId, false, 10), // Optional positional
Argument::Create(ArgType::Help),
Argument::Create(ArgType::Verbose),
};
default:
return {};
}
}
// X-macro format: WSLC_PARSER_TEST_CASE(ArgumentSetValue, ExpectedResult, CommandLine)
// ArgumentSetValue: Just the enum value name (e.g., Run), without ArgumentSet:: prefix
// ExpectedResult: true if test should succeed, false if it should fail
// CommandLine: The command line string to test
// clang-format off
#define WSLC_PARSER_TEST_CASES \
/* Simple case with required arg and simple other args */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -?)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc --verbose cont1)") \
\
/* 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)") \
\
/* Flag parse tests */ \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -v cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -vi cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -rm cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -virm cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -vrmi cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -rmiv cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -rmiv- cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -rmivp- cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -prmiv cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -prmiv=80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, false, LR"(wslc -prmiv 80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -rmivp 80:80 cont1)") \
WSLC_PARSER_TEST_CASE(Run, true, LR"(wslc -rmivp=80:80 cont1)") \
\
/* 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)") \
\
/* List cases with multiple args and flags that can come after the optional multi-positional. */ \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc)") \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1)") \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 cont2)") \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc --verbose cont1)") \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc --verbose cont1 cont2)") \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 --verbose cont2)") \
WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 cont2 --verbose)") \
\
/* Failure List cases */ \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc --invalidarg)") \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc --invalidarg cont1)") \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -i cont1 cont2)") \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -vp cont1)") \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 -v cont2 -12)") \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=false cont2)") \
WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)")
// clang-format on

View File

@@ -0,0 +1,174 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WSLCCLIArgumentUnitTests.cpp
Abstract:
This file contains unit tests for WSLC CLI argument parsing and validation.
--*/
#include "precomp.h"
#include "windows/Common.h"
#include "WSLCCLITestHelpers.h"
#include "Argument.h"
#include "ArgumentTypes.h"
using namespace wsl::windows::wslc;
using namespace wsl::windows::wslc::argument;
using namespace WSLCTestHelpers;
using namespace WEX::Logging;
using namespace WEX::Common;
using namespace WEX::TestExecution;
namespace WSLCCLIArgumentUnitTests {
class WSLCCLIArgumentUnitTests
{
WSL_TEST_CLASS(WSLCCLIArgumentUnitTests)
TEST_CLASS_SETUP(TestClassSetup)
{
// Add any necessary setup for argument tests
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
// Add any necessary cleanup for argument tests
return true;
}
// Test: Verify Argument::Create() successfully creates arguments for all ArgType enum values
TEST_METHOD(ArgumentCreate_AllArguments)
{
// ArgMap is the container for processed args.
ArgMap args;
// Iterate through all ArgType enum values except Max
auto allArgTypes = std::vector<ArgType>{};
for (int i = 0; i < static_cast<int>(ArgType::Max); ++i)
{
ArgType argType = static_cast<ArgType>(i);
// Create argument using Create
Argument arg = Argument::Create(argType);
// Verify the argument was created successfully by checking its type matches
VERIFY_ARE_EQUAL(static_cast<int>(arg.Type()), i);
// Verify the argument has basic properties set
// (Name should not be empty for valid argument types)
VERIFY_IS_FALSE(arg.Name().empty());
LogComment(L"Verified Argument::Create() creates argument with name: " + arg.Name());
// Add the argument to the ArgMap with a test value based on its type.
VERIFY_IS_FALSE(args.Contains(argType));
switch (arg.Kind())
{
case Kind::Value:
case Kind::Positional:
args.Add(argType, std::wstring(L"test"));
break;
case Kind::Forward:
args.Add(argType, std::vector<std::wstring>{L"forward1", L"forward2"});
break;
case Kind::Flag:
args.Add(argType, true);
break;
default:
VERIFY_FAIL(L"Unhandled ValueType in test");
}
allArgTypes.push_back(argType);
VERIFY_IS_TRUE(args.Contains(argType));
}
// We do not have a runtime Get for argument values, so we will instead use the keys
// in the argmap. The fact that the keys exist and can be used to retrieve values
// verifies that Argument::Create() created arguments that are compatible with ArgMap.
// Verify all created argument types are in the ArgMap keys
auto argMapKeys = args.GetKeys();
VERIFY_ARE_EQUAL(argMapKeys.size(), allArgTypes.size());
for (const auto& argType : allArgTypes)
{
VERIFY_IS_TRUE(std::find(argMapKeys.begin(), argMapKeys.end(), argType) != argMapKeys.end());
}
}
// Test: Verify EnumVariantMap behavior with ArgTypes.
TEST_METHOD(EnumVariantMap_AllDataTypes)
{
// ArgMap is an EnumVariantMap
ArgMap argsContainer;
// Verify basic add
argsContainer.Add<ArgType::Help>(true);
VERIFY_IS_TRUE(argsContainer.Contains(ArgType::Help));
argsContainer.Add<ArgType::ContainerId>(std::wstring(L"test"));
VERIFY_IS_TRUE(argsContainer.Contains(ArgType::ContainerId));
argsContainer.Add<ArgType::ForwardArgs>(std::vector<std::wstring>{L"test1", L"test2"});
VERIFY_IS_TRUE(argsContainer.Contains(ArgType::ForwardArgs));
// Verify basic retrieval
auto retrievedBool = argsContainer.Get<ArgType::Help>();
VERIFY_ARE_EQUAL(retrievedBool, true);
auto retrievedString = argsContainer.Get<ArgType::ContainerId>();
VERIFY_ARE_EQUAL(retrievedString, std::wstring(L"test"));
auto retrievedStringSet = argsContainer.Get<ArgType::ForwardArgs>();
VERIFY_ARE_EQUAL(retrievedStringSet[0], std::wstring(L"test1"));
VERIFY_ARE_EQUAL(retrievedStringSet[1], std::wstring(L"test2"));
// Verify multimap functionality and Runtime Add
argsContainer.Add(ArgType::Publish, std::wstring(L"test1"));
argsContainer.Add(ArgType::Publish, std::wstring(L"test2"));
argsContainer.Add(ArgType::Publish, std::wstring(L"test3"));
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
auto publishArgs = argsContainer.GetAll<ArgType::Publish>();
VERIFY_ARE_EQUAL(publishArgs.size(), 3);
VERIFY_ARE_EQUAL(publishArgs[0], std::wstring(L"test1"));
VERIFY_ARE_EQUAL(publishArgs[1], std::wstring(L"test2"));
VERIFY_ARE_EQUAL(publishArgs[2], std::wstring(L"test3"));
// Verify Remove
argsContainer.Remove(ArgType::Publish);
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 0);
// Verify compile time add works like runtime add for multimap types.
argsContainer.Add<ArgType::Publish>(L"test1");
argsContainer.Add<ArgType::Publish>(L"test2");
argsContainer.Add<ArgType::Publish>(L"test3");
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
publishArgs = argsContainer.GetAll<ArgType::Publish>();
VERIFY_ARE_EQUAL(publishArgs.size(), 3);
VERIFY_ARE_EQUAL(publishArgs[0], std::wstring(L"test1"));
VERIFY_ARE_EQUAL(publishArgs[1], std::wstring(L"test2"));
VERIFY_ARE_EQUAL(publishArgs[2], std::wstring(L"test3"));
// Verify Keys
auto allArgTypes = argsContainer.GetKeys();
VERIFY_ARE_EQUAL(allArgTypes.size(), 4);
VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::Help) != allArgTypes.end());
VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::ContainerId) != allArgTypes.end());
VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::Publish) != allArgTypes.end());
VERIFY_IS_TRUE(std::find(allArgTypes.begin(), allArgTypes.end(), ArgType::ForwardArgs) != allArgTypes.end());
// Verify count
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Help), 1);
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::ContainerId), 1);
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::Publish), 3);
VERIFY_ARE_EQUAL(argsContainer.Count(ArgType::ForwardArgs), 1);
VERIFY_ARE_EQUAL(argsContainer.GetCount(), 6); // 1 Help + 1 ContainerId + 3 Publish + 1 ForwardArgs
argsContainer.Remove(ArgType::Help);
argsContainer.Remove(ArgType::ContainerId);
argsContainer.Remove(ArgType::Publish);
argsContainer.Remove(ArgType::ForwardArgs);
VERIFY_ARE_EQUAL(argsContainer.GetCount(), 0);
}
};
} // namespace WSLCCLIArgumentUnitTests

View File

@@ -0,0 +1,82 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WSLCCLICommandUnitTests.cpp
Abstract:
This file contains unit tests for WSLC CLI Command classes.
--*/
#include "precomp.h"
#include "windows/Common.h"
#include "WSLCCLITestHelpers.h"
#include "Command.h"
#include "DiagCommand.h"
#include "RootCommand.h"
using namespace wsl::windows::wslc;
using namespace WSLCTestHelpers;
using namespace WEX::Logging;
using namespace WEX::Common;
using namespace WEX::TestExecution;
namespace WSLCCLICommandUnitTests {
class WSLCCLICommandUnitTests
{
WSL_TEST_CLASS(WSLCCLICommandUnitTests)
TEST_CLASS_SETUP(TestClassSetup)
{
Log::Comment(L"WSLC CLI Command Unit Tests - Class Setup");
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
Log::Comment(L"WSLC CLI Command Unit Tests - Class Cleanup");
return true;
}
// Test: Verify RootCommand has subcommands
TEST_METHOD(RootCommand_HasSubcommands)
{
auto cmd = RootCommand();
auto subcommands = cmd.GetCommands();
// Verify it has subcommands
VERIFY_IS_TRUE(subcommands.size() > 0);
LogComment(L"RootCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
// Verify each subcommand is valid
for (const auto& subcmd : subcommands)
{
VERIFY_IS_NOT_NULL(subcmd.get());
}
}
// Test: Verify ContainerCommand has subcommands
TEST_METHOD(DiagCommand_HasSubcommands)
{
auto cmd = DiagCommand(L"diag");
auto subcommands = cmd.GetCommands();
// Verify it has subcommands (create, list, run, etc.)
VERIFY_IS_TRUE(subcommands.size() > 0);
LogComment(L"DiagCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
// Log subcommand types
for (const auto& subcmd : subcommands)
{
VERIFY_IS_NOT_NULL(subcmd.get());
}
}
};
} // namespace WSLCCLICommandUnitTests

View File

@@ -0,0 +1,159 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WSLCCLIExecutionUnitTests.cpp
Abstract:
This file contains unit tests for WSLC CLI command execution.
--*/
#include "precomp.h"
#include "windows/Common.h"
#include "WSLCCLITestHelpers.h"
#include "Command.h"
#include "RootCommand.h"
using namespace wsl::windows::wslc;
using namespace WSLCTestHelpers;
using namespace WEX::Logging;
using namespace WEX::Common;
using namespace WEX::TestExecution;
namespace WSLCCLIExecutionUnitTests {
// Helper structure to hold test data
struct CommandLineTestCase
{
std::wstring commandLine;
std::wstring expectedCommand;
bool shouldSucceed;
};
class WSLCCLIExecutionUnitTests
{
WSL_TEST_CLASS(WSLCCLIExecutionUnitTests)
TEST_CLASS_SETUP(TestClassSetup)
{
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
return true;
}
// Test: Verify EnumVariantMap on DataMap for Context Data
TEST_METHOD(EnumVariantMap_DataMapValidation)
{
// DataMap is an EnumVariantMap, but for command execution context data instead of arguments.
// It does not have rigid typing like the Args map, so this will verify every Data enum value
// can be added and retrieved successfully. The arguments unit tests have more complex tests
// for the EnumVariantMap behavior. This one ensures Data enum values are correct.
wsl::windows::wslc::execution::DataMap dataMap;
// Verify all data enum values defined.
auto allDataTypes = std::vector<Data>{};
for (int i = 0; i < static_cast<int>(Data::Max); ++i)
{
Data dataType = static_cast<Data>(i);
// Add the data to the DataMap with a test value based on its type.
// Each data type needs to be added here as each enum may have its own value.
VERIFY_IS_FALSE(dataMap.Contains(dataType));
switch (dataType)
{
case Data::SessionId:
dataMap.Add(dataType, std::wstring(L"Session1234"));
break;
default:
VERIFY_FAIL(L"Unhandled Data type in test");
}
allDataTypes.push_back(dataType);
VERIFY_IS_TRUE(dataMap.Contains(dataType));
}
// Verify basic retrieval.
auto sessionId = dataMap.Get<Data::SessionId>();
VERIFY_ARE_EQUAL(L"Session1234", sessionId);
// 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.
}
// 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.
// It does not actually verify the execution of the command, just that the correct command is
// found and the provided command line parsed correctly according to the command's defined arguments,
// and the argument validation rules are correctly applied. The test cases are defined in
// CommandLineTestCases.h and cover various valid and invalid command lines.
TEST_METHOD(CommandLineParsing_AllCases)
{
std::vector<CommandLineTestCase> testCases = {
#define COMMAND_LINE_TEST_CASE(cmdLine, expectedCmd, shouldPass) {cmdLine, expectedCmd, shouldPass},
#include "CommandLineTestCases.h"
#undef COMMAND_LINE_TEST_CASE
};
// Run all test cases
for (const auto& testCase : testCases)
{
LogComment(L"Testing: " + testCase.commandLine);
// Pre-pend executable name, which will get stripped off by CommandLineToArgvW
auto fullCommandLine = L"wslc " + testCase.commandLine;
// Process the command line as Windows does.
int argc = 0;
auto argv = CommandLineToArgvW(fullCommandLine.c_str(), &argc);
std::vector<std::wstring> args;
for (int i = 1; i < argc; ++i)
{
args.emplace_back(argv[i]);
}
// And now process the command line like WSLC does.
bool succeeded = true;
try
{
Invocation invocation{std::move(args)};
std::unique_ptr<Command> command = std::make_unique<RootCommand>();
std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
while (subCommand)
{
command = std::move(subCommand);
subCommand = command->FindSubCommand(invocation);
}
// Ensure we found the expected command
VERIFY_ARE_EQUAL(testCase.expectedCommand, command->Name());
CLIExecutionContext context;
// Parse and validate and compare to expected results.
command->ParseArguments(invocation, context.Args);
command->ValidateArguments(context.Args);
}
catch (const CommandException& ce)
{
LogComment(L"Command line parsing threw an exception: " + ce.Message());
succeeded = false;
}
catch (...)
{
LogComment(L"Command line parsing threw an unexpected exception.");
succeeded = false;
}
VERIFY_ARE_EQUAL(testCase.shouldSucceed, succeeded);
}
}
};
} // namespace WSLCCLIExecutionUnitTests

View File

@@ -0,0 +1,148 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WSLCCLIParserUnitTests.cpp
Abstract:
This file contains unit tests for WSLC CLI argument parsing and validation.
--*/
#include "precomp.h"
#include "windows/Common.h"
#include "WSLCCLITestHelpers.h"
#include "Argument.h"
#include "ArgumentTypes.h"
#include "ArgumentParser.h"
#include "Invocation.h"
#include "ParserTestCases.h"
using namespace wsl::windows::wslc;
using namespace wsl::windows::wslc::argument;
using namespace WSLCTestHelpers;
using namespace WEX::Logging;
using namespace WEX::Common;
using namespace WEX::TestExecution;
namespace WSLCCLIParserUnitTests {
class WSLCCLIParserUnitTests
{
WSL_TEST_CLASS(WSLCCLIParserUnitTests)
TEST_CLASS_SETUP(TestClassSetup)
{
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
return true;
}
// Test: Verify command line to argv mapping and GetRemainingRawCommandLineFromIndex
TEST_METHOD(ParserTest_StateMachine_PositionalForward)
{
// Build test cases from x-macro
std::vector<ParserTestCase> testCases = {
#define WSLC_PARSER_TEST_CASE(argSetValue, expected, cmdLine) {ArgumentSet::argSetValue, expected, cmdLine},
WSLC_PARSER_TEST_CASES
#undef WSLC_PARSER_TEST_CASE
};
for (const auto& testCase : testCases)
{
try
{
Log::Comment(String().Format(L"Testing: %ls", testCase.commandLine.c_str()));
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(testCase.commandLine);
// Get argument definitions from the helper function
std::vector<Argument> definedArgs = GetArgumentsForSet(testCase.argumentSet);
ArgMap args;
ParseArgumentsStateMachine stateMachine{inv, args, std::move(definedArgs)};
while (stateMachine.Step())
{
stateMachine.ThrowIfError();
}
if (testCase.commandLine.find(L"cont1") != std::wstring::npos)
{
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)
{
// Ensure 'rm' was parsed wherever it was found.
VERIFY_IS_TRUE(args.Contains(ArgType::Remove));
}
if (testCase.commandLine.find(L"command") != std::wstring::npos)
{
VERIFY_IS_TRUE(args.Contains(ArgType::Command));
auto command = args.Get<ArgType::Command>();
VERIFY_IS_TRUE(command.find(L"command") != std::wstring::npos);
}
if (testCase.commandLine.find(L"forward") != std::wstring::npos)
{
VERIFY_IS_TRUE(args.Contains(ArgType::ForwardArgs));
auto forwardArgs = args.Get<ArgType::ForwardArgs>();
std::wstring forwardArgsConcat;
for (const auto& arg : forwardArgs)
{
if (!forwardArgsConcat.empty())
{
forwardArgsConcat += L" ";
}
forwardArgsConcat += arg;
}
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"command") == std::wstring::npos); // Forward args should not contain the command
LogComment(L"Forwarded Args: " + forwardArgsConcat);
}
if (testCase.commandLine.find(L"443") != std::wstring::npos)
{
VERIFY_IS_TRUE(args.Contains(ArgType::Publish));
auto publishArgs = args.GetAll<ArgType::Publish>();
VERIFY_ARE_EQUAL(2, publishArgs.size()); // Should have both publish args
VERIFY_ARE_NOT_EQUAL(publishArgs[0], publishArgs[1]); // Both publish args should be different
}
}
catch (ArgumentException& ex)
{
if (testCase.expectedResult)
{
VERIFY_FAIL(String().Format(L"Test case threw unexpected argument exception: %ls", ex.Message().c_str()));
}
else
{
Log::Comment(String().Format(L"Test case threw expected argument exception: %ls", ex.Message().c_str()));
}
}
catch (std::exception& ex)
{
if (testCase.expectedResult)
{
VERIFY_FAIL(String().Format(L"Test case threw unexpected exception: %hs", ex.what()));
}
else
{
Log::Comment(String().Format(L"Test case threw expected exception: %hs", ex.what()));
}
}
}
}
};
} // namespace WSLCCLIParserUnitTests

View File

@@ -0,0 +1,63 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WSLCTestHelpers.h
Abstract:
Helper utilities for WSLC CLI unit tests.
--*/
#pragma once
#include <string>
#include <Windows.h>
#include <WexTestClass.h>
#include "Invocation.h"
namespace WSLCTestHelpers {
inline wsl::windows::wslc::Invocation CreateInvocationFromCommandLine(const std::wstring& commandLine)
{
// Simulate creation of Arvc/Argc from command line as Windows does.
int argc = 0;
wil::unique_hlocal_ptr<LPWSTR[]> argv;
argv.reset(CommandLineToArgvW(commandLine.c_str(), &argc));
VERIFY_IS_NOT_NULL(argv.get());
VERIFY_IS_GREATER_THAN(argc, 0);
// Convert to vector for Invocation, skipping argv[0] (executable path)
// This is what we do in wmain() to populate Invocation input vector.
std::vector<std::wstring> args;
for (int i = 1; i < argc; ++i) // Skip argv[0]
{
args.push_back(argv[i]);
}
return wsl::windows::wslc::Invocation(std::move(args));
}
// Helper function to convert wstring to UTF-8 string for TAEF logging
inline std::string WStringToUTF8(const std::wstring& wstr)
{
if (wstr.empty())
{
return std::string();
}
int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), &result[0], size_needed, nullptr, nullptr);
return result;
}
// Convenience wrapper for Log::Comment with wstring
inline void LogComment(const std::wstring& message)
{
WEX::Logging::Log::Comment(reinterpret_cast<const char8_t*>(WStringToUTF8(message).c_str()));
}
} // namespace WSLCTestHelpers