diff --git a/changelog.md b/changelog.md index 94a8c43..5746056 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,6 @@ # Changelog +- #129 [feature] `uenv image find` and `uenv image ls` do partial match on uenv names - #127 [feature] find views, mount point, etc. information using `uenv image inspect` without opening a uenv. ## 9.1.2 diff --git a/src/cli/add_remove.cpp b/src/cli/add_remove.cpp index 48772e1..fd4d3c2 100644 --- a/src/cli/add_remove.cpp +++ b/src/cli/add_remove.cpp @@ -369,7 +369,7 @@ int image_rm([[maybe_unused]] const image_rm_args& args, } else if (r->size() > 1) { term::error("the pattern {} matches more than one " "uenv:\n{}use a more specific version", - U, format_record_set(*r)); + U, format_record_set_list(*r)); return 1; } else { // check whether there are more than one tag attached to sha @@ -411,7 +411,7 @@ int image_rm([[maybe_unused]] const image_rm_args& args, } else { term::msg("the following uenv {} removed:", (removed.size() > 1 ? "were" : "was")); - print_record_set(removed, true); + print_record_set(removed, record_set_format::list); } return 0; diff --git a/src/cli/copy.cpp b/src/cli/copy.cpp index 352db24..f05c28c 100644 --- a/src/cli/copy.cpp +++ b/src/cli/copy.cpp @@ -116,7 +116,7 @@ int image_copy([[maybe_unused]] const image_copy_args& args, std::string errmsg = fmt::format("more than one uenv found that matches '{}':\n", args.src_uenv_description); - errmsg += format_record_set(*src_matches); + errmsg += format_record_set_table(*src_matches); term::error("{}", errmsg); return 1; } diff --git a/src/cli/delete.cpp b/src/cli/delete.cpp index 40e7fa1..28e19c2 100644 --- a/src/cli/delete.cpp +++ b/src/cli/delete.cpp @@ -102,7 +102,7 @@ int image_delete([[maybe_unused]] const image_delete_args& args, std::string errmsg = fmt::format("more than one sha found that matches '{}':\n", args.uenv_description); - errmsg += format_record_set(*matches); + errmsg += format_record_set_table(*matches); term::error("{}", errmsg); return 1; } diff --git a/src/cli/find.cpp b/src/cli/find.cpp index fe1634d..f8166ba 100644 --- a/src/cli/find.cpp +++ b/src/cli/find.cpp @@ -30,9 +30,13 @@ void image_find_args::add_cli(CLI::App& cli, find_cli->add_option("uenv", uenv_description, "search term"); find_cli->add_flag("--no-header", no_header, "print only the matching records, with no header."); - find_cli->add_flag("--json", json, "format output as JSON."); - find_cli->add_flag("--build", build, - "invalid: replaced with 'build::' prefix on uenv label"); + find_cli->add_flag("--json", json, + "format output as JSON (incompatible with --list)."); + find_cli->add_flag("--list", list, + "list the full specs of matching records with no header " + "(incompatible with --json)."); + find_cli->add_flag("--no-partials", no_partials, + "do not match partial names when searching."); find_cli->callback( [&settings]() { settings.mode = uenv::cli_mode::image_find; }); @@ -50,6 +54,11 @@ int image_find([[maybe_unused]] const image_find_args& args, return 1; } + auto format = get_record_set_format(args.no_header, args.json, args.list); + if (!format) { + term::error("{}", format.error()); + } + // find the search term that was provided by the user uenv_label label{}; std::string nspace{site::default_namespace()}; @@ -75,14 +84,13 @@ int image_find([[maybe_unused]] const image_find_args& args, } // search db for matching records - const auto result = store->query(label); + const auto result = store->query(label, !args.no_partials); if (!result) { term::error("invalid search term: {}", store.error()); return 1; } - // pass results to print - print_record_set(*result, args.no_header, args.json); + print_record_set(*result, *format); return 0; } @@ -133,4 +141,20 @@ std::string image_find_footer() { return fmt::format("{}", fmt::join(items, "\n")); } +util::expected +get_record_set_format(bool no_header, bool json, bool list) { + if (json && list) { + return util::unexpected( + "the --json and --list options are incompatible and can not be " + "used at the same time"); + } + + if (!json && !list) { + return no_header ? record_set_format::table_no_header + : record_set_format::table; + } + + return json ? record_set_format::json : record_set_format::list; +} + } // namespace uenv diff --git a/src/cli/find.h b/src/cli/find.h index 75ab820..cf7d3a4 100644 --- a/src/cli/find.h +++ b/src/cli/find.h @@ -13,6 +13,8 @@ struct image_find_args { std::optional uenv_description; bool no_header = false; bool json = false; + bool list = false; + bool no_partials = false; bool build = false; void add_cli(CLI::App&, global_settings& settings); }; diff --git a/src/cli/ls.cpp b/src/cli/ls.cpp index b899ed3..ea9fcb0 100644 --- a/src/cli/ls.cpp +++ b/src/cli/ls.cpp @@ -28,7 +28,13 @@ void image_ls_args::add_cli(CLI::App& cli, ls_cli->add_option("uenv", uenv_description, "search term"); ls_cli->add_flag("--no-header", no_header, "print only the matching records, with no header."); - ls_cli->add_flag("--json", json, "format output as JSON."); + ls_cli->add_flag("--json", json, + "format output as JSON (incompatible with --list)."); + ls_cli->add_flag("--list", list, + "list the full specs of matching records with no header " + "(incompatible with --json)."); + ls_cli->add_flag("--no-partials", no_partials, + "do not match partial names when searching."); ls_cli->callback( [&settings]() { settings.mode = uenv::cli_mode::image_ls; }); @@ -43,6 +49,11 @@ int image_ls(const image_ls_args& args, const global_settings& settings) { return 1; } + auto format = get_record_set_format(args.no_header, args.json, args.list); + if (!format) { + term::error("{}", format.error()); + } + // open the repo auto store = uenv::open_repository(settings.config.repo.value()); if (!store) { @@ -67,13 +78,13 @@ int image_ls(const image_ls_args& args, const global_settings& settings) { site::get_system_name(label.system, settings.calling_environment); // query the repo - const auto result = store->query(label); + const auto result = store->query(label, !args.no_partials); if (!result) { term::error("invalid search term: {}", store.error()); return 1; } - print_record_set(*result, args.no_header, args.json); + print_record_set(*result, *format); return 0; } diff --git a/src/cli/ls.h b/src/cli/ls.h index ca6f8e2..e1a514d 100644 --- a/src/cli/ls.h +++ b/src/cli/ls.h @@ -13,6 +13,8 @@ struct image_ls_args { std::optional uenv_description; bool no_header = false; bool json = false; + bool list = false; + bool no_partials = false; void add_cli(CLI::App&, global_settings& settings); }; @@ -32,7 +34,9 @@ template <> class fmt::formatter { template constexpr auto format(uenv::image_ls_args const& opts, FmtContext& ctx) const { - return fmt::format_to(ctx.out(), "{{uenv: '{}'}}", - opts.uenv_description); + return fmt::format_to( + ctx.out(), + "{{uenv: '{}', json: {}, no_partials: {}, no_header: {}}}", + opts.uenv_description, opts.json, opts.no_partials, opts.no_header); } }; diff --git a/src/cli/pull.cpp b/src/cli/pull.cpp index c5dbff3..5893beb 100644 --- a/src/cli/pull.cpp +++ b/src/cli/pull.cpp @@ -121,7 +121,7 @@ int image_pull([[maybe_unused]] const image_pull_args& args, std::string errmsg = fmt::format("more than one uenv found that matches '{}':\n", args.uenv_description); - errmsg += format_record_set(*remote_matches); + errmsg += format_record_set_table(*remote_matches); term::error("{}", errmsg); return 1; } diff --git a/src/uenv/env.cpp b/src/uenv/env.cpp index 6daaf45..ceb3d63 100644 --- a/src/uenv/env.cpp +++ b/src/uenv/env.cpp @@ -160,7 +160,7 @@ resolve_uenv(const uenv_description& desc, fmt::format("more than one uenv matches the uenv description " "'{}':\n", desc.label().value()); - errmsg += format_record_set(results); + errmsg += format_record_set_table(results); return unexpected(errmsg); } diff --git a/src/uenv/print.cpp b/src/uenv/print.cpp index e477813..d09910f 100644 --- a/src/uenv/print.cpp +++ b/src/uenv/print.cpp @@ -3,13 +3,14 @@ #include #include +#include #include #include #include namespace uenv { -std::string format_record_set(const record_set& records, bool no_header) { +std::string format_record_set_table(const record_set& records, bool no_header) { if (!no_header && records.empty()) { if (!no_header) { return "no matching uenv\n"; @@ -62,6 +63,16 @@ std::string format_record_set(const record_set& records, bool no_header) { return result; } +std::string format_record_set_list(const record_set& records) { + std::string result; + for (auto& r : records) { + result += fmt::format("{}/{}:{}@{}%{}\n", r.name, r.version, r.tag, + r.system, r.uarch); + } + + return result; +} + std::string format_record_set_json(const record_set& records) { using nlohmann::json; std::vector jrecords; @@ -79,9 +90,21 @@ std::string format_record_set_json(const record_set& records) { return json{{"records", jrecords}}.dump(); } -void print_record_set(const record_set& records, bool no_header, bool json) { - fmt::print("{}", json ? format_record_set_json(records) - : format_record_set(records, no_header)); +void print_record_set(const record_set& records, record_set_format f) { + switch (f) { + case record_set_format::json: + fmt::print("{}", format_record_set_json(records)); + return; + case record_set_format::list: + fmt::print("{}", format_record_set_list(records)); + return; + case record_set_format::table: + fmt::print("{}", format_record_set_table(records, false)); + return; + case record_set_format::table_no_header: + fmt::print("{}", format_record_set_table(records, true)); + return; + } } } // namespace uenv diff --git a/src/uenv/print.h b/src/uenv/print.h index fd881c6..c9207c9 100644 --- a/src/uenv/print.h +++ b/src/uenv/print.h @@ -1,10 +1,22 @@ #pragma once #include +#include namespace uenv { -void print_record_set(const record_set& result, bool no_header = true, - bool json = false); -std::string format_record_set(const record_set& records, bool no_header = true); + +enum class record_set_format { table, table_no_header, json, list }; + +void print_record_set(const record_set& result, record_set_format format); + +std::string format_record_set_table(const record_set& records, + bool no_header = true); +std::string format_record_set_list(const record_set& records); std::string format_record_set_json(const record_set& records); + +// a helper for determining the output format based on CLI flags: +// --no-header, --json and --list +util::expected +get_record_set_format(bool no_header, bool json, bool list); + } // namespace uenv diff --git a/src/uenv/repository.cpp b/src/uenv/repository.cpp index d22bd14..ef01906 100644 --- a/src/uenv/repository.cpp +++ b/src/uenv/repository.cpp @@ -396,7 +396,7 @@ struct repository_impl { std::optional path; util::expected, std::string> - query(const uenv_label&) const; + query(const uenv_label&, bool partial_name = false) const; repository::pathset uenv_paths(sha256) const; bool contains(const uenv_record&) const; @@ -675,7 +675,7 @@ repository_impl::add(const uenv::uenv_record& r) { util::expected repository_impl::remove(const sha256& sha) { - auto matches = query({.name = sha.string()}); + auto matches = query(uenv_label{.name = sha.string()}); std::vector statements{ // begin the transaction "PRAGMA foreign_keys = ON", "BEGIN", @@ -740,12 +740,16 @@ WHERE sha256='{}' AND tag='{}' AND version_id IN ( } util::expected, std::string> -repository_impl::query(const uenv_label& label) const { +repository_impl::query(const uenv_label& label, bool partial_name) const { std::vector results; std::vector query_terms; if (label.name) { - query_terms.push_back(fmt::format("name = '{}'", *label.name)); + if (partial_name) { + query_terms.push_back(fmt::format("name LIKE '{}%'", *label.name)); + } else { + query_terms.push_back(fmt::format("name = '{}'", *label.name)); + } } if (label.tag) { query_terms.push_back(fmt::format("tag = '{}'", *label.tag)); @@ -953,8 +957,8 @@ std::optional repository::path() const { } util::expected -repository::query(const uenv_label& label) const { - return impl_->query(label); +repository::query(const uenv_label& label, bool partial_name) const { + return impl_->query(label, partial_name); } util::expected repository::add(const uenv_record& r) { diff --git a/src/uenv/repository.h b/src/uenv/repository.h index 5c29843..802c252 100644 --- a/src/uenv/repository.h +++ b/src/uenv/repository.h @@ -89,7 +89,10 @@ struct repository { std::optional path() const; util::expected - query(const uenv_label& label) const; + // search for all records that match a label. + // the partial_name parameter toggles on partial matches on the + // label.name field, which is useful for `image ls` `image find` searches. + query(const uenv_label& label, bool partial_name = false) const; bool contains(const uenv_record&) const;