Add template manager for remote templates.

This commit is contained in:
Your Name 2025-05-04 13:52:26 +12:00
parent 0e9466365e
commit 5286ec542a
14 changed files with 282 additions and 193 deletions

View File

@ -61,10 +61,13 @@ bool config::save_config()
std::string dropshell_base = homedir + "/.dropshell";
mConfig["tempfiles"] = dropshell_base + "/tmp";
mConfig["backups"] = dropshell_base + "/backups";
mConfig["template_cache"] = dropshell_base + "/cache";
mConfig["template_registry_urls"] = {
mConfig["template_cache"] = dropshell_base + "/template_cache";
mConfig["template_registry_URLs"] = {
"https://templates.dropshell.app"
};
mConfig["template_local_paths"] = {
dropshell_base + "/local_templates"
};
mConfig["server_definition_paths"] = {
dropshell_base + "/servers"
};
@ -102,7 +105,7 @@ std::string config::get_local_template_cache_path() {
}
std::vector<std::string> config::get_template_registry_urls() {
nlohmann::json template_registry_urls = mConfig["template_registry_urls"];
nlohmann::json template_registry_urls = mConfig["template_registry_URLs"];
std::vector<std::string> urls;
for (auto &url : template_registry_urls) {
urls.push_back(url);
@ -110,11 +113,25 @@ std::vector<std::string> config::get_template_registry_urls() {
return urls;
}
std::vector<std::string> config::get_template_local_paths()
{
nlohmann::json template_local_paths = mConfig["template_local_paths"];
std::vector<std::string> paths;
for (auto &path : template_local_paths) {
if (path.is_string() && !path.empty())
paths.push_back(path);
}
return paths;
}
std::vector<std::string> config::get_local_server_definition_paths() {
nlohmann::json server_definition_paths = mConfig["server_definition_paths"];
std::vector<std::string> paths;
for (auto &path : server_definition_paths) {
paths.push_back(path);
if (path.is_string() && !path.empty())
paths.push_back(path);
else
std::cerr << "Warning: Invalid server definition path: " << path << std::endl;
}
return paths;
}

View File

@ -22,7 +22,9 @@ class config {
std::string get_local_template_cache_path();
std::vector<std::string> get_template_registry_urls();
std::vector<std::string> get_template_local_paths();
std::vector<std::string> get_local_server_definition_paths();
std::string get_template_upload_registry_url();
std::string get_template_upload_registry_token();

View File

@ -1,14 +1,17 @@
#include "server_env_manager.hpp"
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "services.hpp"
#include "contrib/base64.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include <iostream>
#include <memory>
#include <filesystem>
#include <fstream>
namespace dropshell {
@ -17,7 +20,7 @@ server_env_manager::server_env_manager(const std::string& server_name) : mValid(
return;
// Construct the full path to server.env
std::string server_env_path = localfile::server_env(server_name);
std::string server_env_path = localfile::server_json(server_name);
// Check if file exists
if (!std::filesystem::exists(server_env_path)) {
@ -27,11 +30,16 @@ server_env_manager::server_env_manager(const std::string& server_name) : mValid(
try {
// Use envmanager to handle the environment file
envmanager env_manager(server_env_path);
env_manager.load();
// Get all variables
env_manager.get_all_variables_substituted(mVariables);
nlohmann::json server_env_json = nlohmann::json::parse(std::ifstream(server_env_path));
if (server_env_json.empty()) {
std::cerr << "Error: Failed to parse server environment file: " + server_env_path << std::endl;
return;
}
// get the variables from the json
for (const auto& var : server_env_json.items()) {
mVariables[var.key()] = replace_with_environment_variables_like_bash(var.value());
}
// Verify required variables exist
for (const auto& var : {"SSH_HOST", "SSH_USER", "SSH_PORT", "DROPSHELL_DIR"}) {
@ -51,6 +59,25 @@ server_env_manager::server_env_manager(const std::string& server_name) : mValid(
}
}
bool server_env_manager::create_server_env(const std::string &server_env_path, const std::string &SSH_HOST, const std::string &SSH_USER, const std::string &SSH_PORT, const std::string &DROPSHELL_DIR)
{
nlohmann::json server_env_json;
server_env_json["SSH_HOST"] = SSH_HOST;
server_env_json["SSH_USER"] = SSH_USER;
server_env_json["SSH_PORT"] = SSH_PORT;
server_env_json["DROPSHELL_DIR"] = DROPSHELL_DIR;
try {
std::ofstream server_env_file(server_env_path);
server_env_file << server_env_json.dump(4);
server_env_file.close();
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to create server environment file: " + std::string(e.what()) << std::endl;
return false;
}
}
std::string server_env_manager::get_variable(const std::string& name) const {
auto it = mVariables.find(name);
if (it == mVariables.end()) {

View File

@ -8,7 +8,7 @@
#include <string>
#include <map>
#include <memory>
#include "utils/envmanager.hpp"
#include <vector>
namespace dropshell {
// class to hold a command to run on the remote server.
@ -47,6 +47,14 @@ std::string makesafecmd(const std::string& command);
class server_env_manager {
public:
server_env_manager(const std::string& server_name);
static bool create_server_env(
const std::string& server_env_path,
const std::string& SSH_HOST,
const std::string& SSH_USER,
const std::string& SSH_PORT,
const std::string& DROPSHELL_DIR);
std::string get_variable(const std::string& name) const;
// trivial getters.
@ -83,6 +91,8 @@ class server_env_manager {
bool mValid;
};
} // namespace dropshell

View File

@ -19,14 +19,12 @@ namespace dropshell {
std::vector<ServerInfo> get_configured_servers() {
std::vector<ServerInfo> servers;
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
if (local_config_directories.empty())
std::vector<std::string> lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty())
return servers;
for (int i = 0; i < local_config_directories.size(); i++) {
std::string servers_dir = localpath::config_servers(i);
for (auto servers_dir : lsdp) {
if (!servers_dir.empty() && std::filesystem::exists(servers_dir)) {
for (const auto& entry : std::filesystem::directory_iterator(servers_dir)) {
if (std::filesystem::is_directory(entry)) {
std::string server_name = entry.path().filename().string();
@ -52,12 +50,12 @@ std::vector<ServerInfo> get_configured_servers() {
ServerInfo get_server_info(const std::string &server_name)
{
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
if (local_config_directories.empty())
std::vector<std::string> lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty())
return ServerInfo();
for (auto &config_dir : local_config_directories) {
std::string server_dir = config_dir + "/servers/" + server_name;
for (auto &config_dir : lsdp) {
std::string server_dir = config_dir + "/" + server_name;
if (std::filesystem::exists(server_dir)) {
server_env_manager env(server_name);
if (!env.is_valid()) {
@ -177,8 +175,12 @@ void create_server(const std::string &server_name)
}
// 2. create a new directory in the user config directory
std::string config_servers_dir = localpath::config_servers();
std::string server_dir = config_servers_dir + "/" + server_name;
auto lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty() || lsdp[0].empty()) {
std::cerr << "Error: Local server definition path not found - is DropShell initialised?" << std::endl;
return;
}
std::string server_dir = lsdp[0] + "/" + server_name;
std::filesystem::create_directory(server_dir);
// 3. create a template server.env file in the server directory

View File

@ -20,24 +20,25 @@ std::vector<LocalServiceInfo> get_server_services_info(const std::string& server
if (server_name.empty())
return services;
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
if (local_config_directories.empty()) {
std::cerr << "Error: No local config directories found" << std::endl;
std::vector<std::string> local_server_definition_paths = gConfig().get_local_server_definition_paths();
if (local_server_definition_paths.empty()) {
std::cerr << "Error: No local server definition paths found" << std::endl;
std::cerr << "Run 'dropshell init' to initialise DropShell" << std::endl;
return services;
}
for (int i = 0; i < localpath::num_config_directories(); i++) {
std::string serverpath = localpath::config_servers(i);
if (serverpath.empty()) {
for (const auto& server_definition_path : local_server_definition_paths) {
fs::path serverpath = server_definition_path + "/" + server_name;
if (serverpath.string().empty()) {
std::cerr << "Error: Server directory not found: " << serverpath << std::endl;
return services;
}
fs::path server_dir = fs::path(serverpath) / server_name;
if (fs::exists(server_dir)) {
for (const auto& entry : fs::directory_iterator(server_dir)) {
fs::path servicepath = serverpath / "services";
if (fs::exists(servicepath)) {
for (const auto& entry : fs::directory_iterator(servicepath)) {
if (fs::is_directory(entry)) {
auto service = get_service_info(server_name, entry.path().filename().string());
std::string service_name = entry.path().filename().string();
auto service = get_service_info(server_name, service_name);
if (!service.template_name.empty()) {
services.push_back(service);
}
@ -45,7 +46,6 @@ std::vector<LocalServiceInfo> get_server_services_info(const std::string& server
}
} // end of for (int i = 0; i < getNumConfigDirectories(); i++)
}
return services;
}
@ -125,7 +125,7 @@ std::set<std::string> list_backups(const std::string &server_name, const std::st
return backups;
}
std::string backups_dir = localpath::backups_path();
std::string backups_dir = gConfig().get_local_backup_path();
if (backups_dir.empty())
return backups;

View File

@ -16,8 +16,8 @@
namespace dropshell {
bool get_templates(std::vector<template_info>& templates) {
templates.clear();
std::set<std::string> template_source_local::get_template_list() {
std::set<std::string> templates;
// Helper function to add templates from a directory
auto add_templates_from_dir = [&templates](const std::string& dir_path) {
@ -25,68 +25,47 @@
return;
for (const auto& entry : std::filesystem::directory_iterator(dir_path))
if (entry.is_directory()) {
template_info info(entry.path().filename().string(), entry.path().string());
// Check if template with same name already exists
bool duplicate = false;
auto it = std::find_if(templates.begin(), templates.end(),
[&info](const template_info& t) { return t.template_name == info.template_name; });
duplicate = (it!=templates.end());
if (!duplicate)
templates.push_back(info);
}
if (entry.is_directory())
templates.insert(entry.path().filename().string());
};
// add templates from the local config directories
std::vector<std::string> template_config_directories;
get_all_template_config_directories(template_config_directories);
for (const auto& path : template_config_directories) {
add_templates_from_dir(path);
}
return true;
add_templates_from_dir(mLocalPath);
return templates;
}
bool get_template_info(const std::string& template_name, template_info& info) {
// add templates from the local config directories
std::vector<std::string> paths_to_search;
get_all_template_config_directories(paths_to_search);
for (const auto& path : paths_to_search) {
std::filesystem::path full_path = path + "/" + template_name;
if (std::filesystem::exists(full_path))
{
info.template_name = template_name;
info.local_template_path = full_path.string();
return true;
}
}
std::cout << "Warning: Template '" << template_name << "' not found" << std::endl;
return false;
bool template_source_local::has_template(const std::string& template_name) {
return std::filesystem::exists(mLocalPath / template_name);
}
bool template_command_exists(const std::string &template_name, const std::string &command)
{
template_info info;
if (!get_template_info(template_name, info)) {
return false;
std::string template_source_local::template_command_filename(const std::string& template_name, const std::string& command) {
std::vector<std::string> commands = {
command,
command + ".sh"
};
for (const auto& cmd : commands) {
std::filesystem::path path = mLocalPath / template_name / cmd;
if (std::filesystem::exists(path))
return cmd;
}
std::string path = info.local_template_path + "/" + command + ".sh";
return (std::filesystem::exists(path));
return "";
}
template_info template_source_local::get_template_info(const std::string& template_name) {
std::filesystem::path path = mLocalPath / template_name;
if (!std::filesystem::exists(path))
return template_info();
return template_info(
template_name,
mLocalPath.string(),
path
);
}
void list_templates() {
std::vector<template_info> templates;
if (!get_templates(templates)) {
std::cerr << "Error: Failed to get templates" << std::endl;
return;
}
void template_manager::list_templates() {
auto templates = get_template_list();
if (templates.empty()) {
std::cout << "No templates found." << std::endl;
@ -94,44 +73,55 @@
}
std::cout << "Available templates:" << std::endl;
std::cout << std::left << std::setw(20) << "Name" << "Path" << std::endl;
std::cout << std::string(60, '-') << std::endl;
// sort templates alphabetically.
std::sort(templates.begin(), templates.end());
// print templates.
std::cout << std::string(60, '-') << std::endl;
bool first = true;
for (const auto& t : templates) {
std::cout << std::left << std::setw(20) << t.template_name << t.local_template_path << std::endl;
std::cout << (first ? "" : ", ") << t;
first = false;
}
std::cout << std::endl;
std::cout << std::string(60, '-') << std::endl;
}
void create_template(const std::string& template_name) {
void template_manager::create_template(const std::string& template_name) {
// 1. Create a new directory in the user templates directory
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
std::vector<std::string> local_server_definition_paths = gConfig().get_local_server_definition_paths();
if (local_config_directories.empty()) {
std::cerr << "Error: No local config directories found" << std::endl;
if (local_server_definition_paths.empty()) {
std::cerr << "Error: No local server definition paths found" << std::endl;
std::cerr << "Run 'dropshell init' to initialise DropShell" << std::endl;
return;
}
template_info info;
if (get_template_info(template_name, info)) {
std::cerr << "Error: Template '" << template_name << "' already exists at " << info.local_template_path << std::endl;
auto info = get_template_info(template_name);
if (info.is_set()) {
std::cerr << "Error: Template '" << template_name << "' already exists at " << info.locationID() << std::endl;
return;
}
std::string new_template_path = localpath::config_templates() + "/" + template_name;
auto local_template_paths = gConfig().get_template_local_paths();
if (local_template_paths.empty()) {
std::cerr << "Error: No local template paths found" << std::endl;
std::cerr << "Run 'dropshell edit' to add one to the DropShell config" << std::endl;
return;
}
std::string new_template_path = local_template_paths[0] + "/" + template_name;
// Create the new template directory
std::filesystem::create_directories(new_template_path);
// 2. Copy the example template from the system templates directory
std::string system_templates_dir = localpath::system_templates();
std::string example_template_path = system_templates_dir + "/example-nginx";
if (!std::filesystem::exists(example_template_path)) {
std::cerr << "Error: Example template not found at " << example_template_path << std::endl;
auto example_info = gTemplateManager().get_template_info("example-nginx");
if (!example_info.is_set()) {
std::cerr << "Error: Example template not found" << std::endl;
return;
}
std::string example_template_path = example_info.local_template_path();
// Copy all files from example template to new template
for (const auto& entry : std::filesystem::recursive_directory_iterator(example_template_path)) {
std::string relative_path = entry.path().string().substr(example_template_path.length());
@ -144,10 +134,9 @@
}
}
// modify the TEMPLATE=example line in the service.env file to TEMPLATE=<template_name>
// modify the TEMPLATE=example line in the .template_info.env file to TEMPLATE=<template_name>
std::string search_string = "TEMPLATE=";
std::string replacement_line = "TEMPLATE=" + template_name;
// replace the line in the example/service.env file with the replacement line
std::string service_env_path = new_template_path + "/example/.template_info.env";
if (!replace_line_in_file(service_env_path, search_string, replacement_line)) {
std::cerr << "Error: Failed to replace TEMPLATE= line in the .template_info.env file" << std::endl;
@ -175,24 +164,10 @@
std::cout << std::endl;
std::cout << "Template '" << template_name << "' created at " << new_template_path << std::endl;
test_template(new_template_path);
}
bool get_all_template_config_directories(std::vector<std::string> &template_config_directories)
{
template_config_directories.clear();
for (int i = 0; i < localpath::num_config_directories(); i++) {
std::string config_templates_path = localpath::config_templates(i);
if (config_templates_path.empty()) {
std::cerr << "Error: Templates directory not found: " << config_templates_path << std::endl;
return false;
}
template_config_directories.push_back(config_templates_path);
}
template_config_directories.push_back(localpath::system_templates());
return true;
}
bool required_file(std::string path, std::string template_name)
bool template_manager::required_file(std::string path, std::string template_name)
{
if (!std::filesystem::exists(path)) {
std::cerr << "Error: " << path << " file not found in template " << template_name << std::endl;
@ -201,7 +176,7 @@
return true;
}
bool test_template(const std::string &template_path)
bool template_manager::test_template(const std::string &template_path)
{
std::string template_name = std::filesystem::path(template_path).filename().string();
@ -256,4 +231,10 @@
return true;
}
template_manager & gTemplateManager()
{
static template_manager instance;
return instance;
}
} // namespace dropshell

View File

@ -1,39 +1,98 @@
#include <string>
#include <vector>
#include <filesystem>
#include <memory>
#include <set>
#include "utils/json.hpp"
namespace dropshell {
typedef enum template_source_type {
TEMPLATE_SOURCE_TYPE_LOCAL,
TEMPLATE_SOURCE_TYPE_REGISTRY,
TEMPLATE_SOURCE_NOT_SET
} template_source_type;
class template_info {
public:
template_info() {}
template_info(std::string n, std::string p) : template_name(n), local_template_path(p) {}
std::string template_name;
std::string local_template_path;
template_info() : mIsSet(false) {}
template_info(const std::string& template_name, const std::string& location_id, const std::filesystem::path& local_template_path) : mTemplateName(template_name), mLocationID(location_id), mTemplateLocalPath(local_template_path), mIsSet(true) {}
virtual ~template_info() {}
bool is_set() { return mIsSet; }
std::string name() { return mTemplateName; }
std::string locationID() { return mLocationID; }
std::filesystem::path local_template_path() { return mTemplateLocalPath; }
private:
std::string mTemplateName;
std::string mLocationID;
std::filesystem::path mTemplateLocalPath; // source or cache.
bool mIsSet;
};
// templates are stored in multiple locations:
// 1. /opt/dropshell/templates
// 2. CONFIG_DIR/templates
class template_source_interface {
public:
virtual ~template_source_interface() {}
virtual std::set<std::string> get_template_list() = 0;
virtual bool has_template(const std::string& template_name) = 0;
virtual template_info get_template_info(const std::string& template_name) = 0;
virtual std::string template_command_filename(const std::string& template_name,const std::string& command) = 0;
};
class template_source_registry : public template_source_interface {
public:
template_source_registry(std::string URL) : mURL(URL) {}
bool get_templates(std::vector<template_info>& templates);
bool get_template_info(const std::string& template_name, template_info& info);
bool template_command_exists(const std::string& template_name,const std::string& command);
void list_templates();
~template_source_registry() {}
std::set<std::string> get_template_list();
bool has_template(const std::string& template_name);
template_info get_template_info(const std::string& template_name);
std::string template_command_filename(const std::string& template_name,const std::string& command);
private:
std::filesystem::path get_cache_dir();
private:
std::string mURL;
std::vector<nlohmann::json> mTemplates; // cached list.
};
// create a template
// 1. create a new directory in the user templates directory
// 2. copy the example template from the system templates directory into the new directory
// 3. print out the README.txt file in the new template directory, and the path to the new template
void create_template(const std::string& template_name);
class template_source_local : public template_source_interface {
public:
template_source_local(std::string local_path) : mLocalPath(local_path) {}
~template_source_local() {}
std::set<std::string> get_template_list();
bool has_template(const std::string& template_name);
template_info get_template_info(const std::string& template_name);
std::string template_command_filename(const std::string& template_name,const std::string& command);
private:
std::filesystem::path mLocalPath;
};
bool get_all_template_config_directories(std::vector<std::string>& template_config_directories);
class template_manager {
public:
template_manager() : mLoaded(false) {}
~template_manager() {}
std::set<std::string> get_template_list();
bool has_template(const std::string& template_name);
template_info get_template_info(const std::string& template_name);
std::string template_command_filename(const std::string& template_name,const std::string& command);
void create_template(const std::string& template_name);
bool test_template(const std::string& template_path);
void list_templates();
private:
void load_sources();
bool required_file(std::string path, std::string template_name);
private:
bool mLoaded;
std::vector<std::unique_ptr<template_source_interface>> mSources;
};
template_manager & gTemplateManager();
bool test_template(const std::string& template_path);
} // namespace dropshell

View File

@ -22,9 +22,9 @@ namespace localfile {
return std::string();
}
std::string server_env(const std::string &server_name) {
std::string server_json(const std::string &server_name) {
std::string serverpath = localpath::server(server_name);
return (serverpath.empty() ? "" : (fs::path(serverpath) / "server.env").string());
return (serverpath.empty() ? "" : (fs::path(serverpath) / "server.json").string());
}
std::string service_env(const std::string &server_name, const std::string &service_name) {

View File

@ -14,7 +14,7 @@ namespace dropshell {
// server_definition_path
// |-- <server_name>
// |-- server.env
// |-- server.json
// |-- services
// |-- <service_name>
// |-- service.env
@ -42,7 +42,7 @@ namespace dropshell {
namespace localfile {
// ~/.config/dropshell/dropshell.json
std::string dropshell_json();
std::string server_env(const std::string &server_name);
std::string server_json(const std::string &server_name);
std::string service_env(const std::string &server_name, const std::string &service_name);
std::string template_info_env(const std::string &server_name, const std::string &service_name);
} // namespace localfile

View File

@ -72,18 +72,6 @@ void envmanager::get_all_variables(std::map<std::string, std::string>& variables
variables = m_variables;
}
std::string envmanager::get_variable_substituted(std::string key) const {
std::string value = get_variable(key);
return expand_patterns(value);
}
void envmanager::get_all_variables_substituted(std::map<std::string, std::string>& variables) const {
variables.clear();
for (const auto& pair : m_variables) {
variables[pair.first] = expand_patterns(pair.second);
}
}
void envmanager::add_variables(std::map<std::string, std::string> variables) {
for (auto& pair : variables) {
set_variable(pair.first, pair.second);
@ -98,26 +86,4 @@ void envmanager::clear_variables() {
m_variables.clear();
}
std::string envmanager::expand_patterns(std::string str) const {
// Combined regex pattern for both ${var} and $var formats
std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))");
std::string result = str;
std::smatch match;
while (std::regex_search(result, match, var_pattern)) {
// match[1] will contain capture from ${var} format
// match[2] will contain capture from $var format
std::string var_name = match[1].matched ? match[1].str() : match[2].str();
// Get value from system environment variables
const char* env_value = std::getenv(var_name.c_str());
std::string value = env_value ? env_value : "";
result = result.replace(match.position(), match.length(), value);
}
// dequote the result
return result;
}
} // namespace dropshell

View File

@ -25,20 +25,12 @@ class envmanager {
std::string get_variable(std::string key) const;
void get_all_variables(std::map<std::string, std::string>& variables) const;
// get variables, but replace patterns ${var} and $var with the actual environment variable in the returned string.
// trim whitespace from the values.
std::string get_variable_substituted(std::string key) const;
void get_all_variables_substituted(std::map<std::string, std::string>& variables) const;
// add variables to the environment files.
// trim whitespace from the values.
void add_variables(std::map<std::string, std::string> variables);
void set_variable(std::string key, std::string value);
void clear_variables();
private:
std::string expand_patterns(std::string str) const;
private:
std::string m_path;
std::map<std::string, std::string> m_variables;

View File

@ -5,6 +5,7 @@
#include <vector>
#include <algorithm>
#include <filesystem>
#include <regex>
namespace dropshell {
void maketitle(const std::string& title) {
@ -268,4 +269,32 @@ std::vector<std::string> split(const std::string& str, const std::string& delimi
return tokens;
}
std::string replace_with_environment_variables_like_bash(std::string str) {
// Combined regex pattern for both ${var} and $var formats
std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))");
std::string result = str;
std::smatch match;
while (std::regex_search(result, match, var_pattern)) {
// match[1] will contain capture from ${var} format
// match[2] will contain capture from $var format
std::string var_name = match[1].matched ? match[1].str() : match[2].str();
// Get value from system environment variables
const char* env_value = std::getenv(var_name.c_str());
std::string value = env_value ? env_value : "";
result = result.replace(match.position(), match.length(), value);
}
// dequote the result
return result;
}
std::string requote(std::string str) {
return quote(trim(dequote(trim(str))));
}
} // namespace dropshell

View File

@ -20,6 +20,8 @@ std::string trim(std::string str);
std::string dequote(std::string str);
std::string quote(std::string str);
std::string halfquote(std::string str);
std::string requote(std::string str);
std::string multi2string(std::vector<std::string> values);
std::vector<std::string> string2multi(std::string values);
std::vector<std::string> split(const std::string& str, const std::string& delimiter);
@ -34,4 +36,6 @@ void ensure_directories_exist(std::vector<std::string> directories);
std::vector<int> search(const std::string &pat, const std::string &txt);
int count_substring(const std::string &substring, const std::string &text);
std::string replace_with_environment_variables_like_bash(std::string str);
} // namespace dropshell