CLI: Align alias listing with docker, Apple and other CLIs (#41439)
David Bennett committed
Aug 28, 2026 at 11:17 UTC
4bfbacae1338c37443f8b5e8d10a69b216b5e8ab
2 files changed
+144
-9
src/windows/wslc/core/Command.cpp
+120
-9
@@ -19,6 +19,7 @@ Abstract:
19
#include "TableOutput.h"
20
21
#include <algorithm>
22
+#include <typeinfo>
23
24
using namespace wsl::shared;
25
using namespace wsl::windows::common::wslutil;
@@ -29,6 +30,112 @@ namespace wsl::windows::wslc {
30
31
std::wstring s_ExecutableName = L"wslc";
32
33
+namespace {
34
+ std::vector<std::wstring> WrapAliases(std::span<const std::wstring> aliases, std::optional<size_t> consoleWidth, size_t indent)
35
+ {
36
+ std::vector<std::wstring> lines;
37
+ std::wstring line(indent, L' ');
38
+
39
+ for (size_t i = 0; i < aliases.size(); ++i)
40
+ {
41
+ std::wstring token = aliases[i];
42
+ if (i + 1 < aliases.size())
43
+ {
44
+ token += L',';
45
+ }
46
+
47
+ const bool hasAlias = line.size() > indent;
48
+ const size_t requiredWidth = token.size() + (hasAlias ? 1 : 0);
49
+ if (hasAlias && consoleWidth.has_value() && line.size() + requiredWidth > *consoleWidth)
50
+ {
51
+ lines.emplace_back(std::move(line));
52
+ line.assign(indent, L' ');
53
+ }
54
+ else if (hasAlias)
55
+ {
56
+ line += L' ';
57
+ }
58
+
59
+ line += token;
60
+ }
61
+
62
+ if (line.size() > indent)
63
+ {
64
+ lines.emplace_back(std::move(line));
65
+ }
66
+
67
+ return lines;
68
+ }
69
+
70
+ std::wstring FormatCommandInvocation(const Command& command, std::wstring_view name)
71
+ {
72
+ std::wstring commandChain = command.FullName();
73
+ const auto firstSplit = commandChain.find_first_of(Command::ParentSplitChar);
74
+ if (firstSplit == std::wstring::npos)
75
+ {
76
+ return s_ExecutableName;
77
+ }
78
+
79
+ commandChain = commandChain.substr(firstSplit + 1);
80
+ const auto lastSplit = commandChain.find_last_of(Command::ParentSplitChar);
81
+ commandChain.replace(lastSplit == std::wstring::npos ? 0 : lastSplit + 1, std::wstring::npos, name);
82
+ std::ranges::replace(commandChain, Command::ParentSplitChar, L' ');
83
+
84
+ std::wstring invocation = s_ExecutableName;
85
+ invocation += L' ';
86
+ invocation += commandChain;
87
+ return invocation;
88
+ }
89
+
90
+ void AddCommandInvocations(const Command& command, std::vector<std::wstring>& invocations)
91
+ {
92
+ const auto addInvocation = [&](std::wstring_view name) {
93
+ auto invocation = FormatCommandInvocation(command, name);
94
+ if (std::ranges::find(invocations, invocation) == invocations.end())
95
+ {
96
+ invocations.emplace_back(std::move(invocation));
97
+ }
98
+ };
99
+
100
+ addInvocation(command.Name());
101
+ for (const auto alias : command.Aliases())
102
+ {
103
+ addInvocation(alias);
104
+ }
105
+ }
106
+
107
+ void FindCommandInvocations(const Command& target, const Command& parent, std::vector<std::wstring>& invocations)
108
+ {
109
+ for (const auto& command : parent.GetCommands())
110
+ {
111
+ if (typeid(target) == typeid(*command))
112
+ {
113
+ AddCommandInvocations(*command, invocations);
114
+ }
115
+
116
+ FindCommandInvocations(target, *command, invocations);
117
+ }
118
+ }
119
+
120
+ std::vector<std::wstring> GetCommandInvocations(const Command& command)
121
+ {
122
+ std::vector<std::wstring> invocations;
123
+ FindCommandInvocations(command, RootCommand(), invocations);
124
+
125
+ if (invocations.empty())
126
+ {
127
+ AddCommandInvocations(command, invocations);
128
+ }
129
+
130
+ if (invocations.size() == 1)
131
+ {
132
+ invocations.clear();
133
+ }
134
+
135
+ return invocations;
136
+ }
137
+} // namespace
138
+
139
Command::Command(std::wstring_view name, std::vector<std::wstring_view>&& aliases, const std::wstring& parent) :
140
m_name(name), m_aliases(std::move(aliases))
141
{
@@ -96,7 +203,11 @@ void Command::OutputHelp(Terminal& terminal, HelpOutput output, const CommandExc
203
}
204
}
205
99
- auto commandAliases = Aliases();
206
+ std::vector<std::wstring> commandAliases;
207
+ if (fullHelp)
208
+ {
209
+ commandAliases = GetCommandInvocations(*this);
210
+ }
211
auto commands = GetCommands();
212
auto arguments = GetAllArguments();
213
std::vector<Argument> helpArguments;
@@ -253,17 +364,17 @@ void Command::OutputHelp(Terminal& terminal, HelpOutput output, const CommandExc
364
{
365
terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingAliases(), Format::Default);
366
256
- std::wstring aliasLine;
257
- for (size_t i = 0; i < commandAliases.size(); ++i)
367
+ std::optional<size_t> consoleWidth;
368
+ if (const auto width = terminal.GetConsoleWidth(helpLevel); width.has_value() && *width > 0)
369
{
259
- if (i != 0)
260
- {
261
- aliasLine += L", ";
262
- }
263
- aliasLine += commandAliases[i];
370
+ consoleWidth = static_cast<size_t>(*width);
371
}
372
266
- terminal.Write(helpLevel, L"{}{}\n\n", std::wstring(c_helpRowIndent, L' '), aliasLine);
373
+ for (const auto& line : WrapAliases(commandAliases, consoleWidth, c_helpRowIndent))
374
+ {
375
+ terminal.Write(helpLevel, L"{}\n", line);
376
+ }
377
+ terminal.Write(helpLevel, L"\n");
378
}
379
380
// Col0: name/command
test/windows/wslc/e2e/WSLCE2EAliasTests.cpp
+24
@@ -75,6 +75,30 @@ class WSLCE2EAliasTests
75
76
VERIFY_ARE_EQUAL(wslcOutput, containerResult.Stdout.value());
77
}
78
+
79
+ WSLC_TEST_METHOD(WSLCE2E_CommandHelp_ListsFullInvocationAliases)
80
+ {
81
+ const std::wstring startAliases = L"Aliases:\r\n wslc container start, wslc start\r\n";
82
+
83
+ for (const auto commandLine : {L"container start --help", L"start --help"})
84
+ {
85
+ const auto result = RunWslc(commandLine);
86
+ result.Verify({.Stderr = L"", .ExitCode = 0});
87
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(startAliases));
88
+ }
89
+
90
+ const auto containerResult = RunContainerExe(L"start --help");
91
+ containerResult.Verify({.Stderr = L"", .ExitCode = 0});
92
+ VERIFY_IS_TRUE(containerResult.StdoutContainsSubstring(L"Aliases:\r\n container container start, container start\r\n"));
93
+
94
+ const auto imageListResult = RunWslc(L"image list --help");
95
+ imageListResult.Verify({.Stderr = L"", .ExitCode = 0});
96
+ VERIFY_IS_TRUE(imageListResult.StdoutContainsSubstring(L"Aliases:\r\n wslc image list, wslc image ls, wslc images\r\n"));
97
+
98
+ const auto volumeListResult = RunWslc(L"volume list --help");
99
+ volumeListResult.Verify({.Stderr = L"", .ExitCode = 0});
100
+ VERIFY_IS_TRUE(volumeListResult.StdoutContainsSubstring(L"Aliases:\r\n wslc volume list, wslc volume ls\r\n"));
101
+ }
102
};
103
104
} // namespace WSLCE2ETests