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