mirror of
https://github.com/microsoft/WSL.git
synced 2026-04-30 16:18:57 -05:00
Add base CLI implementation (#14216)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -123,4 +123,4 @@ int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ClientRunningWS
|
||||
|
||||
return process.Wait();
|
||||
}
|
||||
} // namespace wsl::windows::wslc::services
|
||||
} // namespace wsl::windows::wslc::services
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
src/windows/wslc/README.md
Normal file
2
src/windows/wslc/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
### WSL Container CLI
|
||||
This is the WSL Container CLI README
|
||||
@@ -34,4 +34,4 @@ const WSLA_SESSION_SETTINGS* SessionOptions::Get() const
|
||||
{
|
||||
return &m_sessionSettings;
|
||||
}
|
||||
} // namespace wsl::windows::wslc::models
|
||||
} // namespace wsl::windows::wslc::models
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -126,4 +126,4 @@ std::vector<SessionInformation> ShellService::List()
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace wsl::windows::wslc::services
|
||||
} // namespace wsl::windows::wslc::services
|
||||
|
||||
@@ -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
|
||||
|
||||
75
src/windows/wslc/arguments/Argument.cpp
Normal file
75
src/windows/wslc/arguments/Argument.cpp
Normal 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
|
||||
105
src/windows/wslc/arguments/Argument.h
Normal file
105
src/windows/wslc/arguments/Argument.h
Normal 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
|
||||
45
src/windows/wslc/arguments/ArgumentDefinitions.h
Normal file
45
src/windows/wslc/arguments/ArgumentDefinitions.h
Normal 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
|
||||
409
src/windows/wslc/arguments/ArgumentParser.cpp
Normal file
409
src/windows/wslc/arguments/ArgumentParser.cpp
Normal 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
|
||||
122
src/windows/wslc/arguments/ArgumentParser.h
Normal file
122
src/windows/wslc/arguments/ArgumentParser.h
Normal 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
|
||||
103
src/windows/wslc/arguments/ArgumentTypes.h
Normal file
103
src/windows/wslc/arguments/ArgumentTypes.h
Normal 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
|
||||
49
src/windows/wslc/commands/DiagCommand.cpp
Normal file
49
src/windows/wslc/commands/DiagCommand.cpp
Normal 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
|
||||
49
src/windows/wslc/commands/DiagCommand.h
Normal file
49
src/windows/wslc/commands/DiagCommand.h
Normal 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
|
||||
47
src/windows/wslc/commands/DiagListCommand.cpp
Normal file
47
src/windows/wslc/commands/DiagListCommand.cpp
Normal 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
|
||||
56
src/windows/wslc/commands/RootCommand.cpp
Normal file
56
src/windows/wslc/commands/RootCommand.cpp
Normal 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
|
||||
34
src/windows/wslc/commands/RootCommand.h
Normal file
34
src/windows/wslc/commands/RootCommand.h
Normal 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
|
||||
39
src/windows/wslc/core/CLIExecutionContext.h
Normal file
39
src/windows/wslc/core/CLIExecutionContext.h
Normal 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
|
||||
360
src/windows/wslc/core/Command.cpp
Normal file
360
src/windows/wslc/core/Command.cpp
Normal 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
|
||||
93
src/windows/wslc/core/Command.h
Normal file
93
src/windows/wslc/core/Command.h
Normal 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
|
||||
339
src/windows/wslc/core/EnumVariantMap.h
Normal file
339
src/windows/wslc/core/EnumVariantMap.h
Normal 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
|
||||
40
src/windows/wslc/core/Exceptions.h
Normal file
40
src/windows/wslc/core/Exceptions.h
Normal 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
|
||||
49
src/windows/wslc/core/ExecutionContextData.h
Normal file
49
src/windows/wslc/core/ExecutionContextData.h
Normal 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
|
||||
100
src/windows/wslc/core/Invocation.h
Normal file
100
src/windows/wslc/core/Invocation.h
Normal 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
|
||||
116
src/windows/wslc/core/Main.cpp
Normal file
116
src/windows/wslc/core/Main.cpp
Normal 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);
|
||||
}
|
||||
@@ -143,4 +143,4 @@ int wmain(int, wchar_t**)
|
||||
}
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
105
src/windows/wslc/tasks/DiagTasks.cpp
Normal file
105
src/windows/wslc/tasks/DiagTasks.cpp
Normal 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
|
||||
21
src/windows/wslc/tasks/DiagTasks.h
Normal file
21
src/windows/wslc/tasks/DiagTasks.h
Normal 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
|
||||
56
src/windows/wslc/tasks/Task.h
Normal file
56
src/windows/wslc/tasks/Task.h
Normal 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
|
||||
@@ -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})
|
||||
34
test/windows/wslc/CMakeLists.txt
Normal file
34
test/windows/wslc/CMakeLists.txt
Normal 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
|
||||
)
|
||||
34
test/windows/wslc/CommandLineTestCases.h
Normal file
34
test/windows/wslc/CommandLineTestCases.h
Normal 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)
|
||||
129
test/windows/wslc/ParserTestCases.h
Normal file
129
test/windows/wslc/ParserTestCases.h
Normal 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
|
||||
174
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
Normal file
174
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
Normal 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
|
||||
82
test/windows/wslc/WSLCCLICommandUnitTests.cpp
Normal file
82
test/windows/wslc/WSLCCLICommandUnitTests.cpp
Normal 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
|
||||
159
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
Normal file
159
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
Normal 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
|
||||
148
test/windows/wslc/WSLCCLIParserUnitTests.cpp
Normal file
148
test/windows/wslc/WSLCCLIParserUnitTests.cpp
Normal 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
|
||||
63
test/windows/wslc/WSLCCLITestHelpers.h
Normal file
63
test/windows/wslc/WSLCCLITestHelpers.h
Normal 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
|
||||
Reference in New Issue
Block a user