From b36d55ceeed6ae0ba03dfcd068307df8f7537fc6 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sat, 1 Jul 2023 21:32:24 -0600 Subject: [PATCH] Change command-line parsing (again...) - Added formatted table with `--style` option - Added `warning` log level - Fixed bugs and cleaned up API - Removed some extra debugging output --- .gitmodules | 3 + CMakeLists.txt | 2 +- include/config.hpp | 21 +- include/error.hpp | 7 + include/log.hpp | 16 ++ include/package.hpp | 1 + include/types.hpp | 3 + modules/argparse | 1 + src/config.cpp | 167 ++++++++++-- src/package.cpp | 104 +++++--- src/package_manager.cpp | 567 ++++++++++++++++++++++++++++------------ src/remote.cpp | 23 +- src/rest_api.cpp | 6 +- src/utils.cpp | 14 +- tests/basic.cpp | 5 +- 15 files changed, 685 insertions(+), 255 deletions(-) create mode 160000 modules/argparse diff --git a/.gitmodules b/.gitmodules index 4d31fd3..1efeb8d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "modules/csv2"] path = modules/csv2 url = https://github.com/p-ranav/csv2 +[submodule "modules/argparse"] + path = modules/argparse + url = https://github.com/p-ranav/argparse diff --git a/CMakeLists.txt b/CMakeLists.txt index e9bcb56..d5a7d72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ find_package(SQLiteCpp CONFIG REQUIRED) set(CMAKE_CXX_COMPILER "clang++") set(CMAKE_BUILD_RPATH "build/cmake") set(CMAKE_CXX_FLAGS - "${CMAKE_CXX_FLAGS} -std=c++20 -Ofast -fPIC -fPIE -fpermissive -Wall -Wno-switch -Wno-unused-variable -Wno-unused-function -Wno-sign-conversion -pedantic-errors" + "${CMAKE_CXX_FLAGS} -std=c++20 -Ofast -fPIC -fPIE -fpermissive -Wall -Wno-switch -Wno-unused-variable -Wno-unused-function -Wno-sign-conversion -Wno-deprecated-declarations -pedantic-errors" ) set(INCLUDE_DIRS "include" diff --git a/include/config.hpp b/include/config.hpp index 4f35339..d0fee1e 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -17,6 +17,11 @@ namespace gdpm::package{ } namespace gdpm::config{ + enum class print_style{ + list = 0, + table = 1, + }; + struct context{ string username; string password; @@ -25,24 +30,28 @@ namespace gdpm::config{ string packages_dir; string tmp_dir; string_map remote_sources; - size_t jobs = 1; - size_t timeout = 3000; - size_t max_results = 200; + int jobs = 1; + int timeout = 3000; bool enable_sync = true; bool enable_cache = true; bool skip_prompt = false; bool enable_file_logging; bool clean_temporary; - int verbose; + + int verbose = log::INFO; + print_style style = print_style::list; package::info info; - rest_api::request_params api_params; + rest_api::request_params rest_api_params; }; string to_json(const context& config, bool pretty_print = false); error load(std::filesystem::path path, context& config); error save(std::filesystem::path path, const context& config); error handle_config(config::context& config, const args_t& args, const var_opts& opts); - context make_context(const string& username = GDPM_CONFIG_USERNAME, const string& password = GDPM_CONFIG_PASSWORD, const string& path = GDPM_CONFIG_PATH, const string& token = GDPM_CONFIG_TOKEN, const string& godot_version = GDPM_CONFIG_GODOT_VERSION, const string& packages_dir = GDPM_CONFIG_LOCAL_PACKAGES_DIR, const string& tmp_dir = GDPM_CONFIG_LOCAL_TMP_DIR, const string_map& remote_sources = {GDPM_CONFIG_REMOTE_SOURCES}, size_t threads = GDPM_CONFIG_THREADS, size_t timeout = 0, bool enable_sync = GDPM_CONFIG_ENABLE_SYNC, bool enable_file_logging = GDPM_CONFIG_ENABLE_FILE_LOGGING, int verbose = GDPM_CONFIG_VERBOSE); + error set_property(config::context& config, const string& property, const any& value); + template + T& get_property(const config::context& config, const string& property); + context make_context(const string& username = GDPM_CONFIG_USERNAME, const string& password = GDPM_CONFIG_PASSWORD, const string& path = GDPM_CONFIG_PATH, const string& token = GDPM_CONFIG_TOKEN, const string& godot_version = GDPM_CONFIG_GODOT_VERSION, const string& packages_dir = GDPM_CONFIG_LOCAL_PACKAGES_DIR, const string& tmp_dir = GDPM_CONFIG_LOCAL_TMP_DIR, const string_map& remote_sources = {GDPM_CONFIG_REMOTE_SOURCES}, int jobs = GDPM_CONFIG_THREADS, int timeout = 0, bool enable_sync = GDPM_CONFIG_ENABLE_SYNC, bool enable_file_logging = GDPM_CONFIG_ENABLE_FILE_LOGGING, int verbose = GDPM_CONFIG_VERBOSE); error validate(const rapidjson::Document& doc); void print_json(const context& config); void print_properties(const context& config, const string_list& properties); diff --git a/include/error.hpp b/include/error.hpp index 356ae91..544187c 100644 --- a/include/error.hpp +++ b/include/error.hpp @@ -13,6 +13,8 @@ namespace gdpm::constants::error{ NONE = 0, UNKNOWN, UNKNOWN_COMMAND, + UNKNOWN_ARGUMENT, + ARGPARSE_ERROR, NOT_FOUND, NOT_DEFINED, NOT_IMPLEMENTED, @@ -102,6 +104,11 @@ namespace gdpm{ #endif } + static constexpr gdpm::error error_rc(const gdpm::error& e){ + error(e); + return e; + } + static void error(const char *p, const gdpm::error& e){ println("{}{}{}", p, prefix.contents, e.get_message()); } diff --git a/include/log.hpp b/include/log.hpp index 07516cd..16c5b18 100644 --- a/include/log.hpp +++ b/include/log.hpp @@ -77,6 +77,7 @@ namespace gdpm::log inline constexpr const char* get_info_prefix() { return "[INFO {}] "; } inline constexpr const char* get_error_prefix() { return "[ERROR {}] "; } inline constexpr const char* get_debug_prefix() { return "[DEBUG {}] "; } + inline constexpr const char* get_warning_prefix() { return "[WARN {}] "; } static void vlog(fmt::string_view format, fmt::format_args args){ fmt::vprint(format, args); @@ -142,6 +143,21 @@ namespace gdpm::log #endif } + template + static constexpr void warn(const S& format, Args&&...args){ + if(log::level < to_int(log::WARNING)) + return; +#if GDPM_LOG_LEVEL > WARN + set_prefix_if(std::format(get_warning_prefix(), utils::timestamp()), true); + set_suffix_if("\n"); + vlog( + fmt::format(GDPM_COLOR_LOG_WARNING "{}{}{}" GDPM_COLOR_LOG_RESET, prefix.contents, format, suffix), + fmt::make_format_args(args...) + ); +#endif + } + + template static constexpr void print(const S& format, Args&&...args){ vlog( diff --git a/include/package.hpp b/include/package.hpp index e91e614..d3c2033 100644 --- a/include/package.hpp +++ b/include/package.hpp @@ -116,6 +116,7 @@ namespace gdpm::package { GDPM_DLL_EXPORT void print_list(const rapidjson::Document& json); GDPM_DLL_EXPORT void print_list(const info_list& packages); + GDPM_DLL_EXPORT void print_table(const info_list& packages); GDPM_DLL_EXPORT result_t get_package_info(const opts_t& opts); GDPM_DLL_EXPORT result_t get_package_titles(const info_list& packages); GDPM_DLL_EXPORT void clean_temporary(const config::context& config, const title_list& package_titles); diff --git a/include/types.hpp b/include/types.hpp index c604c8c..9a5014e 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -1,11 +1,13 @@ #pragma once +#include "clipp.h" #include #include #include #include #include #include +#include namespace gdpm{ class error; @@ -47,6 +49,7 @@ namespace gdpm{ using string_list = std::vector; using string_map = std::unordered_map; using string_pair = std::pair; + using any = std::any; using var = std::variant; template using _args_t = std::vector; diff --git a/modules/argparse b/modules/argparse new file mode 160000 index 0000000..557948f --- /dev/null +++ b/modules/argparse @@ -0,0 +1 @@ +Subproject commit 557948f1236db9e27089959de837cc23de6c6bbd diff --git a/src/config.cpp b/src/config.cpp index dacaacb..3854b53 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -7,6 +7,7 @@ #include "types.hpp" // RapidJSON +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include // fmt @@ -67,7 +69,7 @@ namespace gdpm::config{ file.open(path, std::ios::in); if(!file){ if(config.verbose) - log::info("No configuration file found. Creating a new one."); + log::warn("No configuration file found. Creating a new one."); config = make_context(); save(config.path, config); return error(); @@ -230,6 +232,46 @@ namespace gdpm::config{ return error(); } + error set_property( + config::context& config, + const string& property, + const any& value + ){ + log::println("config::set_property() called"); + if(property == "username") config.username = std::any_cast(value); + else if(property == "password") config.password = std::any_cast(value); + else if(property == "path") config.path = std::any_cast(value); + else if(property == "token") config.token = std::any_cast(value); + else if(property == "packages_dir") config.packages_dir = std::any_cast(value); + else if(property == "tmp_dir") config.tmp_dir = std::any_cast(value); + else if(property == "remote_sources") config.remote_sources = std::any_cast(value); + else if(property == "jobs") config.jobs = std::any_cast(value); + else if(property == "timeout") config.timeout = std::any_cast(value); + else if(property == "enable_sync") config.enable_sync = std::any_cast(value); + else if(property == "enable_cache") config.enable_cache = std::any_cast(value); + else if(property == "skip_prompt") config.skip_prompt = std::any_cast(value); + else if(property == "enable_file_logging") config.enable_file_logging = std::any_cast(value); + else if(property == "clean_temporary") config.clean_temporary = std::any_cast(value); + else{ + return log::error_rc(error( + constants::error::INVALID_CONFIG, + "Could not find property" + )); + } + } + + template + T& get_property( + const config::context& config, + const string& property + ){ + log::println("config::get_property() called"); + if(property == "username") return config.username; + else if(property == "password") return config.password; + else if(property == "path") return config.path; + else if(property == "token") return config.token; + else if(property == "package_dir") return config.packages_dir; + } context make_context( const string& username, @@ -240,8 +282,8 @@ namespace gdpm::config{ const string& packages_dir, const string& tmp_dir, const string_map& remote_sources, - size_t threads, - size_t timeout, + int jobs, + int timeout, bool enable_sync, bool enable_file_logging, int verbose @@ -254,7 +296,7 @@ namespace gdpm::config{ .packages_dir = (packages_dir.empty()) ? string(getenv("HOME")) + ".gdpm" : packages_dir, .tmp_dir = tmp_dir, .remote_sources = remote_sources, - .jobs = threads, + .jobs = jobs, .timeout = timeout, .enable_sync = enable_sync, .enable_file_logging = enable_file_logging, @@ -293,6 +335,8 @@ namespace gdpm::config{ const context& config, const string& property ){ + using namespace tabulate; + if(property.empty()) return; else if(property == "username") log::println("username: {}", config.username); else if(property == "password") log::println("password: {}", config.password); @@ -311,34 +355,103 @@ namespace gdpm::config{ else if(property == "verbose") log::println("verbose: {}", config.verbose); } + + void add_row( + tabulate::Table& table, + const context& config, + const string property + ){ + if(property.empty()) return; + else if(property == "username") table.add_row({"Username", config.username}); + else if(property == "password") table.add_row({"Password", config.password}); + else if(property == "path") table.add_row({"Path", config.path}); + else if(property == "token") table.add_row({"Token", config.token}); + else if(property == "packages_dir") table.add_row({"Package Directory", config.packages_dir}); + else if(property == "tmp_dir") table.add_row({"Temp Directory", config.tmp_dir}); + else if(property == "remote_sources") table.add_row({"Remotes", utils::join(config.remote_sources, "\t", "\n")}); + else if(property == "jobs") table.add_row({"Threads", std::to_string(config.jobs)}); + else if(property == "timeout") table.add_row({"Timeout", std::to_string(config.timeout)}); + else if(property == "sync") table.add_row({"Fetch Assets", std::to_string(config.enable_sync)}); + else if(property == "cache") table.add_row({"Cache", std::to_string(config.enable_cache)}); + else if(property == "prompt") table.add_row({"Skip Prompt", std::to_string(config.skip_prompt)}); + else if(property == "logging") table.add_row({"File Logging", std::to_string(config.enable_file_logging)}); + else if(property == "clean") table.add_row({"Clean Temporary", std::to_string(config.clean_temporary)}); + else if(property == "verbose") table.add_row({"Verbosity", std::to_string(config.verbose)}); + } + void print_properties( const context& config, const string_list& properties ){ - if(properties.empty()){ - _print_property(config, "username"); - _print_property(config, "password"); - _print_property(config, "path"); - _print_property(config, "token"); - _print_property(config, "packages_dir"); - _print_property(config, "tmp_dir"); - _print_property(config, "remote_sources"); - _print_property(config, "jobs"); - _print_property(config, "timeout"); - _print_property(config, "sync"); - _print_property(config, "cache"); - _print_property(config, "prompt"); - _print_property(config, "logging"); - _print_property(config, "clean"); - _print_property(config, "verbose"); - } - std::for_each( - properties.begin(), - properties.end(), - [&config](const string& property){ - _print_property(config, property); + using namespace tabulate; + + if(config.style == config::print_style::list){ + if(properties.empty()){ + _print_property(config, "username"); + _print_property(config, "password"); + _print_property(config, "path"); + _print_property(config, "token"); + _print_property(config, "packages_dir"); + _print_property(config, "tmp_dir"); + _print_property(config, "remote_sources"); + _print_property(config, "jobs"); + _print_property(config, "timeout"); + _print_property(config, "sync"); + _print_property(config, "cache"); + _print_property(config, "prompt"); + _print_property(config, "logging"); + _print_property(config, "clean"); + _print_property(config, "verbose"); } - ); + else { + std::for_each( + properties.begin(), + properties.end(), + [&config](const string& property){ + _print_property(config, property); + } + ); + } + } + else if(config.style == config::print_style::table){ + Table table; + if(properties.empty()){ + table.add_row({"Property", "Value"}); + table.add_row({"Username", config.username}); + table.add_row({"Password", config.password}); + table.add_row({"Path", config.path}); + table.add_row({"Token", config.token}); + table.add_row({"Package Directory", config.token}); + table.add_row({"Temp Directory", config.tmp_dir}); + table.add_row({"Remotes", utils::join(config.remote_sources)}); + table.add_row({"Threads", std::to_string(config.jobs)}); + table.add_row({"Timeout", std::to_string(config.timeout)}); + table.add_row({"Fetch Data", std::to_string(config.enable_sync)}); + table.add_row({"Use Cache", std::to_string(config.enable_cache)}); + table.add_row({"Logging", std::to_string(config.enable_file_logging)}); + table.add_row({"Clean", std::to_string(config.clean_temporary)}); + table.add_row({"Verbosity", std::to_string(config.verbose)}); + } + else{ + std::for_each( + properties.begin(), + properties.end(), + [&table, &config](const string& property){ + add_row(table, config, property); + } + ); + } + table[0].format() + .padding_top(1) + .padding_bottom(1) + .font_background_color(Color::red) + .font_style({FontStyle::bold}); + table.column(1).format() + .font_color(Color::yellow); + table[0][1].format() + .font_background_color(Color::blue); + table.print(std::cout); + } } } \ No newline at end of file diff --git a/src/package.cpp b/src/package.cpp index 21f4381..f52b9ad 100644 --- a/src/package.cpp +++ b/src/package.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace gdpm::package{ @@ -39,8 +40,7 @@ namespace gdpm::package{ */ /* Append files from --file option */ - if(!params.input_files.empty()){ - log::print("input files"); + // if(!params.input_files.empty()){ for(const auto& filepath : params.input_files){ string contents = utils::readfile(filepath); log::print("contents: {}", contents); @@ -51,7 +51,7 @@ namespace gdpm::package{ std::end(input_titles) ); } - } + // } result_t result = cache::get_package_info_by_title(package_titles); package::info_list p_found = {}; package::info_list p_cache = result.unwrap_unsafe(); @@ -445,16 +445,14 @@ namespace gdpm::package{ rest_api_params.filter = http.url_escape(p_title); - std::string request_url{constants::HostUrl}; + string request_url{constants::HostUrl}; request_url += rest_api::endpoints::GET_Asset; Document doc = rest_api::get_assets_list(request_url, rest_api_params); if(doc.IsNull()){ - error error( + return log::error_rc(error( constants::error::HOST_UNREACHABLE, "Could not fetch metadata." - ); - log::error(error); - return error; + )); } // log::info("{} package(s) found...", doc["total_items"].GetInt()); @@ -476,18 +474,21 @@ namespace gdpm::package{ result_t r_installed = cache::get_installed_packages(); info_list p_installed = r_installed.unwrap_unsafe(); if(!p_installed.empty()){ - print_list(p_installed); + if(config.style == config::print_style::list) + print_list(p_installed); + else if(config.style == config::print_style::table){ + print_table(p_installed); + } } } else if(show == "remote"){ remote::print_repositories(config); } else{ - error error( + log::error(error( constants::error::UNKNOWN_COMMAND, "Unrecognized subcommand. Try either 'packages' or 'remote' instead." - ); - log::error(error); + )); } return error(); } @@ -520,7 +521,7 @@ namespace gdpm::package{ } } std::ofstream of(path); - log::println("writing contents to file"); + log::println("export: {}", path); of << output; of.close(); } @@ -532,17 +533,15 @@ namespace gdpm::package{ error link( const config::context& config, const title_list& package_titles, - const package::params& params /* path is last arg */ + const package::params& params ){ using namespace std::filesystem; - if(params.args.empty()){ - error error( - constants::error::INVALID_ARG_COUNT, - "Must supply at least 2 arguments (package name and path)" - ); - log::error(error); - return error; + if(params.paths.empty()){ + return log::error_rc(error( + constants::error::PATH_NOT_DEFINED, + "Path is required" + )); } /* Check for packages in cache to link */ @@ -550,12 +549,10 @@ namespace gdpm::package{ info_list p_found = {}; info_list p_cache = r_cache.unwrap_unsafe(); if(p_cache.empty()){ - error error( + return log::error_rc(error( constants::error::NOT_FOUND, "Could not find any packages to link in cache." - ); - log::error(error); - return error; + )); } for(const auto& p_title : package_titles){ @@ -575,17 +572,15 @@ namespace gdpm::package{ } /* Get the storage paths for all packages to create symlinks */ - path_refs paths = path_refs({params.args.back()}); const path package_dir{config.packages_dir}; for(const auto& p : p_found){ - for(const auto& path : paths){ - const string _path = path; - log::info_n("link: \"{}\" -> '{}'...", p.title, _path + "/" + p.title); + for(const auto& path : params.paths){ + log::info_n("link: \"{}\" -> '{}'...", p.title, path + "/" + p.title); // std::filesystem::path target{config.packages_dir + "/" + p.title}; std::filesystem::path target = {current_path().string() + "/" + config.packages_dir + "/" + p.title}; - std::filesystem::path symlink_path{_path + "/" + p.title}; + std::filesystem::path symlink_path{path + "/" + p.title}; if(!std::filesystem::exists(symlink_path.string())) - std::filesystem::create_directories(_path + "/"); + std::filesystem::create_directories(path + "/"); std::error_code ec; std::filesystem::create_directory_symlink(target, symlink_path, ec); if(ec){ @@ -609,13 +604,11 @@ namespace gdpm::package{ ){ using namespace std::filesystem; - if(params.args.empty()){ - error error( - constants::error::INVALID_ARG_COUNT, - "Must supply at least 2 arguments (package name and path)" - ); - log::error(error); - return error; + if(params.paths.empty()){ + return log::error_rc(error( + constants::error::PATH_NOT_DEFINED, + "Path is required" + )); } result_t r_cache = cache::get_package_info_by_title(package_titles); @@ -624,12 +617,10 @@ namespace gdpm::package{ /* Check for installed packages to clone */ if(p_cache.empty()){ - error error( + return log::error_rc(error( constants::error::NO_PACKAGE_FOUND, "Could not find any packages to clone in cache." - ); - log::error(error); - return error; + )); } for(const auto& p_title : package_titles){ @@ -694,7 +685,6 @@ namespace gdpm::package{ } } - void print_list(const rapidjson::Document& json){ for(const auto& o : json["result"].GetArray()){ log::println( @@ -717,6 +707,34 @@ namespace gdpm::package{ } } + void print_table(const info_list& packages){ + using namespace tabulate; + Table table; + table.add_row({ + "Asset Name", + "Author", + "Category", + "Version", + "Godot Version", + "License/Cost", + "Last Modified", + "Support" + }); + for(const auto& p : packages){ + table.add_row({ + p.title, + p.author, + p.category, + p.version, + p.godot_version, + p.cost, + p.modify_date, + p.support_level + }); + } + table.print(std::cout); + } + result_t get_package_info(const title_list& package_titles){ return cache::get_package_info_by_title(package_titles); diff --git a/src/package_manager.cpp b/src/package_manager.cpp index 97c97a6..4c83e8a 100644 --- a/src/package_manager.cpp +++ b/src/package_manager.cpp @@ -22,9 +22,11 @@ #include #include #include "clipp.h" +#include "argparse/argparse.hpp" #include #include +#include #include #include @@ -79,185 +81,426 @@ namespace gdpm::package_manager{ return error; } + template + auto set_if_used( + const argparse::ArgumentParser& cmd, + T& value, + const String& arg + ){ + using namespace argparse; + if(cmd.is_used(arg)){ + value = cmd.get(arg); + } + }; + + string_list get_packages_from_parser( + const argparse::ArgumentParser& cmd, + const std::string& arg = "packages" + ){ + if(cmd.is_used(arg)) + return cmd.get(arg); + return string_list(); + } + error parse_arguments(int argc, char **argv){ using namespace clipp; + using namespace argparse; /* Replace cxxopts with clipp */ action_e action = action_e::none; package::title_list package_titles; package::params params; - auto doc_format = clipp::doc_formatting{} - .first_column(7) - .doc_column(45) - .last_column(99); + ArgumentParser program(argv[0], "0.0.1", argparse::default_arguments::help); + ArgumentParser install_command("install"); + ArgumentParser add_command("add"); + ArgumentParser remove_command("remove"); + ArgumentParser update_command("update"); + ArgumentParser search_command("search"); + ArgumentParser export_command("export"); + ArgumentParser list_command("list"); + ArgumentParser link_command("link"); + ArgumentParser clone_command("clone"); + ArgumentParser clean_command("clean"); + ArgumentParser config_command("config"); + ArgumentParser fetch_command("fetch"); + ArgumentParser version_command("version"); + ArgumentParser remote_command("remote"); + ArgumentParser ui_command("ui"); + ArgumentParser help_command("help"); + + ArgumentParser config_get("get"); + ArgumentParser config_set("set"); + + ArgumentParser remote_add("add"); + ArgumentParser remote_remove("remove"); + ArgumentParser remote_list("list"); + + program.add_description("Manage Godot engine assets from CLI"); + program.add_argument("-v", "--verbose") + .action([&](const auto&){ config.verbose += 1; }) + .default_value(false) + .implicit_value(true) + .help("set verbosity level") + .nargs(0); + + install_command.add_description("install package(s)"); + install_command.add_argument("packages") + .required() + .nargs(nargs_pattern::at_least_one) + .help("packages to install"); + install_command.add_argument("--godot-version") + .help("set Godot version for request"); + install_command.add_argument("--clean") + .help("clean temporary files") + .implicit_value(true) + .default_value(false) + .nargs(0); + install_command.add_argument("--disable-sync") + .help("enable syncing with remote before installing") + .implicit_value(true) + .default_value(false) + .nargs(0); + install_command.add_argument("--disable-cache") + .help("disable caching asset data") + .implicit_value(true) + .default_value(false) + .nargs(0); + install_command.add_argument("--remote") + .help("set the remote to use") + .nargs(1); + install_command.add_argument("-j", "--jobs") + .help("set the number of parallel downloads") + .default_value(1) + .nargs(1) + .scan<'i', int>(); + install_command.add_argument("-y", "--skip-prompt") + .help("skip the yes/no prompt") + .implicit_value(true) + .default_value(false) + .nargs(0); + install_command.add_argument("-f", "--file") + .help("set the file(s) to read as input") + .append() + .nargs(1); + install_command.add_argument("-t", "--timeout") + .help("set the request timeout") + .default_value(30) + .nargs(0); + + add_command.add_description("add package to project"); + add_command.add_argument("packages").nargs(nargs_pattern::at_least_one); + add_command.add_argument("--remote"); + add_command.add_argument("-j", "--jobs") + .help("") + .nargs(1) + .default_value(1) + .nargs(1) + .scan<'i', int>(); + add_command.add_argument("-y", "--skip-prompt"); + add_command.add_argument("-f", "--file") + .help("set the file(s) to read as input") + .append() + .nargs(nargs_pattern::at_least_one); + + remove_command.add_description("remove package(s)"); + remove_command.add_argument("packages").nargs(nargs_pattern::at_least_one); + remove_command.add_argument("--clean"); + remove_command.add_argument("-y", "--skip-prompt"); + remove_command.add_argument("-f", "--file") + .help("set the file(s) to read as input") + .append() + .nargs(nargs_pattern::at_least_one); - /* Set global options */ - auto debugOpt = option("-d", "--debug").set(config.verbose, to_int(log::DEBUG)) % "show debug output"; - auto configOpt = option("--config-path").set(config.path) % "set config path"; - auto pathOpt = option("--path").set(params.paths) % "specify a path to use with command"; - auto typeOpt = option("--type").set(config.info.type) % "set package type (any|addon|project)"; - auto sortOpt = option("--sort").set(config.rest_api_params.sort) % "sort packages in order (rating|cost|name|updated)"; - auto supportOpt = option("--support").set(config.rest_api_params.support) % "set the support level for API (all|official|community|testing)"; - auto maxResultsOpt = option("--max-results").set(config.rest_api_params.max_results) % "set the request max results"; - auto godotVersionOpt = option("--godot-version").set(config.rest_api_params.godot_version) % "set the request Godot version"; - auto packageDirOpt = option("--package-dir").set(config.packages_dir) % "set the global package location"; - auto tmpDirOpt = option("--tmp-dir").set(config.tmp_dir) % "set the temporary download location"; - auto timeoutOpt = option("--timeout").set(config.timeout) % "set the request timeout"; - auto verboseOpt = joinable(repeatable(option("-v", "--verbose").call([]{ config.verbose += 1; }))) % "show verbose output"; + update_command.add_description("update package(s)"); + update_command.add_argument("packages").nargs(nargs_pattern::at_least_one); + update_command.add_argument("--clean"); + update_command.add_argument("--remote"); + update_command.add_argument("-f", "--file") + .help("set the file(s) to read as input") + .append() + .nargs(nargs_pattern::at_least_one); - /* Set the options */ - // auto fileOpt = repeatable(option("--file", "-f").set(params.input_files) % "read file as input"); - auto fileOpt = repeatable(option("--file", "-f") & values("input", params.input_files)) % "read file as input"; - auto cleanOpt = option("--clean").set(config.clean_temporary) % "enable/disable cleaning temps"; - auto parallelOpt = option("--jobs").set(config.jobs) % "set number of parallel jobs"; - auto cacheOpt = option("--enable-cache").set(config.enable_cache) % "enable/disable local caching"; - auto syncOpt = option("--enable-sync").set(config.enable_sync) % "enable/disable remote syncing"; - auto skipOpt = option("-y", "--skip-prompt").set(config.skip_prompt, true) % "skip the y/n prompt"; - auto remoteOpt = option("--remote").set(params.remote_source) % "set remote source to use"; + search_command.add_description("search for package(s)"); + search_command.add_argument("packages").nargs(nargs_pattern::at_least_one); + search_command.add_argument("--godot-version"); + search_command.add_argument("--remote"); + search_command.add_argument("-f", "--file") + .help("set the file(s) to read as input") + .append() + .nargs(nargs_pattern::at_least_one); - auto packageValues = values("packages", package_titles); - auto requiredPath = required("--path", params.args); + ui_command.add_description("show user interface (WIP)"); + version_command.add_description("show version and exit"); + help_command.add_description("show help message and exit"); + + export_command.add_description("export install package(s) list"); + export_command.add_argument("paths") + .help("export list of installed packages") + .required() + .nargs(nargs_pattern::at_least_one); + + list_command.add_description("show install package(s) and remotes"); + list_command.add_argument("show") + .help("show installed packages or remote") + .nargs(nargs_pattern::any) + .default_value("packages"); + list_command.add_argument("--style") + .help("set how to print output") + .nargs(1) + .default_value("list"); + + link_command.add_description("link package(s) to path"); + link_command.add_argument("packages") + .help("package(s) to link") + .required() + .nargs(1); + link_command.add_argument("path") + .help("path to link") + .required() + .nargs(1); + + clone_command.add_description("clone package(s) to path"); + clone_command.add_argument("packages") + .help("package(s) to clone") + .required() + .nargs(1);; + clone_command.add_argument("path") + .help("path to clone") + .required() + .nargs(1); + + clean_command.add_description("clean package(s) temporary files"); + clean_command.add_argument("packages") + .help("package(s) to clean") + .required() + .nargs(nargs_pattern::at_least_one); - auto installCmd = "install" % ( - command("install").set(action, action_e::install), - packageValues % "package(s) to install from asset library", - godotVersionOpt, cleanOpt, parallelOpt, syncOpt, skipOpt, remoteOpt, fileOpt - ); - auto addCmd = "add" % ( - command("add").set(action, action_e::add), - packageValues % "package(s) to add to project", - parallelOpt, skipOpt, remoteOpt, fileOpt - ); - auto removeCmd = "remove" % ( - command("remove").set(action, action_e::remove), - packageValues % "packages(s) to remove from project", - fileOpt, skipOpt, cleanOpt - ); - auto updateCmd = "update package(s)" % ( - command("update").set(action, action_e::update), - packageValues % "" - ); - auto searchCmd = "search for package(s)" % ( - command("search").set(action, action_e::search), - packageValues % "", - godotVersionOpt, fileOpt, remoteOpt, configOpt - ); - auto exportCmd = "export installed package list to file" % ( - command("export").set(action, action_e::p_export), - values("paths", params.args) % "" - ); - auto listCmd = "show installed packages" % ( - command("list").set(action, action_e::list) - ); - auto linkCmd = "create link from package to project" % ( - command("link").set(action, action_e::link), - value("package", package_titles) % "", - value("path", params.args) % "" - ); - auto cloneCmd = "clone package to project" % ( - command("clone").set(action, action_e::clone), - value("package", package_titles) % "", - value("path", params.args) % "" - ); - auto cleanCmd = "clean temporary download files" % ( - command("clean").set(action, action_e::clean), - values("packages", package_titles) % "" - ); - auto configCmd = "manage config properties" % ( - command("config").set(action, action_e::config_get) , - ( - ( greedy(command("get")).set(action, action_e::config_get), - option(repeatable(values("properties", params.args))) - ) % "get config properties" - | - ( command("set").set(action, action_e::config_set) , - value("property", params.args[1]).call([]{}), - value("value", params.args[2]).call([]{}) - ) % "set config properties" - ) - ); - auto fetchCmd = "fetch asset data from remote" % ( - command("fetch").set(action, action_e::fetch), - option(values("remote", params.args)) % "" - ); - auto versionCmd = "show the version and exit" %( - command("version").set(action, action_e::version) - ); - auto add_arg = [¶ms](string arg) { params.args.emplace_back(arg); }; - auto remoteCmd = "manage remote sources" % ( - command("remote").set(action, action_e::remote_list).if_missing( - []{ remote::print_repositories(config); } - ), - ( - "add" % ( command("add").set(action, action_e::remote_add), - word("name").call(add_arg) % "", - value("url").call(add_arg) % "" - ) - | - "remove a remote source" % ( command("remove").set(action, action_e::remote_remove), - words("names", params.args) % "" - ) - | - "list remote sources" % ( command("list").set(action, action_e::remote_list)) - ) - ); - auto uiCmd = "start with UI" % ( - command("ui").set(action, action_e::ui) - ); - auto helpCmd = "show this message and exit" % ( - command("help").set(action, action_e::help) - ); + fetch_command.add_description("fetch and sync asset data"); + fetch_command.add_argument("remote") + .help("remote to fetch") + .required() + .nargs(1); - auto cli = ( - debugOpt, verboseOpt, configOpt, - (installCmd | addCmd | removeCmd | updateCmd | searchCmd | exportCmd | - listCmd | linkCmd | cloneCmd | cleanCmd | configCmd | fetchCmd | - remoteCmd | uiCmd | helpCmd | versionCmd) - ); + config_get.add_argument("properties") + .help("get config properties") + .nargs(nargs_pattern::any); + config_get.add_description("get config properties"); + config_set.add_argument("property") + .help("property name") + .required() + .nargs(1); + config_set.add_argument("value") + .help("property value") + .required() + .nargs(1); + config_set.add_description("set config property"); - /* Make help output */ - string man_page_format(""); - auto man_page = make_man_page(cli, argv[0], doc_format) - .prepend_section("DESCRIPTION", "\tManage Godot Game Engine assets from the command-line.") - .append_section("LICENSE", "\tSee the 'LICENSE.md' file for more details."); - std::for_each(man_page.begin(), man_page.end(), - [&man_page_format](const man_page::section& s){ - man_page_format += s.title() + "\n"; - man_page_format += s.content() + "\n\n"; + config_command.add_description("manage config properties"); + config_command.add_subparser(config_get); + config_command.add_subparser(config_set); + + remote_add.add_argument("name") + .help("remote name") + .nargs(1); + remote_add.add_argument("url") + .help("remote url") + .nargs(1); + remote_remove.add_argument("names") + .help("remote name") + .nargs(nargs_pattern::at_least_one); + remote_list.add_argument("--style") + .help("set print style") + .nargs(1); + + remote_command.add_description("manage remote(s)"); + remote_command.add_subparser(remote_add); + remote_command.add_subparser(remote_remove); + remote_command.add_subparser(remote_list); + + // version_command.add_argument(Targs f_args...) + + program.add_subparser(install_command); + program.add_subparser(add_command); + program.add_subparser(remove_command); + program.add_subparser(update_command); + program.add_subparser(search_command); + program.add_subparser(export_command); + program.add_subparser(list_command); + program.add_subparser(link_command); + program.add_subparser(clone_command); + program.add_subparser(clean_command); + program.add_subparser(config_command); + program.add_subparser(fetch_command); + program.add_subparser(remote_command); + program.add_subparser(version_command); + program.add_subparser(ui_command); + program.add_subparser(help_command); + + try{ + program.parse_args(argc, argv); + // program.parse_known_args(argc, argv); + } catch(const std::runtime_error& e){ + return log::error_rc(error( + constants::error::ARGPARSE_ERROR, + e.what()) + ); + } + + if(program.is_subcommand_used(install_command)){ + action = action_e::install; + if(install_command.is_used("packages")) + package_titles = install_command.get("packages"); + set_if_used(install_command, config.rest_api_params.godot_version, "godot-version"); + set_if_used(install_command, config.clean_temporary, "clean"); + set_if_used(install_command, config.enable_sync, "disable-sync"); + set_if_used(install_command, config.enable_cache, "disable-cache"); + set_if_used(install_command, params.remote_source, "remote"); + set_if_used(install_command, config.jobs, "jobs"); + set_if_used(install_command, config.skip_prompt, "skip-prompt"); + set_if_used(install_command, params.input_files, "file"); + set_if_used(install_command, config.timeout, "timeout"); + } + else if(program.is_subcommand_used(add_command)){ + action = action_e::add; + package_titles = get_packages_from_parser(add_command); + set_if_used(add_command, params.remote_source, "remote"); + set_if_used(add_command, config.jobs, "jobs"); + set_if_used(add_command, config.skip_prompt, "skip-prompt"); + set_if_used(add_command, params.input_files, "files"); + } + else if(program.is_subcommand_used(remove_command)){ + action = action_e::remove; + package_titles = get_packages_from_parser(remove_command); + set_if_used(remove_command, config.clean_temporary, "clean"); + set_if_used(remove_command, config.skip_prompt, "skip-prompt"); + set_if_used(remove_command, params.input_files, "file"); + } + else if(program.is_subcommand_used(update_command)){ + action = action_e::update; + package_titles = get_packages_from_parser(program); + set_if_used(update_command, config.clean_temporary, "clean"); + set_if_used(update_command, params.remote_source, "remote"); + set_if_used(update_command, params.input_files, "file"); + } + else if(program.is_subcommand_used(search_command)){ + action = action_e::search; + package_titles = get_packages_from_parser(search_command); + set_if_used(search_command, config.rest_api_params.godot_version, "godot-version"); + set_if_used(search_command, params.remote_source, "remote"); + set_if_used(search_command, params.input_files, "file"); + } + else if(program.is_subcommand_used(export_command)){ + action = action_e::p_export; + params.paths = export_command.get("paths"); + } + else if(program.is_subcommand_used(list_command)){ + action = action_e::list; + // auto list = get_parser(program, "list"); + if(list_command.is_used("show")) + params.args = list_command.get("show"); + if(list_command.is_used("style")){ + string style = list_command.get("style"); + if(!style.compare("list")) + config.style = config::print_style::list; + else if(!style.compare("table")) + config.style = config::print_style::table; } - ); - - // log::level = config.verbose; - if(clipp::parse(argc, argv, cli)){ - log::level = config.verbose; - switch(action){ - case action_e::install: package::install(config, package_titles, params); break; - case action_e::add: package::add(config, package_titles); - case action_e::remove: package::remove(config, package_titles, params); break; - case action_e::update: package::update(config, package_titles, params); break; - case action_e::search: package::search(config, package_titles, params); break; - case action_e::p_export: package::export_to(params.args); break; - case action_e::list: package::list(config, params); break; - /* ...opts are the paths here */ - case action_e::link: package::link(config, package_titles, params); break; - case action_e::clone: package::clone(config, package_titles, params); break; - case action_e::clean: package::clean_temporary(config, package_titles); break; - case action_e::config_get: config::print_properties(config, params.args); break; - case action_e::config_set: config::handle_config(config, package_titles, params.opts); break; - case action_e::fetch: package::synchronize_database(config, package_titles); break; - case action_e::sync: package::synchronize_database(config, package_titles); break; - case action_e::remote_list: remote::print_repositories(config); break; - case action_e::remote_add: remote::add_repository(config, params.args); break; - case action_e::remote_remove: remote::remove_respositories(config, params.args); break; - case action_e::ui: log::println("UI not implemented yet"); break; - case action_e::help: log::println("{}", man_page_format); break; - case action_e::version: break; - case action_e::none: /* ...here to run with no command */ break; + } + else if(program.is_subcommand_used(link_command)){ + action = action_e::link; + package_titles = get_packages_from_parser(link_command); + set_if_used(link_command, params.paths, "path"); + } + else if(program.is_subcommand_used(clone_command)){ + action = action_e::clone; + package_titles = get_packages_from_parser(clone_command); + set_if_used(clone_command, params.paths, "path"); + } + else if(program.is_subcommand_used(clean_command)){ + action = action_e::clean; + package_titles = get_packages_from_parser(clean_command); + } + else if(program.is_subcommand_used(config_command)){ + if(config_command.is_subcommand_used(config_get)){ + action = action_e::config_get; + if(config_get.is_used("properties")) + params.args = config_get.get("properties"); } - } else { - log::println("usage:\n{}", usage_lines(cli, argv[0]).str()); + else if(config_command.is_subcommand_used(config_set)){ + action = action_e::config_set; + if(config_set.is_used("property")) + params.args.emplace_back(config_set.get("property")); + if(config_set.is_used("value")) + params.args.emplace_back(config_set.get("value")); + } + // else{ + // action = action_e::config_get; + // } + } + else if(program.is_subcommand_used(fetch_command)){ + action = action_e::fetch; + params.remote_source = fetch_command.get("remote"); + } + else if(program.is_subcommand_used(version_command)){ + action = action_e::version; + } + else if(program.is_subcommand_used(remote_command)){ + if(remote_command.is_subcommand_used(remote_add)){ + action = action_e::remote_add; + if(remote_add.is_used("name")) + params.args.emplace_back(remote_add.get("name")); + if(remote_add.is_used("url")) + params.args.emplace_back(remote_add.get("url")); + for(const auto& arg: params.args){ + log::println("{}: {}", params.args[0], params.args[1]); + } + } + if(remote_command.is_subcommand_used(remote_remove)){ + action = action_e::remote_remove; + if(remote_remove.is_used("names")) + params.args = remote_remove.get("names"); + } + if(remote_command.is_subcommand_used(remote_list)){ + action = action_e::remote_list; + string style = remote_list.get("style"); + if(!style.compare("list")) + config.style = config::print_style::list; + else if(!style.compare("table")) + config.style = config::print_style::table; + } + } + else if(program.is_subcommand_used("ui")){ + action = action_e::ui; + } + else if(program.is_subcommand_used("help")){ + action = action_e::help; + } + + switch(action){ + case action_e::install: package::install(config, package_titles, params); break; + case action_e::add: package::add(config, package_titles, params); + case action_e::remove: package::remove(config, package_titles, params); break; + case action_e::update: package::update(config, package_titles, params); break; + case action_e::search: package::search(config, package_titles, params); break; + case action_e::p_export: package::export_to(params.paths); break; + case action_e::list: package::list(config, params); break; + /* ...opts are the paths here */ + case action_e::link: package::link(config, package_titles, params); break; + case action_e::clone: package::clone(config, package_titles, params); break; + case action_e::clean: package::clean_temporary(config, package_titles); break; + case action_e::config_get: config::print_properties(config, params.args); break; + case action_e::config_set: config::set_property(config, params.args[0], params.args[1]); break; + case action_e::fetch: package::synchronize_database(config, package_titles); break; + case action_e::sync: package::synchronize_database(config, package_titles); break; + case action_e::remote_list: remote::print_repositories(config); break; + case action_e::remote_add: remote::add_repository(config, params.args); break; + case action_e::remote_remove: remote::remove_respositories(config, params.args); break; + case action_e::ui: log::println("UI not implemented"); break; + case action_e::help: program.print_help(); break; + case action_e::version: break; + case action_e::none: program.usage(); break;/* ...here to run with no command */ break; } return error(); } diff --git a/src/remote.cpp b/src/remote.cpp index 94dca06..c16d5a1 100644 --- a/src/remote.cpp +++ b/src/remote.cpp @@ -1,9 +1,11 @@ #include "remote.hpp" +#include "config.hpp" #include "error.hpp" #include "log.hpp" #include "types.hpp" #include +#include namespace gdpm::remote{ @@ -12,7 +14,7 @@ namespace gdpm::remote{ const args_t &args ){ /* Check if enough args were provided. */ - log::debug("arg count: {}\nargs: {}", args.size(), utils::join(args)); + log::println("arg count: {}\nargs: {}", args.size(), utils::join(args)); if (args.size() < 2){ return error( constants::error::INVALID_ARG_COUNT, @@ -31,7 +33,7 @@ namespace gdpm::remote{ config::context& config, const args_t& args ){ - log::debug("arg count: {}\nargs: {}", args.size(), utils::join(args)); + log::println("arg count: {}\nargs: {}", args.size(), utils::join(args)); if(args.size() < 1){ return error( constants::error::INVALID_ARG_COUNT, @@ -61,8 +63,19 @@ namespace gdpm::remote{ void print_repositories(const config::context& config){ const auto &rs = config.remote_sources; - std::for_each(rs.begin(), rs.end(), [](const string_pair& p){ - log::println("{}: {}", p.first, p.second); - }); + if(config.style == config::print_style::list){ + std::for_each(rs.begin(), rs.end(), [](const string_pair& p){ + log::println("{}: {}", p.first, p.second); + }); + } + else if(config.style == config::print_style::table){ + using namespace tabulate; + Table table; + table.add_row({"Name", "URL"}); + std::for_each(rs.begin(), rs.end(), [&table](const string_pair& p){ + table.add_row({p.first, p.second}); + }); + table.print(std::cout); + } } } \ No newline at end of file diff --git a/src/rest_api.cpp b/src/rest_api.cpp index cc5d5a4..fbf56e3 100644 --- a/src/rest_api.cpp +++ b/src/rest_api.cpp @@ -17,7 +17,9 @@ namespace gdpm::rest_api{ request_params make_from_config(const config::context& config){ - return config.rest_api_params; + request_params rp = config.rest_api_params; + rp.verbose = config.verbose; + return rp; } request_params make_request_params( @@ -212,7 +214,7 @@ namespace gdpm::rest_api{ http_params.headers.insert(http::header("Connection", "keep-alive")); string request_url = _prepare_request(url, c); http::response r = http.request_get(request_url, http_params); - if(c.verbose > 0) + if(c.verbose >= log::INFO) log::info("get_asset().URL: {}", request_url); return _parse_json(r.body, c.verbose); } diff --git a/src/utils.cpp b/src/utils.cpp index ccfde94..5941056 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -186,24 +186,24 @@ namespace gdpm::utils{ int verbose ){ constexpr const char *prog = "gpdm"; - struct zip *za; + struct zip *zip; struct zip_file *zf; struct zip_stat sb; char buf[100]; int err; int i, len, fd; - zip_uint64_t sum; + zip_uint64_t sum; // log::info_n("Extracting package contents to '{}'...", dest); log::info_n("Extracting package contents..."); - if((za = zip_open(archive, 0, &err)) == nullptr){ + if((zip = zip_open(archive, 0, &err)) == nullptr){ zip_error_to_str(buf, sizeof(buf), err, errno); log::error("{}: can't open zip archive {}: {}", prog, archive, buf); return 1; } - for(i = 0; i < zip_get_num_entries(za, 0); i++){ - if(zip_stat_index(za, i, 0, &sb) == 0){ + for(i = 0; i < zip_get_num_entries(zip, 0); i++){ + if(zip_stat_index(zip, i, 0, &sb) == 0){ len = strlen(sb.name); if(verbose > 1){ log::print("{}, ", sb.name); @@ -215,7 +215,7 @@ namespace gdpm::utils{ // safe_create_dir(sb.name); std::filesystem::create_directory(path); } else { - zf = zip_fopen_index(za, i, 0); + zf = zip_fopen_index(zip, i, 0); if(!zf){ log::error("extract_zip: zip_fopen_index() failed."); return 100; @@ -248,7 +248,7 @@ namespace gdpm::utils{ } } - if(zip_close(za) == -1){ + if(zip_close(zip) == -1){ log::error("{}: can't close zip archive '{}'", prog, archive); return 1; } diff --git a/tests/basic.cpp b/tests/basic.cpp index ba59b1c..ef071fc 100644 --- a/tests/basic.cpp +++ b/tests/basic.cpp @@ -22,7 +22,7 @@ TEST_SUITE("Command functions"){ using namespace gdpm::package_manager; package::params params = package::params{ - .remote_source = "test" + .remote_source = "test", }; config::context config = config::context{ .username = "", @@ -33,9 +33,10 @@ TEST_SUITE("Command functions"){ .remote_sources = { {"test", "http://godotengine.org/asset-library/api"} }, + .skip_prompt = true, .info { .godot_version = "latest", - } + }, }; package::title_list package_titles{"ResolutionManagerPlugin","godot-hmac", "Godot"};