Skip to content

Commit 5051e17

Browse files
committed
feat(cli): add shell completion command (bash)
2 parents 63c93a7 + be79513 commit 5051e17

4 files changed

Lines changed: 228 additions & 4 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
*
3+
* @file CompletionCommand.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
*/
14+
#ifndef VIX_COMPLETION_COMMAND_HPP
15+
#define VIX_COMPLETION_COMMAND_HPP
16+
17+
#include <vector>
18+
#include <string>
19+
20+
namespace vix::commands
21+
{
22+
struct CompletionCommand
23+
{
24+
static int run(const std::vector<std::string> &args);
25+
static int help();
26+
};
27+
}
28+
29+
#endif

src/CLI.cpp

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include <vix/cli/commands/UpdateCommand.hpp>
4343
#include <vix/cli/commands/OutdatedCommand.hpp>
4444
#include <vix/cli/commands/MakeCommand.hpp>
45+
#include <vix/cli/commands/CompletionCommand.hpp>
4546
#include <vix/utils/Env.hpp>
4647
#include <vix/cli/Style.hpp>
4748
#include <vix/utils/Logger.hpp>
@@ -56,6 +57,10 @@
5657
#include <algorithm>
5758
#include <cctype>
5859
#include <filesystem>
60+
#include <functional>
61+
#include <climits>
62+
#include <functional>
63+
#include <climits>
5964

6065
namespace vix
6166
{
@@ -92,6 +97,57 @@ namespace vix
9297
return std::nullopt;
9398
}
9499

