diff --git a/include/cxxopts.hpp b/include/cxxopts.hpp index a5c67f15..c697384a 100644 --- a/include/cxxopts.hpp +++ b/include/cxxopts.hpp @@ -411,6 +411,16 @@ class Value : public std::enable_shared_from_this virtual bool is_boolean() const = 0; + + virtual std::shared_ptr + completion(const std::string& type) = 0; + + virtual std::shared_ptr + completion_values(const std::vector& values) = 0; + + virtual const std::string& completion_type() const = 0; + + virtual const std::vector& completion_values() const = 0; }; CXXOPTS_DIAGNOSTIC_POP @@ -1260,6 +1270,30 @@ class abstract_value : public Value return std::is_same::value; } + std::shared_ptr + completion(const std::string& type) override + { + m_completion_type = type; + return shared_from_this(); + } + + std::shared_ptr + completion_values(const std::vector& values) override + { + m_completion_values = values; + return shared_from_this(); + } + + const std::string& completion_type() const override + { + return m_completion_type; + } + + const std::vector& completion_values() const override + { + return m_completion_values; + } + const T& get() const { @@ -1279,6 +1313,8 @@ class abstract_value : public Value std::string m_default_value{}; std::string m_implicit_value{}; + std::string m_completion_type{"none"}; + std::vector m_completion_values; }; template @@ -1371,6 +1407,7 @@ class OptionDetails , m_desc(std::move(desc)) , m_value(std::move(val)) , m_count(0) + , m_completion_type("none") { m_hash = std::hash{}(first_long_name() + m_short); } @@ -1438,6 +1475,16 @@ class OptionDetails return m_hash; } + void set_completion_type(const std::string& type) + { + m_completion_type = type; + } + + const std::string& completion_type() const + { + return m_completion_type; + } + private: std::string m_short{}; OptionNames m_long{}; @@ -1446,6 +1493,8 @@ class OptionDetails int m_count; std::size_t m_hash{}; + std::string m_completion_type; + std::vector m_completion_values; }; struct HelpOptionDetails @@ -2050,14 +2099,47 @@ class Options std::vector groups() const; + // Option dependency management + Options& depends(const std::string& option, const std::string& dependency) + { + m_dependencies[option].push_back(dependency); + return *this; + } + + Options& conflicts(const std::string& option, const std::string& conflict) + { + m_conflicts[option].push_back(conflict); + return *this; + } + + Options& required_if(const std::string& option, const std::string& value, const std::string& required_option) + { + m_required_if.emplace_back(option, value, required_option); + return *this; + } + const HelpGroupDetails& group_help(const std::string& group) const; + // Completion script generation + std::string generate_bash_completion() const; + std::string generate_zsh_completion() const; + std::string generate_fish_completion() const; + + private: + void generate_bash_completion_case(std::ostream& script, const std::string& comp_type, const std::vector& comp_values) const; + group_help(const std::string& group) const; + const std::string& program() const { return m_program; } + // Completion script generation + std::string generate_bash_completion() const; + std::string generate_zsh_completion() const; + std::string generate_fish_completion() const; + private: void @@ -2096,6 +2178,13 @@ class Options //mapping from groups to help options std::vector m_group{}; std::map m_help{}; + + // Option dependencies: option -> list of dependencies + std::unordered_map> m_dependencies; + // Option conflicts: option -> list of conflicting options + std::unordered_map> m_conflicts; + // Required if: (option, value, required_option) + std::vector> m_required_if; }; class OptionAdder @@ -2723,6 +2812,9 @@ Options::add_option auto stringDesc = toLocalString(std::move(desc)); auto option = std::make_shared(s, l, stringDesc, value); + // Set completion information + option->set_completion_type(value->completion_type()); + if (!s.empty()) { add_one_option(s, option); @@ -2920,6 +3012,264 @@ Options::group_help(const std::string& group) const return m_help.at(group); } +inline +std::string +Options::generate_bash_completion() const +{ + std::stringstream script; + + script << "# Bash completion script for " << m_program << std::endl; + script << std::endl; + script << "_" << m_program << "() {" << std::endl; + script << " local cur prev opts" << std::endl; + script << " cur=\"$COMP_WORDS[COMP_CWORD]\"" << std::endl; + script << " prev=\"$COMP_WORDS[COMP_CWORD-1]\"" << std::endl; + script << std::endl; + + // Collect all options + script << " opts=()" << std::endl; + for (const auto& opt_pair : *m_options) + { + const auto& option = opt_pair.second; + if (!option->short_name().empty()) + { + script << " opts+=(\"-" << option->short_name() << "\")" << std::endl; + } + for (const auto& long_name : option->long_names()) + { + script << " opts+=(\"--" << long_name << "\")" << std::endl; + } + } + script << std::endl; + + script << " case \"$prev\" in" << std::endl; + + // Handle options that require arguments + for (const auto& opt_pair : *m_options) + { + const auto& option = opt_pair.second; + if (option->value().is_boolean()) + { + continue; // Boolean options don't take arguments + } + + const auto& comp_type = option->completion_type(); + + if (!option->short_name().empty()) + { + script << " -" << option->short_name() << ":" << std::endl; + generate_bash_completion_case(script, comp_type, option->value().completion_values()); + } + + for (const auto& long_name : option->long_names()) + { + script << " --" << long_name << ":" << std::endl; + generate_bash_completion_case(script, comp_type, option->value().completion_values()); + } + } + + script << " *)" << std::endl; + script << " COMPREPLY=( $(compgen -W \"${opts[*]}\" -- \"$cur\") )" << std::endl; + script << " ;;&" << std::endl; + script << " esac" << std::endl; + script << "}" << std::endl; + script << std::endl; + script << "complete -F _" << m_program << " " << m_program << std::endl; + + return script.str(); +} + +inline +void +Options::generate_bash_completion_case(std::ostream& script, const std::string& comp_type, const std::vector& comp_values) const +{ + if (comp_type == "file") + { + script << " COMPREPLY=( $(compgen -f -- \"$cur\") )" << std::endl; + } + else if (comp_type == "directory") + { + script << " COMPREPLY=( $(compgen -d -- \"$cur\") )" << std::endl; + } + else if (comp_type == "enum" && !comp_values.empty()) + { + script << " COMPREPLY=( $(compgen -W \""; + for (const auto& value : comp_values) + { + script << value << " "; + } + script << "\" -- \"$cur\") )" << std::endl; + } + else if (comp_type == "user") + { + script << " COMPREPLY=( $(compgen -u -- \"$cur\") )" << std::endl; + } + else if (comp_type == "host") + { + script << " COMPREPLY=( $(compgen -h -- \"$cur\") )" << std::endl; + } + else + { + // Default: no specific completion + script << " COMPREPLY=( $(compgen -f -- \"$cur\") )" << std::endl; + } + script << " return 0" << std::endl; + script << " ;;&" << std::endl; +} + +inline +std::string +Options::generate_zsh_completion() const +{ + std::stringstream script; + + script << "# Zsh completion script for " << m_program << std::endl; + script << std::endl; + script << "#compdef " << m_program << std::endl; + script << std::endl; + script << "_arguments \\" << std::endl; + + for (const auto& opt_pair : *m_options) + { + const auto& option = opt_pair.second; + const auto& comp_type = option->completion_type(); + const auto& comp_values = option->value().completion_values(); + + std::string desc = toUTF8String(option->description()); + // Escape quotes in description + size_t pos = 0; + while ((pos = desc.find('"', pos)) != std::string::npos) + { + desc.replace(pos, 1, "\\\""); + pos += 2; + } + + if (!option->short_name().empty() && !option->long_names().empty()) + { + script << " '(-" << option->short_name() << " --" << option->long_names().front() << ")'" << std::endl; + script << " '-" << option->short_name() << "[\"" << desc << "\"]'"; + } + else if (!option->short_name().empty()) + { + script << " '-" << option->short_name() << "[\"" << desc << "\"]'"; + } + else if (!option->long_names().empty()) + { + script << " '--" << option->long_names().front() << "[\"" << desc << "\"]'"; + } + + if (!option->value().is_boolean()) + { + script << "'['"; + + if (comp_type == "file") + { + script << "':file:_files'"; + } + else if (comp_type == "directory") + { + script << "':directory:_directories'"; + } + else if (comp_type == "enum" && !comp_values.empty()) + { + script << "': :("; + for (size_t i = 0; i < comp_values.size(); ++i) + { + if (i > 0) + script << " "; + script << comp_values[i]; + } + script << ")'"; + } + else if (comp_type == "user") + { + script << "':user:_users'"; + } + else if (comp_type == "host") + { + script << "':host:_hosts'"; + } + else + { + script << "': :_default'"; + } + + script << "']'"; + } + + script << " \\" << std::endl; + } + + script << " '*::arguments:_default'" << std::endl; + + return script.str(); +} + +inline +std::string +Options::generate_fish_completion() const +{ + std::stringstream script; + + script << "# Fish completion script for " << m_program << std::endl; + script << std::endl; + script << "complete -c " << m_program << " -s h -l help -d \"Show help message\"" << std::endl; + + for (const auto& opt_pair : *m_options) + { + const auto& option = opt_pair.second; + const auto& comp_type = option->completion_type(); + const auto& comp_values = option->value().completion_values(); + + std::string desc = toUTF8String(option->description()); + + std::string cmd = "complete -c " + m_program; + + if (!option->short_name().empty()) + { + cmd += " -s " + option->short_name(); + } + + if (!option->long_names().empty()) + { + cmd += " -l " + option->long_names().front(); + } + + cmd += " -d \"" + desc + "\""; + + if (!option->value().is_boolean()) + { + cmd += " -r"; + + if (comp_type == "file") + { + cmd += " -f"; + } + else if (comp_type == "directory") + { + cmd += " -d"; + } + else if (comp_type == "enum" && !comp_values.empty()) + { + cmd += " -a \"(echo " + comp_values[0]; + for (size_t i = 1; i < comp_values.size(); ++i) + { + cmd += " " + comp_values[i]; + } + cmd += ")\"; + } + } + + cmd += ")"; + fish_script += cmd + "\n"; + } + + fish_script += "\n"; + fish_script += "complete -c " + program() + " -r -a \"(eval __fish_complete_directories)\"\n"; + fish_script += "\n"; + return fish_script; +} + } // namespace cxxopts #endif //CXXOPTS_HPP_INCLUDED