Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bash completion #58

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ subprojects/CLI11-*
subprojects/curl-*
subprojects/fmt-*
subprojects/packagecache
subprojects/range-v3-*
subprojects/spdlog-*
subprojects/sqlite-amalgamation-*
subprojects/nlohmann_json-*
Expand Down
6 changes: 4 additions & 2 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ catch_dep = subproject('catch2', default_options: ['werror=false', 'warning_leve
cli11_dep = subproject('cli11', default_options: ['werror=false', 'warning_level=0']).get_variable('CLI11_dep')
fmt_dep = subproject('fmt', default_options: ['werror=false', 'warning_level=0']).get_variable('fmt_dep')
json_dep = subproject('nlohmann_json', default_options: ['werror=false', 'warning_level=0']).get_variable('nlohmann_json_dep')
range_dep = subproject('range-v3', default_options: ['werror=false', 'warning_level=0']).get_variable('range_dep')
spdlog_dep = subproject('spdlog', default_options: ['werror=false', 'warning_level=0','std_format=disabled','external_fmt=enabled']).get_variable('spdlog_dep')
sqlite3_dep = subproject('sqlite3', default_options: ['werror=false', 'warning_level=0']).get_variable('sqlite3_dep')
subproject('curl', default_options: ['werror=false', 'warning_level=0', 'tests=disabled', 'unittests=disabled', 'tool=disabled'])
Expand Down Expand Up @@ -62,7 +63,7 @@ lib_inc = include_directories('src')

lib_dep = declare_dependency(
sources: lib_src,
dependencies: [curl_dep, sqlite3_dep, fmt_dep, spdlog_dep, json_dep, barkeep_dep],
dependencies: [curl_dep, sqlite3_dep, fmt_dep, range_dep, spdlog_dep, json_dep, barkeep_dep],
include_directories: lib_inc
)

Expand All @@ -86,12 +87,13 @@ if uenv_cli
'src/cli/status.cpp',
'src/cli/uenv.cpp',
'src/cli/build.cpp',
'src/util/completion.cpp',
]
uenv_dep = [sqlite3_dep]

cli = executable('uenv',
sources: uenv_src,
dependencies: [uenv_dep, lib_dep, fmt_dep, spdlog_dep, cli11_dep, barkeep_dep],
dependencies: [uenv_dep, lib_dep, fmt_dep, range_dep, spdlog_dep, cli11_dep, barkeep_dep],
c_args: [
'-DVERSION="@0@"'.format(version),
],
Expand Down
11 changes: 11 additions & 0 deletions src/cli/uenv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <uenv/parse.h>
#include <uenv/repository.h>
#include <util/color.h>
#include <util/completion.h>
#include <util/expected.h>
#include <util/fs.h>

Expand All @@ -30,6 +31,7 @@ std::string help_footer();
int main(int argc, char** argv) {
uenv::global_settings settings;
bool print_version = false;
bool generate_completion = false;

// enable/disable color depending on NOCOLOR env. var and tty terminal
// status.
Expand All @@ -45,6 +47,8 @@ int main(int argc, char** argv) {
"enable color output");
cli.add_flag("--repo", settings.repo_, "the uenv repository");
cli.add_flag("--version", print_version, "print version");
cli.add_flag("--generate-completion", generate_completion,
"generate bash completion script");

cli.footer(help_footer);

Expand Down Expand Up @@ -92,6 +96,13 @@ int main(int argc, char** argv) {
return 0;
}

// generate bash completion script and exit if the --generate-completion
// flag was passed
if (generate_completion) {
fmt::print("{}", util::completion::create_completion(&cli));
return 0;
}

// if a repo was not provided as a flag, look at environment variables
if (!settings.repo_) {
if (const auto p = uenv::default_repo_path()) {
Expand Down
96 changes: 96 additions & 0 deletions src/util/completion.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#include <CLI/CLI.hpp>
#include <fmt/core.h>
#include <fmt/ranges.h>
#include <range/v3/all.hpp>
#include <ranges>

#include <util/completion.h>

namespace util {

namespace completion {

std::string get_prefix(CLI::App* cli) {
if (cli == nullptr)
return "";

CLI::App* parent = cli->get_parent();
return get_prefix(parent) + "_" + cli->get_name();
}

std::string traverse_subcommand_tree(CLI::App* cli) {
const auto subcommands = cli->get_subcommands({});
const auto options_non_positional = cli->get_options(
[](CLI::Option* option) { return option->nonpositional(); });

auto get_option_name = [](CLI::Option* option) {
return option->get_name();
};
auto get_subcommand_name = [](CLI::App* subcommand) {
return subcommand->get_name();
};

auto tmp1 = std::views::transform(options_non_positional, get_option_name);
auto tmp2 = std::views::transform(subcommands, get_subcommand_name);
auto completions = ranges::view::concat(tmp1, tmp2);

std::vector<std::string> func_command_str = {};
func_command_str.push_back(fmt::format(R"({}()
{{
UENV_OPTS="{}"
}}

)",
get_prefix(cli),
fmt::join(completions, " ")));

auto func_subcommands_str =
std::views::transform(subcommands, traverse_subcommand_tree);

return fmt::format("{}{}", fmt::join(func_command_str, ""),
fmt::join(func_subcommands_str, ""));
}

std::string create_completion(CLI::App* cli) {
std::string prefix_functions = traverse_subcommand_tree(cli);

std::string main_functions = R"(
_uenv_completions()
{
local cur prefix func_name UENV_OPTS

local -a COMP_WORDS_NO_FLAGS
local index=0
while [[ "$index" -lt "$COMP_CWORD" ]]
do
if [[ "${COMP_WORDS[$index]}" == [a-z]* ]]
then
COMP_WORDS_NO_FLAGS+=("${COMP_WORDS[$index]}")
fi
let index++
done
COMP_WORDS_NO_FLAGS+=("${COMP_WORDS[$COMP_CWORD]}")
local COMP_CWORD_NO_FLAGS=$((${#COMP_WORDS_NO_FLAGS[@]} - 1))

cur="${COMP_WORDS_NO_FLAGS[COMP_CWORD_NO_FLAGS]}"
prefix="_${COMP_WORDS_NO_FLAGS[*]:0:COMP_CWORD_NO_FLAGS}"
func_name="${prefix// /_}"
func_name="${func_name//-/_}"

UENV_OPTS=""
if typeset -f $func_name >/dev/null
then
$func_name
fi

COMPREPLY=($(compgen -W "${UENV_OPTS}" -- "${cur}"))
}

complete -F _uenv_completions uenv
)";

return fmt::format("{}{}", prefix_functions, main_functions);
}

} // namespace completion
} // namespace util
11 changes: 11 additions & 0 deletions src/util/completion.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <CLI/CLI.hpp>

namespace util {
namespace completion {

std::string create_completion(CLI::App* cli);

} // namespace completion
} // namespace util
13 changes: 13 additions & 0 deletions subprojects/range-v3.wrap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[wrap-file]
directory = range-v3-0.12.0
source_url = https://github.com/ericniebler/range-v3/archive/0.12.0.tar.gz
source_filename = range-v3-0.12.0.tar.gz
source_hash = 015adb2300a98edfceaf0725beec3337f542af4915cec4d0b89fa0886f4ba9cb
patch_filename = range-v3_0.12.0-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/range-v3_0.12.0-2/get_patch
patch_hash = 0d0b2959701d42bb8c509eb770276de67e44b60ffba9af9cd66dad855700ec63
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/range-v3_0.12.0-2/range-v3-0.12.0.tar.gz
wrapdb_version = 0.12.0-2

[provide]
range-v3 = range_dep