100+
static int levenshtein_distance(const std::string &a, const std::string &b)
101+
{
102+
const size_t m = a.size();
103+
const size_t n = b.size();
104+
105+
std::vector<int> prev(n + 1), curr(n + 1);
106+
107+
for (size_t j = 0; j <= n; ++j)
108+
prev[j] = j;
109+
110+
for (size_t i = 1; i <= m; ++i)
111+
{
112+
curr[0] = i;
113+
for (size_t j = 1; j <= n; ++j)
114+
{
115+
int cost = (a[i - 1] == b[j - 1]) ? 0 : 1;
116+
117+
curr[j] = std::min({prev[j] + 1,
118+
curr[j - 1] + 1,
119+
prev[j - 1] + cost});
120+
}
121+
prev = curr;
122+
}
123+
124+
return prev[n];
125+
}
126+
127+
static std::optional<std::string> find_closest_command(
128+
const std::string &input,
129+
const std::unordered_map<std::string, vix::cli::dispatch::Entry> &entries)
130+
{
131+
int bestScore = INT_MAX;
132+
std::string best;
133+
134+
for (const auto &[name, _] : entries)
135+
{
136+
int d = levenshtein_distance(input, name);
137+
138+
if (d < bestScore)
139+
{
140+
bestScore = d;
141+
best = name;
142+
}
143+
}
144+
145+
if (bestScore <= 3)
146+
return best;
147+
148+
return std::nullopt;
149+
}
150+
95151
void apply_log_level_from_env(Logger &logger)
96152
{
97153
if (const char *env = vix::utils::vix_getenv("VIX_LOG_LEVEL"))
@@ -290,8 +346,20 @@ namespace vix
290346
{
291347
if (!dispatcher.has(cmd))
292348
{
293-
std::cerr << "vix: unknown command '" << cmd << "'\n\n";
294-
return help({});
349+
vix::cli::util::err_line(
350+
std::cerr,
351+
"unrecognized subcommand " + vix::cli::util::quote(cmd));
352+
353+
auto suggestion = find_closest_command(cmd, dispatcher.entries());
354+
355+
if (suggestion.has_value())
356+
{
357+
vix::cli::util::tip_line(
358+
std::cerr,
359+
"A similar command exists: " + vix::cli::util::quote(suggestion.value()));
360+
}
361+
362+
return 1;
295363
}
296364
return dispatcher.help(cmd);
297365
}
@@ -307,8 +375,19 @@ namespace vix
307375

308376
if (!dispatcher.has(cmd))
309377
{
310-
std::cerr << "vix: unknown command '" << cmd << "'\n\n";
311-
help({});
378+
vix::cli::util::err_line(
379+
std::cerr,
380+
"unrecognized subcommand " + vix::cli::util::quote(cmd));
381+
382+
auto suggestion = find_closest_command(cmd, dispatcher.entries());
383+
384+
if (suggestion.has_value())
385+
{
386+
vix::cli::util::tip_line(
387+
std::cerr,
388+
"A similar command exists: " + vix::cli::util::quote(suggestion.value()));
389+
}
390+
312391
return 1;
313392
}
314393

@@ -372,6 +451,8 @@ namespace vix
372451
return commands::StoreCommand::help();
373452
if (cmd == "publish")
374453
return commands::PublishCommand::help();
454+
if (cmd == "completion")
455+
return commands::CompletionCommand::help();
375456
if (cmd == "deps")
376457
{
377458
vix::cli::util::warn_line(std::cout, "'vix deps' is deprecated, use 'vix install'");
@@ -480,6 +561,7 @@ namespace vix
480561
// Help
481562
out << indent(2) << "Help:\n";
482563
out << indent(3) << "help [command] Show command help\n";
564+
out << indent(3) << "completion Generate shell completion script\n";
483565
out << indent(3) << "version Show version\n\n";
484566

485567
out << indent(1) << "Global options:\n";

src/commands/CompletionCommand.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
*
3+
* @file CompletionCommand.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
*/
14+
#include <vix/cli/commands/CompletionCommand.hpp>
15+
#include <vix/cli/commands/Dispatch.hpp>
16+
17+
#include <iostream>
18+
#include <vector>
19+
#include <string>
20+
#include <algorithm>
21+
22+
namespace vix::commands
23+
{
24+
namespace
25+
{
26+
std::vector<std::string> collect_commands()
27+
{
28+
std::vector<std::string> names;
29+
30+
const auto &entries = vix::cli::dispatch::global().entries();
31+
names.reserve(entries.size());
32+
33+
for (const auto &[name, _] : entries)
34+
{
35+
names.push_back(name);
36+
}
37+
38+
std::sort(names.begin(), names.end());
39+
return names;
40+
}
41+
42+
int print_bash()
43+
{
44+
const auto commands = collect_commands();
45+
46+
std::cout << "_vix_completions()\n";
47+
std::cout << "{\n";
48+
std::cout << " local cur prev\n";
49+
std::cout << " cur=\"${COMP_WORDS[COMP_CWORD]}\"\n";
50+
std::cout << " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n\n";
51+
52+
std::cout << " local commands=\"";
53+
for (size_t i = 0; i < commands.size(); ++i)
54+
{
55+
if (i != 0)
56+
std::cout << " ";
57+
std::cout << commands[i];
58+
}
59+
std::cout << "\"\n\n";
60+
61+
std::cout << " if [[ ${COMP_CWORD} -eq 1 ]]; then\n";
62+
std::cout << " COMPREPLY=( $(compgen -W \"$commands\" -- \"$cur\") )\n";
63+
std::cout << " return 0\n";
64+
std::cout << " fi\n\n";
65+
66+
std::cout << " case \"${COMP_WORDS[1]}\" in\n";
67+
std::cout << " help)\n";
68+
std::cout << " COMPREPLY=( $(compgen -W \"$commands\" -- \"$cur\") )\n";
69+
std::cout << " return 0\n";
70+
std::cout << " ;;\n";
71+
std::cout << " esac\n\n";
72+
73+
std::cout << " COMPREPLY=()\n";
74+
std::cout << "}\n\n";
75+
76+
std::cout << "complete -o default -F _vix_completions vix\n";
77+
return 0;
78+
}
79+
}
80+
81+
int CompletionCommand::run(const std::vector<std::string> &args)
82+
{
83+
if (args.empty() || args[0] == "bash")
84+
return print_bash();
85+
86+
if (args[0] == "--help" || args[0] == "-h")
87+
return help();
88+
89+
std::cerr << "vix completion: unsupported shell '" << args[0] << "'\n";
90+
std::cerr << "Supported: bash\n";
91+
return 1;
92+
}
93+
94+
int CompletionCommand::help()
95+
{
96+
std::cout
97+
<< "vix completion [bash]\n\n"
98+
<< "Generate shell completion script.\n\n"
99+
<< "Examples:\n"
100+
<< " vix completion bash > ~/.vix-completion.bash\n"
101+
<< " source ~/.vix-completion.bash\n";
102+
return 0;
103+
}
104+
}

src/commands/Dispatch.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include <vix/cli/commands/OutdatedCommand.hpp>
4242
#include <vix/cli/commands/MakeCommand.hpp>
4343
#include <vix/cli/util/Ui.hpp>
44+
#include <vix/cli/commands/CompletionCommand.hpp>
4445

4546
#include <stdexcept>
4647

@@ -62,6 +63,14 @@ namespace vix::cli::dispatch
6263
[]()
6364
{ return vix::commands::NewCommand::help(); }});
6465

66+
add({"completion",
67+
"Info",
68+
"Generate shell completion script",
69+
[](const Args &a)
70+
{ return vix::commands::CompletionCommand::run(a); },
71+
[]()
72+
{ return vix::commands::CompletionCommand::help(); }});
73+
6574
add({"build",
6675
"Project",
6776
"Configure + build project",

0 commit comments

Comments
 (0)