diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c960f..ce4670d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.2.0 (2024.05.08) + +* Switch to new command line parser [CLI11](https://github.com/CLIUtils/CLI11) +* Suggest workspace name in `up` command if it not provided. Name suggested if current directory matches workspace or + try to up the latest active workspace. +* Added shortcuts for all commands + # 1.1.2 (2024.04.24) * Fix location of state yaml file to `~/.local/share/dcw/state.yml` diff --git a/README.md b/README.md index ec76ae1..4600340 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,17 @@ sudo sh -c 'curl -sSL https://github.com/navrocky/dcw/raw/master/install.sh | ba ## Usage ``` -USAGE: dcw +Usage: dcw [OPTIONS] SUBCOMMAND - add Add named docker compose file as workspace - down Down current workspace - list List registered workspaces - rm Remove workspace by name - up Switch to the workspace with a name +Options: + -h,--help Print this help message and exit -OPTIONAL: - -h, --help Print this help. +Subcommands: + add, a Add named docker compose file as workspace + rm, r Remove workspace + list, l List registered workspaces + up, u Switch to the workspace + down, d Down current workspace ``` ## Examples diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 2923c2e..841f196 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -1,8 +1,8 @@ set(TARGET dcw) -find_package(args-parser REQUIRED) find_package(termcolor REQUIRED) find_package(yaml-cpp REQUIRED) +find_package(CLI11 REQUIRED) add_link_options(-static -static-libgcc -static-libstdc++) @@ -35,7 +35,6 @@ set(SOURCES commands/remove_command.h commands/remove_command.cpp commands/base_command.h - commands/base_command.cpp commands/list_command.h commands/list_command.cpp commands/up_command.h @@ -47,9 +46,9 @@ set(SOURCES add_executable(${TARGET} ${SOURCES}) target_link_libraries(${TARGET} - args-parser::args-parser termcolor::termcolor yaml-cpp::yaml-cpp + CLI11::CLI11 ) include(GNUInstallDirs) diff --git a/cli/commands/add_command.cpp b/cli/commands/add_command.cpp index aceb70b..6d7b70a 100644 --- a/cli/commands/add_command.cpp +++ b/cli/commands/add_command.cpp @@ -1,7 +1,6 @@ #include "add_command.h" #include -#include using namespace std; @@ -10,30 +9,17 @@ AddCommand::AddCommand(const WorkspaceServicePtr& service) { } -void AddCommand::reg(Args::CmdLine& cmdLine) +void AddCommand::reg(CLI::App& app) { - cmdLine - .addCommand("add", Args::ValueOptions::ManyValues, false, "Add named docker compose file as workspace", - string(), string(), "name> alias("a") + ->callback(std::bind(&AddCommand::process, this)); + cmd->add_option("name", name, "Workspace name")->required(); + cmd->add_option("file", file, "Docker compose file")->required(); } -bool AddCommand::process(const Args::CmdLine& cmdLine) +void AddCommand::process() { - if (!cmdLine.isDefined("add")) - return false; - auto vals = cmdLine.values("add"); - if (vals.size() != 2) - throw runtime_error("Expected two arguments for 'add' command"); - auto name = vals[0]; - auto composeFile = vals[1]; - if (name.empty()) - throw runtime_error("Name is empty"); - if (composeFile.empty()) - throw runtime_error("Compose file is empty"); - - auto composeFilePath = std::filesystem::absolute(composeFile); - + auto composeFilePath = std::filesystem::absolute(file); service->add(name, composeFilePath); - return true; } diff --git a/cli/commands/add_command.h b/cli/commands/add_command.h index cf1d16f..b36eb32 100644 --- a/cli/commands/add_command.h +++ b/cli/commands/add_command.h @@ -7,9 +7,12 @@ class AddCommand : public BaseCommand { public: AddCommand(const WorkspaceServicePtr& service); - void reg(Args::CmdLine& cmdLine) override; - bool process(const Args::CmdLine& cmdLine) override; + void reg(CLI::App& app) override; private: + void process(); + WorkspaceServicePtr service; + std::string name; + std::string file; }; diff --git a/cli/commands/base_command.cpp b/cli/commands/base_command.cpp deleted file mode 100644 index 8dde816..0000000 --- a/cli/commands/base_command.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "base_command.h" diff --git a/cli/commands/base_command.h b/cli/commands/base_command.h index eb10c06..7f665b6 100644 --- a/cli/commands/base_command.h +++ b/cli/commands/base_command.h @@ -1,13 +1,12 @@ #pragma once -#include +#include #include class BaseCommand { public: virtual ~BaseCommand() { } - virtual void reg(Args::CmdLine& cmdLine) = 0; - virtual bool process(const Args::CmdLine& cmdLine) = 0; + virtual void reg(CLI::App& app) = 0; }; using CommandPtr = std::shared_ptr; diff --git a/cli/commands/down_command.cpp b/cli/commands/down_command.cpp index 4517961..af426c8 100644 --- a/cli/commands/down_command.cpp +++ b/cli/commands/down_command.cpp @@ -2,21 +2,16 @@ DownCommand::DownCommand(const WorkspaceServicePtr& service) : service(service) + , purge(false) { } -void DownCommand::reg(Args::CmdLine& cmdLine) +void DownCommand::reg(CLI::App& app) { - cmdLine.addCommand("down", Args::ValueOptions::NoValue, false, "Down current workspace") - .addArgWithFlagAndName('p', "purge", false, false, "Purge workspace data (docker volumes)") - .end(); + auto cmd = app.add_subcommand("down", "Down current workspace") + ->alias("d") + ->callback(std::bind(&DownCommand::process, this)); + cmd->add_flag("-p, --purge", purge, "Purge workspace data (docker volumes)"); } -bool DownCommand::process(const Args::CmdLine& cmdLine) -{ - if (!cmdLine.isDefined("down")) - return false; - bool purge = cmdLine.isDefined("-p"); - service->down(purge); - return true; -} +void DownCommand::process() { service->down(purge); } diff --git a/cli/commands/down_command.h b/cli/commands/down_command.h index 033ffd8..e25fc18 100644 --- a/cli/commands/down_command.h +++ b/cli/commands/down_command.h @@ -7,9 +7,10 @@ class DownCommand : public BaseCommand { public: DownCommand(const WorkspaceServicePtr& service); - void reg(Args::CmdLine& cmdLine) override; - bool process(const Args::CmdLine& cmdLine) override; + void reg(CLI::App& app) override; private: + void process(); WorkspaceServicePtr service; + bool purge; }; diff --git a/cli/commands/list_command.cpp b/cli/commands/list_command.cpp index 5f6a3db..0fb4870 100644 --- a/cli/commands/list_command.cpp +++ b/cli/commands/list_command.cpp @@ -2,21 +2,16 @@ ListCommand::ListCommand(const WorkspaceServicePtr& service) : service(service) + , namesOnly(false) { } -void ListCommand::reg(Args::CmdLine& cmdLine) +void ListCommand::reg(CLI::App& app) { - cmdLine.addCommand("list", Args::ValueOptions::NoValue, false, "List registered workspaces") - .addArgWithFlagAndName('n', "names", false, false, "Show names only") - .end(); + auto cmd = app.add_subcommand("list", "List registered workspaces") + ->alias("l") + ->callback(std::bind(&ListCommand::process, this)); + cmd->add_flag("-n, --names", namesOnly, "Show names only"); } -bool ListCommand::process(const Args::CmdLine& cmdLine) -{ - if (!cmdLine.isDefined("list")) - return false; - bool namesOnly = cmdLine.isDefined("-n"); - service->list(namesOnly); - return true; -} +void ListCommand::process() { service->list(namesOnly); } diff --git a/cli/commands/list_command.h b/cli/commands/list_command.h index bb59e18..ff79318 100644 --- a/cli/commands/list_command.h +++ b/cli/commands/list_command.h @@ -7,9 +7,10 @@ class ListCommand : public BaseCommand { public: ListCommand(const WorkspaceServicePtr& service); - void reg(Args::CmdLine& cmdLine) override; - bool process(const Args::CmdLine& cmdLine) override; + void reg(CLI::App& app) override; private: + void process(); WorkspaceServicePtr service; + bool namesOnly; }; diff --git a/cli/commands/remove_command.cpp b/cli/commands/remove_command.cpp index 13db5bc..545e31d 100644 --- a/cli/commands/remove_command.cpp +++ b/cli/commands/remove_command.cpp @@ -1,26 +1,15 @@ #include "remove_command.h" -#include - -using namespace std; - RemoveCommand::RemoveCommand(const WorkspaceServicePtr& service) : service(service) { } -void RemoveCommand::reg(Args::CmdLine& cmdLine) +void RemoveCommand::reg(CLI::App& app) { - cmdLine.addCommand("rm", Args::ValueOptions::OneValue, false, "Remove workspace by name", "", "", "name").end(); + auto cmd + = app.add_subcommand("rm", "Remove workspace")->alias("r")->callback(std::bind(&RemoveCommand::process, this)); + cmd->add_option("name", name, "Name of the workspace"); } -bool RemoveCommand::process(const Args::CmdLine& cmdLine) -{ - if (!cmdLine.isDefined("rm")) - return false; - auto name = cmdLine.value("rm"); - if (name.empty()) - throw runtime_error("Name is empty"); - service->remove(name); - return true; -} +void RemoveCommand::process() { service->remove(name); } diff --git a/cli/commands/remove_command.h b/cli/commands/remove_command.h index bbbeb1d..33d5ae2 100644 --- a/cli/commands/remove_command.h +++ b/cli/commands/remove_command.h @@ -7,9 +7,10 @@ class RemoveCommand : public BaseCommand { public: RemoveCommand(const WorkspaceServicePtr& service); - void reg(Args::CmdLine& cmdLine) override; - bool process(const Args::CmdLine& cmdLine) override; + void reg(CLI::App& app) override; private: + void process(); WorkspaceServicePtr service; + std::string name; }; diff --git a/cli/commands/up_command.cpp b/cli/commands/up_command.cpp index 6ad1e27..747e714 100644 --- a/cli/commands/up_command.cpp +++ b/cli/commands/up_command.cpp @@ -2,25 +2,17 @@ UpCommand::UpCommand(const WorkspaceServicePtr& service) : service(service) + , clean(false) { } -void UpCommand::reg(Args::CmdLine& cmdLine) +void UpCommand::reg(CLI::App& app) { - cmdLine - .addCommand( - "up", Args::ValueOptions::OneValue, false, "Switch to the workspace with a name", "", "default", "name") - .addArgWithFlagAndName('c', "clean", false, false, "Purge workspace data (docker volumes) before start") - .end(); + auto cmd = app.add_subcommand("up", "Switch to the workspace") + ->alias("u") + ->callback(std::bind(&UpCommand::process, this)); + cmd->add_option("name", name, "Workspace name"); + cmd->add_flag("-c, --clean", clean, "Purge workspace data (docker volumes) before start"); } -bool UpCommand::process(const Args::CmdLine& cmdLine) -{ - if (!cmdLine.isDefined("up")) - return false; - auto vals = cmdLine.values("up"); - auto name = vals[0]; - bool clean = cmdLine.isDefined("-c"); - service->up(name, clean); - return true; -} +void UpCommand::process() { service->up(name, clean); } diff --git a/cli/commands/up_command.h b/cli/commands/up_command.h index e1886ae..b80b1f2 100644 --- a/cli/commands/up_command.h +++ b/cli/commands/up_command.h @@ -7,9 +7,11 @@ class UpCommand : public BaseCommand { public: UpCommand(const WorkspaceServicePtr& service); - void reg(Args::CmdLine& cmdLine) override; - bool process(const Args::CmdLine& cmdLine) override; + void reg(CLI::App& app) override; private: + void process(); WorkspaceServicePtr service; + std::string name; + bool clean; }; diff --git a/cli/main.cpp b/cli/main.cpp index 9b9212a..0846ba5 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include "yaml_workspaces_repository.h" @@ -15,27 +15,10 @@ #include "config.h" using namespace std; -using namespace Args; namespace tc = termcolor; using Commands = vector; -void regCommands(const Commands& commands, CmdLine& cmdLine) -{ - for (const auto& cmd : commands) { - cmd->reg(cmdLine); - } -} - -void processCommands(const Commands& commands, const CmdLine& cmdLine) -{ - for (const auto& cmd : commands) { - if (cmd->process(cmdLine)) - return; - } - throw runtime_error("No command processed"); -} - int main(int argc, char** argv) { try { @@ -57,17 +40,15 @@ int main(int argc, char** argv) make_shared(workspaceService), }; - CmdLine cmdLine(argc, argv, CmdLine::CommandIsRequired); - cmdLine.addHelp(true, argv[0], format("Docker Compose Workspace manager (v{})", APP_VERSION)); - regCommands(commands, cmdLine); - cmdLine.parse(); - processCommands(commands, cmdLine); - return 0; - } catch (const HelpHasBeenPrintedException& e) { + CLI::App app { format("Docker Compose Workspace manager (v{})", APP_VERSION) }; + app.require_subcommand(); + + for (const auto& cmd : commands) { + cmd->reg(app); + } + + CLI11_PARSE(app, argc, argv); return 0; - } catch (const BaseException& e) { - cerr << tc::red << "Call error: " << e.desc() << tc::reset << endl; - return 1; } catch (const exception& e) { cerr << tc::red << "Error: " << e.what() << tc::reset << endl; return 1; diff --git a/cli/workspace_service.cpp b/cli/workspace_service.cpp index dbb689b..8c9beeb 100644 --- a/cli/workspace_service.cpp +++ b/cli/workspace_service.cpp @@ -1,5 +1,6 @@ #include "workspace_service.h" +#include #include #include #include @@ -50,7 +51,7 @@ void WorkspaceService::remove(const std::string& name) void WorkspaceService::down(bool purge) { auto currentWpName = stateRepo->getCurrentWorkspace(); - if (!currentWpName.has_value()) + if (!currentWpName.has_value() || currentWpName->empty()) return; auto wp = getWorkspace(currentWpName.value()); composeExecutor->down(wp.composeFile, wp.name, purge); @@ -62,23 +63,40 @@ void WorkspaceService::down(bool purge) void WorkspaceService::up(const std::string& name, bool clean) { - auto wp = getWorkspace(name); auto currentWpName = stateRepo->getCurrentWorkspace(); - if (currentWpName.has_value() && *currentWpName != name) + optional wp; + + if (!name.empty()) { + wp = getWorkspace(name); + } + + if (!wp.has_value()) { + auto curPath = filesystem::current_path(); + wp = findWorkspaceByPath(curPath); + } + + if (!wp.has_value() && currentWpName.has_value()) { + wp = getWorkspace(*currentWpName); + } + + if (!wp.has_value()) + throw runtime_error("Workspace name required"); + + if (currentWpName.has_value() && *currentWpName != wp->name) down(false); if (clean) - composeExecutor->down(wp.composeFile, wp.name, true); + composeExecutor->down(wp->composeFile, wp->name, true); try { - composeExecutor->up(wp.composeFile, wp.name, true); + composeExecutor->up(wp->composeFile, wp->name, true); } catch (...) { - cerr << "❌ " << "Cannot start workspace \"" << tc::bold << name << tc::reset + cerr << "❌ " << "Cannot start workspace \"" << tc::bold << wp->name << tc::reset << "\". Shutting down partially started containers." << endl; - composeExecutor->down(wp.composeFile, wp.name, false); + composeExecutor->down(wp->composeFile, wp->name, false); throw; } - stateRepo->setCurrentWorkspace(name); - cout << "✅ " << "Workspace \"" << tc::bold << name << tc::reset << "\" activated" << endl; + stateRepo->setCurrentWorkspace(wp->name); + cout << "✅ " << "Workspace \"" << tc::bold << wp->name << tc::reset << "\" activated" << endl; } Workspace WorkspaceService::getWorkspace(const std::string& name) const @@ -88,3 +106,13 @@ Workspace WorkspaceService::getWorkspace(const std::string& name) const throw runtime_error(format("Workspace \"{}\" not found", name)); return *wp; } + +std::optional WorkspaceService::findWorkspaceByPath(const std::string& path) const +{ + vector workspaces; + for (const auto& wp : wpRepo->findAll()) { + if (wp.composeFile.starts_with(path)) + workspaces.push_back(wp); + } + return workspaces.size() == 1 ? optional(workspaces[0]) : nullopt; +} diff --git a/cli/workspace_service.h b/cli/workspace_service.h index f598ae6..7f313fe 100644 --- a/cli/workspace_service.h +++ b/cli/workspace_service.h @@ -20,6 +20,7 @@ class WorkspaceService { private: Workspace getWorkspace(const std::string& name) const; + std::optional findWorkspaceByPath(const std::string& path) const; WorkspacesRepositoryPtr wpRepo; StateRepositoryPtr stateRepo; diff --git a/conanfile.txt b/conanfile.txt index 0d378c7..1f5802b 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,8 +1,8 @@ [requires] -args-parser/6.3.3 termcolor/2.1.0 #boost/1.84.0 yaml-cpp/0.8.0 +cli11/2.4.2 [generators] CMakeDeps diff --git a/install.sh b/install.sh index 80d9dca..39f9133 100644 --- a/install.sh +++ b/install.sh @@ -2,7 +2,7 @@ set -e -curl -fL https://github.com/navrocky/dcw/releases/download/1.1.2/dcw -o /usr/local/bin/dcw +curl -fL https://github.com/navrocky/dcw/releases/download/1.2.0/dcw -o /usr/local/bin/dcw chmod +x /usr/local/bin/dcw curl -fL https://github.com/navrocky/dcw/raw/master/completion.bash -o /etc/bash_completion.d/dcw_completion.bash