diff options
author | Devtools Arcadia <arcadia-devtools@yandex-team.ru> | 2022-02-07 18:08:42 +0300 |
---|---|---|
committer | Devtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net> | 2022-02-07 18:08:42 +0300 |
commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/cpp/getopt/small | |
download | ydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/getopt/small')
35 files changed, 7130 insertions, 0 deletions
diff --git a/library/cpp/getopt/small/completer.cpp b/library/cpp/getopt/small/completer.cpp new file mode 100644 index 0000000000..3fff684adb --- /dev/null +++ b/library/cpp/getopt/small/completer.cpp @@ -0,0 +1,367 @@ +#include "completer.h" + +#include "completion_generator.h" + +#include <util/string/cast.h> +#include <util/generic/fwd.h> + +using NLastGetopt::NEscaping::Q; +using NLastGetopt::NEscaping::QQ; +using NLastGetopt::NEscaping::C; +using NLastGetopt::NEscaping::CC; +using NLastGetopt::NEscaping::S; +using NLastGetopt::NEscaping::SS; +using NLastGetopt::NEscaping::B; +using NLastGetopt::NEscaping::BB; + +namespace NLastGetopt::NComp { +#define L out.Line() +#define I auto Y_GENERATE_UNIQUE_ID(indent) = out.Indent() + + TCompleterManager::TCompleterManager(TStringBuf command) + : Command_(command) + , Id_(0) + { + } + + TStringBuf TCompleterManager::GetCompleterID(const ICompleter* completer) { + return Queue_.emplace_back(TStringBuilder() << "_" << Command_ << "__completer_" << ++Id_, completer).first; + } + + void TCompleterManager::GenerateZsh(TFormattedOutput& out) { + while (!Queue_.empty()) { + auto[name, completer] = Queue_.back(); + Queue_.pop_back(); + + L << "(( $+functions[" << name << "] )) ||"; + L << name << "() {"; + { + I; + completer->GenerateZsh(out, *this); + } + L << "}"; + L; + } + } + + class TAlternativeCompleter: public ICompleter { + public: + TAlternativeCompleter(TVector<TAlternative> alternatives) + : Alternatives_(std::move(alternatives)) + { + } + + void GenerateBash(TFormattedOutput& out) const override { + for (auto& alternative: Alternatives_) { + if (alternative.Completer != nullptr) { + alternative.Completer->GenerateBash(out); + } + } + } + + TStringBuf GenerateZshAction(TCompleterManager& manager) const override { + return manager.GetCompleterID(this); + } + + void GenerateZsh(TFormattedOutput& out, TCompleterManager& manager) const override { + // We should use '_alternative' here, but it doesn't process escape codes in group descriptions, + // so we dispatch alternatives ourselves. + + L << "local expl action"; + + size_t i = 0; + for (auto& alternative: Alternatives_) { + auto tag = "alt-" + ToString(++i); + auto action = alternative.Completer ? alternative.Completer->GenerateZshAction(manager) : TStringBuf(); + + L; + + if (action.empty()) { + L << "_message -e " << SS(tag) << " " << SS(alternative.Description); + } else if (action.StartsWith("((") && action.EndsWith("))")) { + L << "action=" << action.substr(1, action.size() - 2); + L << "_describe -t " << SS(tag) << " " << SS(alternative.Description) << " action -M 'r:|[_-]=* r:|=*'"; + } else if (action.StartsWith("(") && action.EndsWith(")")) { + L << "action=" << action << ""; + L << "_describe -t " << SS(tag) << " " << SS(alternative.Description) << " action -M 'r:|[_-]=* r:|=*'"; + } else if (action.StartsWith(' ')) { + L << action.substr(1); + } else { + L << "_description " << SS(tag) << " expl " << SS(alternative.Description); + TStringBuf word, args; + action.Split(' ', word, args); + L << word << " \"${expl[@]}\" " << args; + } + } + } + + private: + TVector<TAlternative> Alternatives_; + }; + + ICompleterPtr Alternative(TVector<TAlternative> alternatives) { + return MakeSimpleShared<TAlternativeCompleter>(std::move(alternatives)); + } + + class TSimpleCompleter: public ICompleter { + public: + TSimpleCompleter(TString bashCode, TString action) + : BashCode(std::move(bashCode)) + , Action(std::move(action)) + { + } + + void GenerateBash(TFormattedOutput& out) const override { + if (BashCode) { + L << BashCode; + } + } + + TStringBuf GenerateZshAction(TCompleterManager&) const override { + return Action; + } + + void GenerateZsh(TFormattedOutput&, TCompleterManager&) const override { + Y_FAIL("unreachable"); + } + + private: + TString BashCode; + TString Action; + }; + + ICompleterPtr Choice(TVector<TChoice> choices) { + auto bash = TStringBuilder() << "COMPREPLY+=( $(compgen -W '"; + TStringBuf sep = ""; + for (auto& choice : choices) { + bash << sep << B(choice.Choice); + sep = " "; + } + bash << "' -- ${cur}) )"; + + auto action = TStringBuilder(); + action << "(("; + for (auto& choice: choices) { + action << " " << SS(choice.Choice); + if (choice.Description) {{ + action << ":" << SS(choice.Description); + }} + } + action << "))"; + return MakeSimpleShared<TSimpleCompleter>(bash, action); + } + + TString Compgen(TStringBuf flags) { + return TStringBuilder() << "COMPREPLY+=( $(compgen " << flags << " -- ${cur}) )"; + } + + ICompleterPtr Default() { + return MakeSimpleShared<TSimpleCompleter>("", "_default"); + } + + ICompleterPtr File(TString pattern) { + if (pattern) { + pattern = " -g " + SS(pattern); + } + return MakeSimpleShared<TSimpleCompleter>("", "_files" + pattern); + } + + ICompleterPtr Directory() { + return MakeSimpleShared<TSimpleCompleter>("", "_files -/"); + } + + ICompleterPtr Host() { + return MakeSimpleShared<TSimpleCompleter>(Compgen("-A hostname"), "_hosts"); + } + + ICompleterPtr Pid() { + return MakeSimpleShared<TSimpleCompleter>("", "_pids"); + } + + ICompleterPtr User() { + return MakeSimpleShared<TSimpleCompleter>(Compgen("-A user"), "_users"); + } + + ICompleterPtr Group() { + // For some reason, OSX freezes when trying to perform completion for groups. + // You can try removing this ifdef and debugging it, but be prepared to force-shutdown your machine + // (and possibly reinstall OSX if force-shutdown breaks anything). +#ifdef _darwin_ + return MakeSimpleShared<TSimpleCompleter>("", ""); +#else + return MakeSimpleShared<TSimpleCompleter>(Compgen("-A group"), "_groups"); +#endif + } + + ICompleterPtr Url() { + return MakeSimpleShared<TSimpleCompleter>("", "_urls"); + } + + ICompleterPtr Tty() { + return MakeSimpleShared<TSimpleCompleter>("", "_ttys"); + } + + ICompleterPtr NetInterface() { + return MakeSimpleShared<TSimpleCompleter>("", "_net_interfaces"); + } + + ICompleterPtr TimeZone() { + return MakeSimpleShared<TSimpleCompleter>("", "_time_zone"); + } + + ICompleterPtr Signal() { + return MakeSimpleShared<TSimpleCompleter>(Compgen("-A signal"), "_signals"); + } + + ICompleterPtr Domain() { + return MakeSimpleShared<TSimpleCompleter>("", "_domains"); + } + + namespace { + TCustomCompleter* Head = nullptr; + TStringBuf SpecialFlag = "---CUSTOM-COMPLETION---"; + } + + void TCustomCompleter::FireCustomCompleter(int argc, const char** argv) { + if (!argc) { + return; + } + + for (int i = 1; i < argc - 4; ++i) { + if (SpecialFlag == argv[i]) { + auto name = TStringBuf(argv[i + 1]); + auto curIdx = FromString<int>(argv[i + 2]); + auto prefix = TStringBuf(argv[i + 3]); + auto suffix = TStringBuf(argv[i + 4]); + + auto cur = TStringBuf(); + if (0 <= curIdx && curIdx < i) { + cur = TStringBuf(argv[curIdx]); + } + if (cur && !prefix && !suffix) { + prefix = cur; // bash does not send prefix and suffix + } + + auto head = Head; + while (head) { + if (head->GetUniqueName() == name) { + head->GenerateCompletions(i, argv, curIdx, cur, prefix, suffix); + } + head = head->Next_; + } + + exit(0); + } + } + } + + void TCustomCompleter::RegisterCustomCompleter(TCustomCompleter* completer) noexcept { + Y_VERIFY(completer); + completer->Next_ = Head; + Head = completer; + } + + void TCustomCompleter::AddCompletion(TStringBuf completion) { + Cout << completion << Endl; // this was easy =) + // TODO: support option descriptions and messages + } + + void TMultipartCustomCompleter::GenerateCompletions(int argc, const char** argv, int curIdx, TStringBuf cur, TStringBuf prefix, TStringBuf suffix) { + auto root = TStringBuf(); + if (prefix.Contains(Sep_)) { + auto tmp = TStringBuf(); + prefix.RSplit(Sep_, root, tmp); + } + + if (root) { + Cout << root << Sep_ << Endl; + } else { + Cout << Endl; + } + + Cout << Sep_ << Endl; + + GenerateCompletionParts(argc, argv, curIdx, cur, prefix, suffix, root); + } + + class TLaunchSelf: public ICompleter { + public: + TLaunchSelf(TCustomCompleter* completer) + : Completer_(completer) + { + } + + void GenerateBash(TFormattedOutput& out) const override { + L << "IFS=$'\\n'"; + L << "COMPREPLY+=( $(compgen -W \"$(${words[@]} " << SpecialFlag << " " << Completer_->GetUniqueName() << " \"${cword}\" \"\" \"\" 2> /dev/null)\" -- ${cur}) )"; + L << "IFS=$' \\t\\n'"; + } + + TStringBuf GenerateZshAction(TCompleterManager& manager) const override { + return manager.GetCompleterID(this); + } + + void GenerateZsh(TFormattedOutput& out, TCompleterManager&) const override { + L << "compadd ${@} ${expl[@]} -- \"${(@f)$(${words_orig[@]} " << SpecialFlag << " " << Completer_->GetUniqueName() << " \"${current_orig}\" \"${prefix_orig}\" \"${suffix_orig}\" 2> /dev/null)}\""; + } + + private: + TCustomCompleter* Completer_; + }; + + ICompleterPtr LaunchSelf(TCustomCompleter& completer) { + return MakeSimpleShared<TLaunchSelf>(&completer); + } + + class TLaunchSelfMultiPart: public ICompleter { + public: + TLaunchSelfMultiPart(TCustomCompleter* completer) + : Completer_(completer) + { + } + + void GenerateBash(TFormattedOutput& out) const override { + L << "IFS=$'\\n'"; + L << "items=( $(${words[@]} " << SpecialFlag << " " << Completer_->GetUniqueName() << " \"${cword}\" \"\" \"\" 2> /dev/null) )"; + L << "candidates=$(compgen -W \"${items[*]:1}\" -- \"$cur\")"; + L << "COMPREPLY+=( $candidates )"; + L << "[[ $candidates == *\"${items[1]}\" ]] && need_space=\"\""; + L << "IFS=$' \\t\\n'"; + } + + TStringBuf GenerateZshAction(TCompleterManager& manager) const override { + return manager.GetCompleterID(this); + } + + void GenerateZsh(TFormattedOutput& out, TCompleterManager&) const override { + L << "local items=( \"${(@f)$(${words_orig[@]} " << SpecialFlag << " " << Completer_->GetUniqueName() << " \"${current_orig}\" \"${prefix_orig}\" \"${suffix_orig}\" 2> /dev/null)}\" )"; + L; + L << "local rempat=${items[1]}"; + L << "shift items"; + L; + L << "local sep=${items[1]}"; + L << "shift items"; + L; + L << "local files=( ${items:#*\"${sep}\"} )"; + L << "local filenames=( ${files#\"${rempat}\"} )"; + L << "local dirs=( ${(M)items:#*\"${sep}\"} )"; + L << "local dirnames=( ${dirs#\"${rempat}\"} )"; + L; + L << "local need_suf"; + L << "compset -S \"${sep}*\" || need_suf=\"1\""; + L; + L << "compadd ${@} ${expl[@]} -d filenames -- ${(q)files}"; + L << "compadd ${@} ${expl[@]} ${need_suf:+-S\"${sep}\"} -q -d dirnames -- ${(q)dirs%\"${sep}\"}"; + } + + private: + TCustomCompleter* Completer_; + }; + + ICompleterPtr LaunchSelfMultiPart(TCustomCompleter& completer) { + return MakeSimpleShared<TLaunchSelfMultiPart>(&completer); + } + +#undef I +#undef L +} diff --git a/library/cpp/getopt/small/completer.h b/library/cpp/getopt/small/completer.h new file mode 100644 index 0000000000..4136f13add --- /dev/null +++ b/library/cpp/getopt/small/completer.h @@ -0,0 +1,306 @@ +#pragma once + +#include "formatted_output.h" + +#include <util/generic/strbuf.h> +#include <util/generic/hash.h> + +#include <utility> +#include <util/generic/fwd.h> + +namespace NLastGetopt::NComp { + class ICompleter; + + class TCompleterManager { + public: + TCompleterManager(TStringBuf command); + + /// Register new completer and get its function name. + TStringBuf GetCompleterID(const ICompleter* completer); + + /// Generate zsh code for all collected completers. + void GenerateZsh(TFormattedOutput& out); + + private: + TStringBuf Command_; + size_t Id_; + TVector<std::pair<TString, const ICompleter*>> Queue_; + }; + + class ICompleter { + public: + virtual ~ICompleter() = default; + + public: + /// Generate arbitrary bash code that modifies `COMPREPLY`. + virtual void GenerateBash(TFormattedOutput& out) const = 0; + + /// Generate action that will be used with `_arguments`. If this completer requires a separate function, + /// register it in the given manager and return function name assigned by manager. + /// Supported forms are '()', '(items...)', '((items...))', 'command ...' and ' command ...'. + /// Other forms, such as '{eval-string}', '->state', '=action' are not supported. + virtual TStringBuf GenerateZshAction(TCompleterManager& manager) const = 0; + + /// Generate body of a zsh function (if Action points to a custom function). + virtual void GenerateZsh(TFormattedOutput& out, TCompleterManager& manager) const = 0; + }; + + using ICompleterPtr = TSimpleSharedPtr<ICompleter>; + + /// Generate default completions. + /// Output of this completer depends on shell settings. + /// Usually ut generates file paths. + ICompleterPtr Default(); + + struct TAlternative { + /// Description for this group of completions. Leave empty to use completer's default description. + TString Description; + + /// Completer that generates values + ICompleterPtr Completer; + + TAlternative(ICompleterPtr completer) + : Description("") + , Completer(std::move(completer)) + { + } + + TAlternative(TString description, ICompleterPtr completer) + : Description(std::move(description)) + , Completer(std::move(completer)) + { + } + }; + + /// Run multiple completers and unite their output. + /// Each completer's output placed in a separate group with its own description. + ICompleterPtr Alternative(TVector<TAlternative> alternatives); + + struct TChoice { + /// Option value. + TString Choice; + + /// Description for a value. + TString Description = ""; + + TChoice(TString choice) + : Choice(std::move(choice)) + { + } + + TChoice(TString choice, TString description) + : Choice(std::move(choice)) + , Description(std::move(description)) + { + } + }; + + /// Complete items from a predefined list of choices. + ICompleterPtr Choice(TVector<TChoice> choices); + + /// Complete files and directories. May filter results by pattern, e.g. `*.txt`. + ICompleterPtr File(TString pattern= ""); + + /// Complete directories. + ICompleterPtr Directory(); + + /// Complete hosts. + ICompleterPtr Host(); + + /// Complete process IDs. + ICompleterPtr Pid(); + + /// Complete users that're found in the system. + ICompleterPtr User(); + + /// Complete user groups that're found in the system. + /// N: for some reason, + ICompleterPtr Group(); + + /// Complete URLs. + ICompleterPtr Url(); + + /// Complete TTY interfaces. + ICompleterPtr Tty(); + + /// Complete network interfaces. + ICompleterPtr NetInterface(); + + /// Complete timezone identifiers. + ICompleterPtr TimeZone(); + + /// Complete unix signal identifiers, e.g. `ABRT` or `KILL`. + ICompleterPtr Signal(); + + /// Complete domains. + ICompleterPtr Domain(); + + /// Custom completer. See `LaunchSelf` below. + class TCustomCompleter { + public: + static void FireCustomCompleter(int argc, const char** argv); + static void RegisterCustomCompleter(TCustomCompleter* completer) noexcept; + + struct TReg { + TReg(TCustomCompleter* completer) noexcept { + TCustomCompleter::RegisterCustomCompleter(completer); + } + }; + + public: + virtual ~TCustomCompleter() = default; + + public: + /// @param argc total number of command line arguments. + /// @param argv array of command line arguments. + /// @param curIdx index of the currently completed argument, may be equal to `argc` if cursor is at the end + /// of line. + /// @param cur currently completed argument. + /// @param prefix part of the currently completed argument before the cursor. + /// @param suffix part of the currently completed argument after the cursor. + virtual void GenerateCompletions(int argc, const char** argv, int curIdx, TStringBuf cur, TStringBuf prefix, TStringBuf suffix) = 0; + virtual TStringBuf GetUniqueName() const = 0; + + protected: + void AddCompletion(TStringBuf completion); + + private: + TCustomCompleter* Next_ = nullptr; + }; + + /// Custom completer for objects that consist of multiple parts split by a common separator, such as file paths. + class TMultipartCustomCompleter: public TCustomCompleter { + public: + TMultipartCustomCompleter(TStringBuf sep) + : Sep_(sep) + { + Y_VERIFY(!Sep_.empty()); + } + + public: + void GenerateCompletions(int argc, const char** argv, int curIdx, TStringBuf cur, TStringBuf prefix, TStringBuf suffix) final; + + public: + /// @param argc same as in `GenerateCompletions`. + /// @param argv same as in `GenerateCompletions`. + /// @param curIdx same as in `GenerateCompletions`. + /// @param cur same as in `GenerateCompletions`. + /// @param prefix same as in `GenerateCompletions`. + /// @param suffix same as in `GenerateCompletions`. + /// @param root part of the currently completed argument before the last separator. + virtual void GenerateCompletionParts(int argc, const char** argv, int curIdx, TStringBuf cur, TStringBuf prefix, TStringBuf suffix, TStringBuf root) = 0; + + protected: + TStringBuf Sep_; + }; + +#define Y_COMPLETER(N) \ +class T##N: public ::NLastGetopt::NComp::TCustomCompleter { \ + public: \ + void GenerateCompletions(int, const char**, int, TStringBuf, TStringBuf, TStringBuf) override; \ + TStringBuf GetUniqueName() const override { return #N; } \ + }; \ + T##N N = T##N(); \ + ::NLastGetopt::NComp::TCustomCompleter::TReg _Reg_##N = &N; \ + void T##N::GenerateCompletions( \ + Y_DECLARE_UNUSED int argc, \ + Y_DECLARE_UNUSED const char** argv, \ + Y_DECLARE_UNUSED int curIdx, \ + Y_DECLARE_UNUSED TStringBuf cur, \ + Y_DECLARE_UNUSED TStringBuf prefix, \ + Y_DECLARE_UNUSED TStringBuf suffix) + +#define Y_MULTIPART_COMPLETER(N, SEP) \ +class T##N: public ::NLastGetopt::NComp::TMultipartCustomCompleter { \ + public: \ + T##N() : ::NLastGetopt::NComp::TMultipartCustomCompleter(SEP) {} \ + void GenerateCompletionParts(int, const char**, int, TStringBuf, TStringBuf, TStringBuf, TStringBuf) override; \ + TStringBuf GetUniqueName() const override { return #N; } \ + }; \ + T##N N = T##N(); \ + ::NLastGetopt::NComp::TCustomCompleter::TReg _Reg_##N = &N; \ + void T##N::GenerateCompletionParts( \ + Y_DECLARE_UNUSED int argc, \ + Y_DECLARE_UNUSED const char** argv, \ + Y_DECLARE_UNUSED int curIdx, \ + Y_DECLARE_UNUSED TStringBuf cur, \ + Y_DECLARE_UNUSED TStringBuf prefix, \ + Y_DECLARE_UNUSED TStringBuf suffix, \ + Y_DECLARE_UNUSED TStringBuf root) + + /// Launches this binary with a specially formed flags and retrieves completions from stdout. + /// + /// Your application must be set up in a certain way for this to work. + /// + /// First, create a custom completer: + /// + /// ``` + /// Y_COMPLETER(SomeUniqueName) { + /// AddCompletion("foo"); + /// AddCompletion("bar"); + /// AddCompletion("baz"); + /// } + /// ``` + /// + /// Then, use it with this function. + /// + /// On completion attempt, completer will call your binary with some special arguments. + /// + /// In your main, before any other logic, call `TCustomCompleter::FireCustomCompleter`. This function will + /// check for said special arguments and invoke the right completer: + /// + /// ``` + /// int main(int argc, const char** argv) { + /// TCustomCompleter::FireCustomCompleter(argc, argv); + /// ... + /// } + /// ``` + ICompleterPtr LaunchSelf(TCustomCompleter& completer); + + /// Launches this binary with a specially formed flags and retrieves completions from stdout. + /// + /// Your application must be set up in a certain way for this to work. See `LaunchSelf` for more info. + /// + /// Multipart completion is designed for objects that consist of multiple parts split by a common separator. + /// It is ideal for completing remote file paths, for example. + /// + /// Multipart completers format stdout in the following way. + /// + /// On the first line, they print a common prefix that should be stripped from all completions. For example, + /// if you complete paths, this prefix would be a common directory. + /// + /// On the second line, they print a separator. If some completion ends with this separator, shell will not add + /// a whitespace after the item is completed. For example, if you complete paths, the separator would be a slash. + /// + /// On the following lines, they print completions, as formed by the `AddCompletion` function. + /// + /// For example, if a user invokes completion like this: + /// + /// ``` + /// $ program //home/logs/<tab><tab> + /// ``` + /// + /// The stdout might look like this: + /// + /// ``` + /// //home/logs/ + /// / + /// //home/logs/access-log + /// //home/logs/redir-log + /// //home/logs/blockstat-log + /// ``` + /// + /// Then autocompletion will look like this: + /// + /// ``` + /// $ program //home/logs/<tab><tab> + /// -- yt path -- + /// access-log redir-log blockstat-log + /// ``` + /// + /// Note: stdout lines with completion suggestions *must* be formatted by the `AddCompletion` function + /// because their format may change in the future. + /// + /// Note: we recommend using `Y_MULTIPART_COMPLETER` because it will handle all stdout printing for you. + ICompleterPtr LaunchSelfMultiPart(TCustomCompleter& completer); +} diff --git a/library/cpp/getopt/small/completer_command.cpp b/library/cpp/getopt/small/completer_command.cpp new file mode 100644 index 0000000000..5e593eec7e --- /dev/null +++ b/library/cpp/getopt/small/completer_command.cpp @@ -0,0 +1,165 @@ +#include "completer_command.h" + +#include "completion_generator.h" +#include "last_getopt.h" +#include "wrap.h" + +#include <library/cpp/colorizer/colors.h> + +#include <util/string/subst.h> + +namespace NLastGetopt { + TString MakeInfo(TStringBuf command, TStringBuf flag) { + TString info = ( + "This command generates shell script with completion function and prints it to `stdout`, " + "allowing one to re-direct the output to the file of their choosing. " + "Where you place the file will depend on which shell and operating system you are using." + "\n" + "\n" + "\n" + "{B}BASH (Linux){R}:" + "\n" + "\n" + "For system-wide commands, completion files are stored in `/etc/bash_completion.d/`. " + "For user commands, they are stored in `~/.local/share/bash-completion/completions`. " + "So, pipe output of this script to a file in one of these directories:" + "\n" + "\n" + " $ mkdir -p ~/.local/share/bash-completion/completions" + "\n" + " $ {command} {completion} bash >" + "\n" + " ~/.local/share/bash-completion/completions/{command}" + "\n" + "\n" + "You'll need to restart your shell for changes to take effect." + "\n" + "\n" + "\n" + "{B}BASH (OSX){R}:" + "\n" + "\n" + "You'll need `bash-completion` (or `bash-completion@2` if you're using non-default, newer bash) " + "homebrew formula. Completion files are stored in `/usr/local/etc/bash_completion.d`:" + "\n" + "\n" + " $ mkdir -p $(brew --prefix)/etc/bash_completion.d" + "\n" + " $ {command} {completion} bash >" + "\n" + " $(brew --prefix)/etc/bash_completion.d/{command}" + "\n" + "\n" + "Alternatively, just source the script in your `~/.bash_profile`." + "\n" + "\n" + "You'll need to restart your shell for changes to take effect." + "\n" + "\n" + "\n" + "{B}ZSH{R}:" + "\n" + "\n" + "Zsh looks for completion scripts in any directory listed in `$fpath` variable. We recommend placing " + "completions to `~/.zfunc`:" + "\n" + "\n" + " $ mkdir -p ~/.zfunc" + "\n" + " $ {command} {completion} zsh > ~/.zfunc/_{command}" + "\n" + "\n" + "Add the following lines to your `.zshrc` just before `compinit`:" + "\n" + "\n" + " fpath+=~/.zfunc" + "\n" + "\n" + "You'll need to restart your shell for changes to take effect."); + SubstGlobal(info, "{command}", command); + SubstGlobal(info, "{completion}", flag); + SubstGlobal(info, "{B}", NColorizer::StdErr().LightDefault()); + SubstGlobal(info, "{R}", NColorizer::StdErr().Reset()); + return info; + } + + NComp::ICompleterPtr ShellChoiceCompleter() { + return NComp::Choice({{"zsh"}, {"bash"}}); + } + + TOpt MakeCompletionOpt(const TOpts* opts, TString command, TString name) { + return TOpt() + .AddLongName(name) + .Help("generate tab completion script for zsh or bash") + .CompletionHelp("generate tab completion script") + .OptionalArgument("shell-syntax") + .CompletionArgHelp("shell syntax for completion script") + .IfPresentDisableCompletion() + .Completer(ShellChoiceCompleter()) + .Handler1T<TString>([opts, command, name](TStringBuf shell) { + if (shell.empty()) { + Cerr << Wrap(80, MakeInfo(command, "--" + name)) << Endl; + } else if (shell == "bash") { + TBashCompletionGenerator(opts).Generate(command, Cout); + } else if (shell == "zsh") { + TZshCompletionGenerator(opts).Generate(command, Cout); + } else { + Cerr << "Unknown shell name " << TString{shell}.Quote() << Endl; + exit(1); + } + exit(0); + }); + } + + class TCompleterMode: public TMainClassArgs { + public: + TCompleterMode(const TModChooser* modChooser, TString command, TString modName) + : Command_(std::move(command)) + , Modes_(modChooser) + , ModName_(std::move(modName)) + { + } + + protected: + void RegisterOptions(NLastGetopt::TOpts& opts) override { + TMainClassArgs::RegisterOptions(opts); + + opts.SetTitle("Generate tab completion scripts for zsh or bash"); + + opts.AddSection("Description", MakeInfo(Command_, ModName_)); + + opts.SetFreeArgsNum(1); + opts.GetFreeArgSpec(0) + .Title("<shell-syntax>") + .Help("shell syntax for completion script (bash or zsh)") + .CompletionArgHelp("shell syntax for completion script") + .Completer(ShellChoiceCompleter()); + } + + int DoRun(NLastGetopt::TOptsParseResult&& parsedOptions) override { + auto arg = parsedOptions.GetFreeArgs()[0]; + arg.to_lower(); + + if (arg == "bash") { + TBashCompletionGenerator(Modes_).Generate(Command_, Cout); + } else if (arg == "zsh") { + TZshCompletionGenerator(Modes_).Generate(Command_, Cout); + } else { + Cerr << "Unknown shell name " << arg.Quote() << Endl; + parsedOptions.PrintUsage(); + return 1; + } + + return 0; + } + + private: + TString Command_; + const TModChooser* Modes_; + TString ModName_; + }; + + THolder<TMainClassArgs> MakeCompletionMod(const TModChooser* modChooser, TString command, TString modName) { + return MakeHolder<TCompleterMode>(modChooser, std::move(command), std::move(modName)); + } +} diff --git a/library/cpp/getopt/small/completer_command.h b/library/cpp/getopt/small/completer_command.h new file mode 100644 index 0000000000..974cc4617c --- /dev/null +++ b/library/cpp/getopt/small/completer_command.h @@ -0,0 +1,11 @@ +#pragma once + +#include "modchooser.h" + +namespace NLastGetopt { + /// Create an option that generates completion. + TOpt MakeCompletionOpt(const TOpts* opts, TString command, TString optName = "completion"); + + /// Create a mode that generates completion. + THolder<TMainClassArgs> MakeCompletionMod(const TModChooser* modChooser, TString command, TString modName = "completion"); +} diff --git a/library/cpp/getopt/small/completion_generator.cpp b/library/cpp/getopt/small/completion_generator.cpp new file mode 100644 index 0000000000..ac41988217 --- /dev/null +++ b/library/cpp/getopt/small/completion_generator.cpp @@ -0,0 +1,791 @@ +#include "completion_generator.h" + +#include <util/generic/overloaded.h> + +#include <util/string/ascii.h> +#include <util/generic/hash_set.h> + +#include "last_getopt_parse_result.h" + +using NLastGetopt::NEscaping::Q; +using NLastGetopt::NEscaping::QQ; +using NLastGetopt::NEscaping::C; +using NLastGetopt::NEscaping::CC; +using NLastGetopt::NEscaping::S; +using NLastGetopt::NEscaping::SS; +using NLastGetopt::NEscaping::B; +using NLastGetopt::NEscaping::BB; + +namespace NLastGetopt { + +#define L out.Line() +#define I auto Y_GENERATE_UNIQUE_ID(indent) = out.Indent() + + TCompletionGenerator::TCompletionGenerator(const TModChooser* modChooser) + : Options_(modChooser) + { + Y_VERIFY(modChooser != nullptr); + } + + TCompletionGenerator::TCompletionGenerator(const TOpts* opts) + : Options_(opts) + { + Y_VERIFY(opts != nullptr); + } + + void TZshCompletionGenerator::Generate(TStringBuf command, IOutputStream& stream) { + TFormattedOutput out; + NComp::TCompleterManager manager{command}; + + L << "#compdef " << command; + L; + L << "_" << command << "() {"; + { + I; + L << "local state line desc modes context curcontext=\"$curcontext\" ret=1"; + L << "local words_orig=(\"${words[@]}\")"; + L << "local current_orig=\"$((CURRENT - 1))\""; + L << "local prefix_orig=\"$PREFIX\""; + L << "local suffix_orig=\"$SUFFIX\""; + L; + std::visit(TOverloaded{ + [&out, &manager](const TModChooser* modChooser) { + GenerateModesCompletion(out, *modChooser, manager); + }, + [&out, &manager](const TOpts* opts) { + GenerateOptsCompletion(out, *opts, manager); + } + }, Options_); + L; + L << "return ret"; + } + L << "}"; + L; + manager.GenerateZsh(out); + + out.Print(stream); + } + + void TZshCompletionGenerator::GenerateModesCompletion(TFormattedOutput& out, const TModChooser& chooser, NComp::TCompleterManager& manager) { + auto modes = chooser.GetUnsortedModes(); + + L << "_arguments -C \\"; + L << " '(- : *)'{-h,--help}'[show help information]' \\"; + if (chooser.GetVersionHandler() != nullptr) { + L << " '(- : *)'{-v,--version}'[display version information]' \\"; + } + if (!chooser.IsSvnRevisionOptionDisabled()) { + L << " '(- : *)--svnrevision[show build information]' \\"; + } + L << " '(-v --version -h --help --svnrevision)1: :->modes' \\"; + L << " '(-v --version -h --help --svnrevision)*:: :->args' \\"; + L << " && ret=0"; + L; + L << "case $state in"; + { + I; + + L << "modes)"; + { + I; + + size_t tag = 0; + bool empty = true; + + L << "desc='modes'"; + L << "modes=("; + for (auto& mode : modes) { + if (mode->Hidden) { + continue; + } + if (!mode->Name.empty()) { + I; + if (!mode->Description.empty()) { + L << QQ(mode->Name) << ":" << QQ(mode->Description); + } else { + L << QQ(mode->Name); + } + empty = false; + } else { + L << ")"; + if (!empty) { + L << "_describe -t 'mode-group-" << tag << "' $desc modes"; + } + L; + if (mode->Description.empty()) { + L << "desc='modes'"; + } else { + L << "desc=" << SS(mode->Description); + } + L << "modes=("; + empty = true; + ++tag; + } + } + L << ")"; + if (!empty) { + L << "_describe -t 'mode-group-" << tag << "' $desc modes"; + } + L; + + L << ";;"; + } + + L << "args)"; + { + I; + + L << "case $line[1] in"; + { + I; + + for (auto& mode : modes) { + if (mode->Name.empty() || mode->Hidden) { + continue; + } + + auto& line = L << SS(mode->Name); + for (auto& alias : mode->Aliases) { + line << "|" << SS(alias); + } + line << ")"; + + { + I; + + if (auto mainArgs = dynamic_cast<TMainClassArgs*>(mode->Main)) { + GenerateOptsCompletion(out, mainArgs->GetOptions(), manager); + } else if (auto mainModes = dynamic_cast<TMainClassModes*>(mode->Main)) { + GenerateModesCompletion(out, mainModes->GetSubModes(), manager); + } else { + GenerateDefaultOptsCompletion(out, manager); + } + + L << ";;"; + } + } + } + L << "esac"; + L << ";;"; + } + } + L << "esac"; + } + + void TZshCompletionGenerator::GenerateOptsCompletion(TFormattedOutput& out, const TOpts& opts, NComp::TCompleterManager& manager) { + L << "_arguments -s \\"; + { + I; + + if (opts.ArgPermutation_ == EArgPermutation::REQUIRE_ORDER) { + L << "-S \\"; + } + + for (auto opt: opts.GetOpts()) { + if (!opt->Hidden_) { + GenerateOptCompletion(out, opts, *opt, manager); + } + } + + auto argSpecs = opts.GetFreeArgSpecs(); + size_t numFreeArgs = opts.GetFreeArgsMax(); + bool unlimitedArgs = false; + if (numFreeArgs == TOpts::UNLIMITED_ARGS) { + numFreeArgs = argSpecs.empty() ? 0 : (argSpecs.rbegin()->first + 1); + unlimitedArgs = true; + } + + for (size_t i = 0; i < numFreeArgs; ++i) { + auto& spec = argSpecs[i]; + auto& line = L << "'" << (i + 1) << ":"; + if (spec.IsOptional()) { + line << ":"; + } + auto argHelp = spec.GetCompletionArgHelp(opts.GetDefaultFreeArgTitle()); + if (argHelp) { + line << Q(argHelp); + } else { + line << " "; + } + line << ":"; + if (spec.Completer_) { + line << spec.Completer_->GenerateZshAction(manager); + } else { + line << "_default"; + } + line << "' \\"; + } + + if (unlimitedArgs) { + auto& spec = opts.GetTrailingArgSpec(); + auto& line = L << "'*:"; + auto argHelp = spec.GetCompletionArgHelp(opts.GetDefaultFreeArgTitle()); + if (argHelp) { + line << Q(argHelp); + } else { + line << " "; + } + line << ":"; + if (spec.Completer_) { + line << spec.Completer_->GenerateZshAction(manager); + } else { + line << "_default"; + } + line << "' \\"; + } + + L << "&& ret=0"; + } + } + + void TZshCompletionGenerator::GenerateDefaultOptsCompletion(TFormattedOutput& out, NComp::TCompleterManager&) { + L << "_arguments \\"; + L << " '(- *)'{-h,--help}'[show help information]' \\"; + L << " '(- *)--svnrevision[show build information]' \\"; + L << " '(-h --help --svnrevision)*: :_files' \\"; + L << " && ret=0"; + } + + void TZshCompletionGenerator::GenerateOptCompletion(TFormattedOutput& out, const TOpts& opts, const TOpt& opt, NComp::TCompleterManager& manager) { + auto& line = L; + + THashSet<TString> disableOptions; + if (opt.DisableCompletionForOptions_) { + disableOptions.insert("-"); + } else { + if (!opt.AllowMultipleCompletion_) { + for (auto shortName: opt.GetShortNames()) { + disableOptions.insert(TString("-") + shortName); + } + for (auto& longName: opt.GetLongNames()) { + disableOptions.insert("--" + longName); + } + } + for (auto disabledShortName : opt.DisableCompletionForChar_) { + auto disabledOpt = opts.FindCharOption(disabledShortName); + if (disabledOpt) { + for (auto shortName: disabledOpt->GetShortNames()) { + disableOptions.insert(TString("-") + shortName); + } + for (auto& longName: disabledOpt->GetLongNames()) { + disableOptions.insert("--" + longName); + } + } else { + disableOptions.insert(TString("-") + disabledShortName); + } + } + for (auto& disabledLongName : opt.DisableCompletionForLongName_) { + auto disabledOpt = opts.FindLongOption(disabledLongName); + if (disabledOpt) { + for (auto shortName: disabledOpt->GetShortNames()) { + disableOptions.insert(TString("-") + shortName); + } + for (auto& longName: disabledOpt->GetLongNames()) { + disableOptions.insert("--" + longName); + } + } else { + disableOptions.insert("--" + disabledLongName); + } + } + } + if (opt.DisableCompletionForFreeArgs_) { + disableOptions.insert(":"); + disableOptions.insert("*"); + } else { + for (auto i : opt.DisableCompletionForFreeArg_) { + disableOptions.insert(ToString(i + 1)); + } + } + + TStringBuf sep = ""; + + if (!disableOptions.empty()) { + line << "'("; + for (auto& disableOption : disableOptions) { + line << sep << disableOption; + sep = " "; + } + line << ")"; + } + + sep = ""; + TStringBuf mul = ""; + TStringBuf quot = ""; + + if (opt.GetShortNames().size() + opt.GetLongNames().size() > 1) { + if (!disableOptions.empty()) { + line << "'"; + } + line << "{"; + quot = "'"; + } else { + if (disableOptions.empty()) { + line << "'"; + } + } + + if (opt.AllowMultipleCompletion_) { + mul = "*"; + } + + for (auto& flag : opt.GetShortNames()) { + line << sep << quot << mul << "-" << Q(TStringBuf(&flag, 1)) << quot; + sep = ","; + } + + for (auto& flag : opt.GetLongNames()) { + line << sep << quot << mul << "--" << Q(flag) << quot; + sep = ","; + } + + if (opt.GetShortNames().size() + opt.GetLongNames().size() > 1) { + line << "}'"; + } + + if (opt.GetCompletionHelp()) { + line << "["; + line << Q(opt.GetCompletionHelp()); + line << "]"; + } + + if (opt.HasArg_ != EHasArg::NO_ARGUMENT) { + if (opt.HasArg_ == EHasArg::OPTIONAL_ARGUMENT) { + line << ":"; + } + + line << ":"; + + if (opt.GetCompletionArgHelp()) { + line << C(opt.GetCompletionArgHelp()); + } else { + line << " "; + } + + line << ":"; + + if (opt.Completer_) { + line << C(opt.Completer_->GenerateZshAction(manager)); + } else { + line << "_default"; + } + } + + line << "' \\"; + } + + void TBashCompletionGenerator::Generate(TStringBuf command, IOutputStream& stream) { + TFormattedOutput out; + NComp::TCompleterManager manager{command}; + + L << "_" << command << "() {"; + { + I; + L << "COMPREPLY=()"; + L; + L << "local i args opts items candidates"; + L; + L << "local cur prev words cword"; + L << "_get_comp_words_by_ref -n \"\\\"'><=;|&(:\" cur prev words cword"; + L; + L << "local need_space=\"1\""; + L << "local IFS=$' \\t\\n'"; + L; + std::visit(TOverloaded{ + [&out, &manager](const TModChooser* modChooser) { + GenerateModesCompletion(out, *modChooser, manager, 1); + }, + [&out, &manager](const TOpts* opts) { + GenerateOptsCompletion(out, *opts, manager, 1); + } + }, Options_); + L; + L; + L << "__ltrim_colon_completions \"$cur\""; + L; + L << "IFS=$'\\n'"; + L << "if [ ${#COMPREPLY[@]} -ne 0 ]; then"; + { + I; + L << "if [[ -z $need_space ]]; then"; + { + I; + L << "COMPREPLY=( $(printf \"%q\\n\" \"${COMPREPLY[@]}\") )"; + } + L << "else"; + { + I; + L << "COMPREPLY=( $(printf \"%q \\n\" \"${COMPREPLY[@]}\") )"; + } + L << "fi"; + } + L << "fi"; + L; + L << "return 0"; + } + L << "}"; + L; + L << "complete -o nospace -o default -F _" << command << " " << command; + + out.Print(stream); + } + + void TBashCompletionGenerator::GenerateModesCompletion(TFormattedOutput& out, const TModChooser& chooser, NComp::TCompleterManager& manager, size_t level) { + auto modes = chooser.GetUnsortedModes(); + + L << "if [[ ${cword} == " << level << " ]] ; then"; + { + I; + L << "if [[ ${cur} == -* ]] ; then"; + { + I; + auto& line = L << "COMPREPLY+=( $(compgen -W '-h --help"; + if (chooser.GetVersionHandler() != nullptr) { + line << " -v --version"; + } + if (!chooser.IsSvnRevisionOptionDisabled()) { + line << " --svnrevision"; + } + line << "' -- ${cur}) )"; + } + L << "else"; + { + I; + auto& line = L << "COMPREPLY+=( $(compgen -W '"; + TStringBuf sep = ""; + for (auto& mode : modes) { + if (!mode->Hidden && !mode->NoCompletion) { + line << sep << B(mode->Name); + sep = " "; + } + } + line << "' -- ${cur}) )"; + } + L << "fi"; + } + L << "else"; + { + I; + L << "case \"${words[" << level << "]}\" in"; + { + I; + + for (auto& mode : modes) { + if (mode->Name.empty() || mode->Hidden || mode->NoCompletion) { + continue; + } + + auto& line = L << BB(mode->Name); + for (auto& alias : mode->Aliases) { + line << "|" << BB(alias); + } + line << ")"; + + { + I; + + if (auto mainArgs = dynamic_cast<TMainClassArgs*>(mode->Main)) { + GenerateOptsCompletion(out, mainArgs->GetOptions(), manager, level + 1); + } else if (auto mainModes = dynamic_cast<TMainClassModes*>(mode->Main)) { + GenerateModesCompletion(out, mainModes->GetSubModes(), manager, level + 1); + } else { + GenerateDefaultOptsCompletion(out, manager); + } + + L << ";;"; + } + } + } + L << "esac"; + } + L << "fi"; + } + + void TBashCompletionGenerator::GenerateOptsCompletion(TFormattedOutput& out, const TOpts& opts, NComp::TCompleterManager&, size_t level) { + auto unorderedOpts = opts.GetOpts(); + + L << "if [[ ${cur} == -* ]] ; then"; + { + I; + auto& line = L << "COMPREPLY+=( $(compgen -W '"; + TStringBuf sep = ""; + for (auto& opt : unorderedOpts) { + if (opt->IsHidden()) { + continue; + } + + for (auto& shortName : opt->GetShortNames()) { + line << sep << "-" << B(TStringBuf(&shortName, 1)); + sep = " "; + } + for (auto& longName: opt->GetLongNames()) { + line << sep << "--" << B(longName); + sep = " "; + } + } + line << "' -- ${cur}) )"; + } + L << "else"; + { + I; + L << "case ${prev} in"; + { + I; + for (auto& opt : unorderedOpts) { + if (opt->HasArg_ == EHasArg::NO_ARGUMENT || opt->IsHidden()) { + continue; + } + + auto& line = L; + TStringBuf sep = ""; + for (auto& shortName : opt->GetShortNames()) { + line << sep << "'-" << B(TStringBuf(&shortName, 1)) << "'"; + sep = "|"; + } + for (auto& longName: opt->GetLongNames()) { + line << sep << "'--" << B(longName) << "'"; + sep = "|"; + } + line << ")"; + { + I; + if (opt->Completer_ != nullptr) { + opt->Completer_->GenerateBash(out); + } + L << ";;"; + } + } + + L << "*)"; + { + I; + + L << "args=0"; + auto& line = L << "opts='@("; + TStringBuf sep = ""; + for (auto& opt : unorderedOpts) { + if (opt->HasArg_ == EHasArg::NO_ARGUMENT || opt->IsHidden()) { + continue; + } + for (auto& shortName : opt->GetShortNames()) { + line << sep << "-" << B(TStringBuf(&shortName, 1)); + sep = "|"; + } + for (auto& longName: opt->GetLongNames()) { + line << sep << "--" << B(longName); + sep = "|"; + } + } + line << ")'"; + L << "for (( i=" << level << "; i < cword; i++ )); do"; + { + I; + L << "if [[ ${words[i]} != -* && ${words[i-1]} != $opts ]]; then"; + { + I; + L << "(( args++ ))"; + } + L << "fi"; + } + L << "done"; + L; + + auto argSpecs = opts.GetFreeArgSpecs(); + size_t numFreeArgs = opts.GetFreeArgsMax(); + bool unlimitedArgs = false; + if (numFreeArgs == TOpts::UNLIMITED_ARGS) { + numFreeArgs = argSpecs.empty() ? 0 : (argSpecs.rbegin()->first + 1); + unlimitedArgs = true; + } + + L << "case ${args} in"; + { + I; + + for (size_t i = 0; i < numFreeArgs; ++i) { + L << i << ")"; + { + I; + auto& spec = argSpecs[i]; + if (spec.Completer_ != nullptr) { + spec.Completer_->GenerateBash(out); + } + L << ";;"; + } + } + if (unlimitedArgs) { + L << "*)"; + { + I; + auto& spec = opts.GetTrailingArgSpec(); + if (spec.Completer_ != nullptr) { + spec.Completer_->GenerateBash(out); + } + L << ";;"; + } + } + } + L << "esac"; + L << ";;"; + } + } + L << "esac"; + } + L << "fi"; + } + + void TBashCompletionGenerator::GenerateDefaultOptsCompletion(TFormattedOutput& out, NComp::TCompleterManager&) { + L << "if [[ ${cur} == -* ]] ; then"; + { + I; + L << "COMPREPLY+=( $(compgen -W '-h --help --svnrevision' -- ${cur}) )"; + } + L << "fi"; + } + +#undef I +#undef L + + TString NEscaping::Q(TStringBuf string) { + TStringBuilder out; + out.reserve(string.size()); + for (auto c: string) { + switch (c) { + case '\a': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + out << " "; + break; + case '\\': + out << "\\\\"; + break; + case '\'': + out << "''"; + break; + case '\"': + out << "\\\""; + break; + case '[': + out << "\\["; + break; + case ']': + out << "\\]"; + break; + case ':': + out << "\\:"; + break; + case '+': + out << "\\+"; + break; + case '=': + out << "\\="; + break; + default: + out << c; + break; + } + } + return out; + } + + TString NEscaping::QQ(TStringBuf string) { + auto q = Q(string); + return "'" + q + "'"; + } + + TString NEscaping::C(TStringBuf string) { + TStringBuilder out; + out.reserve(string.size() + 1); + for (auto c: string) { + switch (c) { + case '\a': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + out << " "; + break; + case '\'': + out << "''"; + break; + case ':': + out << "\\:"; + break; + default: + out << c; + break; + } + } + return out; + } + + TString NEscaping::CC(TStringBuf string) { + auto c = C(string); + return "'" + c + "'"; + } + + TString NEscaping::S(TStringBuf string) { + TStringBuilder out; + out.reserve(string.size() + 1); + for (auto c: string) { + switch (c) { + case '\a': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + out << " "; + break; + case '\'': + out << "''"; + break; + default: + out << c; + break; + } + } + return out; + } + + TString NEscaping::SS(TStringBuf string) { + auto s = S(string); + return "'" + s + "'"; + } + + TString NEscaping::B(TStringBuf string) { + TStringBuilder out; + out.reserve(string.size() + 1); + for (auto c: string) { + switch (c) { + case '\a': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + out << " "; + break; + case '\'': + out << "'\"'\"'"; + break; + default: + out << c; + break; + } + } + return out; + } + + TString NEscaping::BB(TStringBuf string) { + auto b = B(string); + return "'" + b + "'"; + } +} diff --git a/library/cpp/getopt/small/completion_generator.h b/library/cpp/getopt/small/completion_generator.h new file mode 100644 index 0000000000..4241bb7d6c --- /dev/null +++ b/library/cpp/getopt/small/completion_generator.h @@ -0,0 +1,69 @@ +#pragma once + +#include "completer.h" +#include "formatted_output.h" +#include "last_getopt_opts.h" +#include "modchooser.h" + +#include <util/generic/variant.h> +#include <util/string/builder.h> + +namespace NLastGetopt { + class TCompletionGenerator { + public: + explicit TCompletionGenerator(const TModChooser* modChooser); + explicit TCompletionGenerator(const TOpts* opts); + virtual ~TCompletionGenerator() = default; + + public: + virtual void Generate(TStringBuf command, IOutputStream& stream) = 0; + + protected: + std::variant<const TModChooser*, const TOpts*> Options_; + }; + + class TZshCompletionGenerator: public TCompletionGenerator { + public: + using TCompletionGenerator::TCompletionGenerator; + + public: + void Generate(TStringBuf command, IOutputStream& stream) override; + + private: + static void GenerateModesCompletion(TFormattedOutput& out, const TModChooser& chooser, NComp::TCompleterManager& manager); + static void GenerateOptsCompletion(TFormattedOutput& out, const TOpts& opts, NComp::TCompleterManager& manager); + static void GenerateDefaultOptsCompletion(TFormattedOutput& out, NComp::TCompleterManager& manager); + static void GenerateOptCompletion(TFormattedOutput& out, const TOpts& opts, const TOpt& opt, NComp::TCompleterManager& manager); + }; + + class TBashCompletionGenerator: public TCompletionGenerator { + public: + using TCompletionGenerator::TCompletionGenerator; + + public: + void Generate(TStringBuf command, IOutputStream& stream) override; + + private: + static void GenerateModesCompletion(TFormattedOutput& out, const TModChooser& chooser, NComp::TCompleterManager& manager, size_t level); + static void GenerateOptsCompletion(TFormattedOutput& out, const TOpts& opts, NComp::TCompleterManager& manager, size_t level); + static void GenerateDefaultOptsCompletion(TFormattedOutput& out, NComp::TCompleterManager& manager); + }; + + namespace NEscaping { + /// Escape ':', '-', '=', '[', ']' for use in zsh _arguments + TString Q(TStringBuf string); + TString QQ(TStringBuf string); + + /// Escape colons for use in zsh _alternative and _arguments + TString C(TStringBuf string); + TString CC(TStringBuf string); + + /// Simple escape for use in zsh single-quoted strings + TString S(TStringBuf string); + TString SS(TStringBuf string); + + /// Simple escape for use in bash single-quoted strings + TString B(TStringBuf string); + TString BB(TStringBuf string); + } +} diff --git a/library/cpp/getopt/small/formatted_output.cpp b/library/cpp/getopt/small/formatted_output.cpp new file mode 100644 index 0000000000..bf1b366f25 --- /dev/null +++ b/library/cpp/getopt/small/formatted_output.cpp @@ -0,0 +1,36 @@ +#include "formatted_output.h" + +namespace NLastGetopt { + + TFormattedOutput::IndentGuard::IndentGuard(TFormattedOutput* output) + : Output(output) + { + Output->IndentLevel_ += 2; + } + + TFormattedOutput::IndentGuard::~IndentGuard() { + Output->IndentLevel_ -= 2; + } + + TFormattedOutput::IndentGuard TFormattedOutput::Indent() { + return IndentGuard(this); + } + + TStringBuilder& TFormattedOutput::Line() { + return Lines_.emplace_back(IndentLevel_, TStringBuilder()).second; + } + + void TFormattedOutput::Print(IOutputStream& out) { + for (auto&[indent, line] : Lines_) { + if (indent && !line.empty()) { + TTempBuf buf(indent); + Fill(buf.Data(), buf.Data() + indent, ' '); + out.Write(buf.Data(), indent); + } + out << line; + if (!line.EndsWith('\n')) { + out << Endl; + } + } + } +} diff --git a/library/cpp/getopt/small/formatted_output.h b/library/cpp/getopt/small/formatted_output.h new file mode 100644 index 0000000000..6fd16b73f9 --- /dev/null +++ b/library/cpp/getopt/small/formatted_output.h @@ -0,0 +1,32 @@ +#pragma once + +#include <util/generic/algorithm.h> +#include <util/generic/vector.h> +#include <util/string/builder.h> +#include <util/memory/tempbuf.h> + +namespace NLastGetopt { + /// Utility for printing indented lines. Used by completion generators. + class TFormattedOutput { + public: + struct IndentGuard { + explicit IndentGuard(TFormattedOutput* output); + virtual ~IndentGuard(); + TFormattedOutput* Output; + }; + + public: + /// Increase indentation and return a RAII object that'll decrease it back automatically. + IndentGuard Indent(); + + /// Append a new indented line to the stream. + TStringBuilder& Line(); + + /// Collect all lines into a stream. + void Print(IOutputStream& out); + + private: + int IndentLevel_ = 0; + TVector<std::pair<int, TStringBuilder>> Lines_; + }; +} diff --git a/library/cpp/getopt/small/last_getopt.cpp b/library/cpp/getopt/small/last_getopt.cpp new file mode 100644 index 0000000000..30669b2c5a --- /dev/null +++ b/library/cpp/getopt/small/last_getopt.cpp @@ -0,0 +1,9 @@ +#include "last_getopt.h" + +namespace NLastGetopt { + void PrintUsageAndExit(const TOptsParser* parser) { + parser->PrintUsage(); + exit(0); + } + +} diff --git a/library/cpp/getopt/small/last_getopt.h b/library/cpp/getopt/small/last_getopt.h new file mode 100644 index 0000000000..07687bc914 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt.h @@ -0,0 +1,132 @@ +#pragma once + +#include "last_getopt_opts.h" +#include "last_getopt_easy_setup.h" +#include "last_getopt_parse_result.h" + +#include <util/generic/function.h> +#include <util/string/split.h> + +/// see some documentation in +/// https://wiki.yandex-team.ru/development/poisk/arcadia/util/lastgetopt/ +/// https://wiki.yandex-team.ru/development/poisk/arcadia/library/getopt/ +/// see examples in library/cpp/getopt/last_getopt_demo + +//TODO: in most cases this include is unnecessary, but needed THandlerFunctor1<TpFunc, TpArg>::HandleOpt +#include "last_getopt_parser.h" + +namespace NLastGetopt { + /// Handler to split option value by delimiter into a target container and allow ranges. + template <class Container> + struct TOptRangeSplitHandler: public IOptHandler { + public: + using TContainer = Container; + using TValue = typename TContainer::value_type; + + explicit TOptRangeSplitHandler(TContainer* target, const char elementsDelim, const char rangesDelim) + : Target(target) + , ElementsDelim(elementsDelim) + , RangesDelim(rangesDelim) + { + } + + void HandleOpt(const TOptsParser* parser) override { + const TStringBuf curval(parser->CurValOrDef()); + if (curval.IsInited()) { + StringSplitter(curval).Split(ElementsDelim).Consume([&](const TStringBuf& val) { + TStringBuf mutableValue = val; + + TValue first = NPrivate::OptFromString<TValue>(mutableValue.NextTok(RangesDelim), parser->CurOpt()); + TValue last = mutableValue ? NPrivate::OptFromString<TValue>(mutableValue, parser->CurOpt()) : first; + + if (last < first) { + throw TUsageException() << "failed to parse opt " << NPrivate::OptToString(parser->CurOpt()) << " value " << TString(val).Quote() << ": the second argument is less than the first one"; + } + + for (++last; first < last; ++first) { + Target->insert(Target->end(), first); + } + }); + } + } + + private: + TContainer* Target; + char ElementsDelim; + char RangesDelim; + }; + + template <class Container> + struct TOptSplitHandler: public IOptHandler { + public: + using TContainer = Container; + using TValue = typename TContainer::value_type; + + explicit TOptSplitHandler(TContainer* target, const char delim) + : Target(target) + , Delim(delim) + { + } + + void HandleOpt(const TOptsParser* parser) override { + const TStringBuf curval(parser->CurValOrDef()); + if (curval.IsInited()) { + StringSplitter(curval).Split(Delim).Consume([&](const TStringBuf& val) { + Target->insert(Target->end(), NPrivate::OptFromString<TValue>(val, parser->CurOpt())); + }); + } + } + + private: + TContainer* Target; + char Delim; + }; + + template <class TpFunc> + struct TOptKVHandler: public IOptHandler { + public: + using TKey = typename TFunctionArgs<TpFunc>::template TGet<0>; + using TValue = typename TFunctionArgs<TpFunc>::template TGet<1>; + + explicit TOptKVHandler(TpFunc func, const char kvdelim = '=') + : Func(func) + , KVDelim(kvdelim) + { + } + + void HandleOpt(const TOptsParser* parser) override { + const TStringBuf curval(parser->CurValOrDef()); + const TOpt* curOpt(parser->CurOpt()); + if (curval.IsInited()) { + TStringBuf key, value; + if (!curval.TrySplit(KVDelim, key, value)) { + throw TUsageException() << "failed to parse opt " << NPrivate::OptToString(curOpt) + << " value " << TString(curval).Quote() << ": expected key" << KVDelim << "value format"; + } + Func(NPrivate::OptFromString<TKey>(key, curOpt), NPrivate::OptFromString<TValue>(value, curOpt)); + } + } + + private: + TpFunc Func; + char KVDelim; + }; + + namespace NPrivate { + template <typename TpFunc, typename TpArg> + void THandlerFunctor1<TpFunc, TpArg>::HandleOpt(const TOptsParser* parser) { + const TStringBuf curval = parser->CurValOrDef(!HasDef_); + const TpArg& arg = curval.IsInited() ? OptFromString<TpArg>(curval, parser->CurOpt()) : Def_; + try { + Func_(arg); + } catch (const TUsageException&) { + throw; + } catch (...) { + throw TUsageException() << "failed to handle opt " << OptToString(parser->CurOpt()) + << " value " << TString(curval).Quote() << ": " << CurrentExceptionMessage(); + } + } + + } + +} diff --git a/library/cpp/getopt/small/last_getopt_easy_setup.cpp b/library/cpp/getopt/small/last_getopt_easy_setup.cpp new file mode 100644 index 0000000000..c87dedf95e --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_easy_setup.cpp @@ -0,0 +1,47 @@ +#include "last_getopt_easy_setup.h" + +namespace NLastGetopt { + TEasySetup::TEasySetup(const TStringBuf& optstring) + : TOpts(optstring) + { + AddHelpOption(); + } + + TOpt& TEasySetup::AdjustParam(const char* longName, const char* help, const char* argName, bool required) { + Y_ASSERT(longName); + TOpt& o = AddLongOption(longName); + if (help) { + o.Help(help); + } + if (argName) { + o.RequiredArgument(argName); + } else { + o.HasArg(NO_ARGUMENT); + } + if (required) { + o.Required(); + } + return o; + } + + TEasySetup& TEasySetup::operator()(char shortName, const char* longName, const char* help, bool required) { + AdjustParam(longName, help, nullptr, required).AddShortName(shortName); + return *this; + } + + TEasySetup& TEasySetup::operator()(char shortName, const char* longName, const char* argName, const char* help, bool required) { + AdjustParam(longName, help, argName, required).AddShortName(shortName); + return *this; + } + + TEasySetup& TEasySetup::operator()(const char* longName, const char* help, bool required) { + AdjustParam(longName, help, nullptr, required); + return *this; + } + + TEasySetup& TEasySetup::operator()(const char* longName, const char* argName, const char* help, bool required) { + AdjustParam(longName, help, argName, required); + return *this; + } + +} diff --git a/library/cpp/getopt/small/last_getopt_easy_setup.h b/library/cpp/getopt/small/last_getopt_easy_setup.h new file mode 100644 index 0000000000..60dddda225 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_easy_setup.h @@ -0,0 +1,51 @@ +#pragma once + +#include "last_getopt_opts.h" + +namespace NLastGetopt { + /** + * Wrapper for TOpts class to make the life a bit easier. + * Usual usage: + * TEasySetup opts; + * opts('s', "server", "MR_SERVER", "MapReduce server name in format server:port", true) + * ('u', "user", "MR_USER", "MapReduce user name", true) + * ('o', "output", "MR_TABLE", "Name of MR table which will contain results", true) + * ('r', "rules", "FILE", "Filename for .rules output file") //!< This parameter is optional and has a required argument + * ('v', "version", &PrintSvnVersionAndExit0, "Print version information") //!< Parameter with handler can't get any argument + * ("verbose", "Be verbose") //!< You may not specify short param name + * + * NLastGetopt::TOptsParseResult r(&opts, argc, argv); + */ + class TEasySetup: public TOpts { + public: + TEasySetup(const TStringBuf& optstring = TStringBuf()); + TEasySetup& operator()(char shortName, const char* longName, const char* help, bool required = false); + TEasySetup& operator()(char shortName, const char* longName, const char* argName, const char* help, bool required = false); + + template <class TpFunc> + TEasySetup& operator()(char shortName, const char* longName, TpFunc handler, const char* help, bool required = false) { + AdjustParam(longName, help, nullptr, handler, required).AddShortName(shortName); + return *this; + } + + TEasySetup& operator()(const char* longName, const char* help, bool required = false); + TEasySetup& operator()(const char* longName, const char* argName, const char* help, bool required = false); + + template <class TpFunc> + TEasySetup& operator()(const char* longName, TpFunc handler, const char* help, bool required = false) { + AdjustParam(longName, help, nullptr, handler, required); + return *this; + } + + private: + TOpt& AdjustParam(const char* longName, const char* help, const char* argName, bool required); + + template <class TpFunc> + TOpt& AdjustParam(const char* longName, const char* help, const char* argName, TpFunc handler, bool required) { + TOpt& o = AdjustParam(longName, help, argName, required); + o.Handler0(handler); + return o; + } + }; + +} diff --git a/library/cpp/getopt/small/last_getopt_handlers.h b/library/cpp/getopt/small/last_getopt_handlers.h new file mode 100644 index 0000000000..d35456ef34 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_handlers.h @@ -0,0 +1,24 @@ +#pragma once + +#include "last_getopt_support.h" + +#include <util/string/split.h> +#include <util/system/compiler.h> + +namespace NLastGetopt { + /// Handler to split option value by delimiter into a target container. + template <class Container> + struct TOptSplitHandler; + + /// Handler to split option value by delimiter into a target container and allow ranges. + template <class Container> + struct TOptRangeSplitHandler; + + /// Handler to parse key-value pairs (default delimiter is '=') and apply user-supplied handler to each pair + template <class TpFunc> + struct TOptKVHandler; + + [[noreturn]] void PrintUsageAndExit(const TOptsParser* parser); + [[noreturn]] void PrintVersionAndExit(const TOptsParser* parser); + [[noreturn]] void PrintShortVersionAndExit(const TString& appName); +} diff --git a/library/cpp/getopt/small/last_getopt_opt.cpp b/library/cpp/getopt/small/last_getopt_opt.cpp new file mode 100644 index 0000000000..9a99437f4b --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_opt.cpp @@ -0,0 +1,113 @@ +#include "last_getopt_opt.h" + +#include <util/stream/format.h> +#include <util/string/escape.h> +#include <util/generic/ylimits.h> +#include <util/generic/utility.h> +#include <util/generic/algorithm.h> +#include <ctype.h> + +namespace NLastGetopt { + static const TStringBuf ExcludedShortNameChars = "= -\t\n"; + static const TStringBuf ExcludedLongNameChars = "= \t\n"; + + bool TOpt::NameIs(const TString& name) const { + for (const auto& next : LongNames_) { + if (next == name) + return true; + } + return false; + } + + bool TOpt::CharIs(char c) const { + for (auto next : Chars_) { + if (next == c) + return true; + } + return false; + } + + char TOpt::GetChar() const { + ; + if (Chars_.empty()) + ythrow TConfException() << "no char for option " << this->ToShortString(); + return Chars_.at(0); + } + + char TOpt::GetCharOr0() const { + if (Chars_.empty()) + return 0; + return GetChar(); + } + + TString TOpt::GetName() const { + ; + if (LongNames_.empty()) + ythrow TConfException() << "no name for option " << this->ToShortString(); + return LongNames_.at(0); + } + + bool TOpt::IsAllowedShortName(unsigned char c) { + return isprint(c) && TStringBuf::npos == ExcludedShortNameChars.find(c); + } + + TOpt& TOpt::AddShortName(unsigned char c) { + ; + if (!IsAllowedShortName(c)) + throw TUsageException() << "option char '" << c << "' is not allowed"; + Chars_.push_back(c); + return *this; + } + + bool TOpt::IsAllowedLongName(const TString& name, unsigned char* out) { + for (size_t i = 0; i != name.size(); ++i) { + const unsigned char c = name[i]; + if (!isprint(c) || TStringBuf::npos != ExcludedLongNameChars.find(c)) { + if (nullptr != out) + *out = c; + return false; + } + } + return true; + } + + TOpt& TOpt::AddLongName(const TString& name) { + ; + unsigned char c = 0; + if (!IsAllowedLongName(name, &c)) + throw TUsageException() << "option char '" << c + << "' in long '" << name << "' is not allowed"; + LongNames_.push_back(name); + return *this; + } + + namespace NPrivate { + TString OptToString(char c); + + TString OptToString(const TString& longOption); + } + + TString TOpt::ToShortString() const { + ; + if (!LongNames_.empty()) + return NPrivate::OptToString(LongNames_.front()); + if (!Chars_.empty()) + return NPrivate::OptToString(Chars_.front()); + return "?"; + } + + void TOpt::FireHandlers(const TOptsParser* parser) const { + for (const auto& handler : Handlers_) { + handler->HandleOpt(parser); + } + } + + TOpt& TOpt::IfPresentDisableCompletionFor(const TOpt& opt) { + if (opt.GetLongNames()) { + IfPresentDisableCompletionFor(opt.GetName()); + } else { + IfPresentDisableCompletionFor(opt.GetChar()); + } + return *this; + } +} diff --git a/library/cpp/getopt/small/last_getopt_opt.h b/library/cpp/getopt/small/last_getopt_opt.h new file mode 100644 index 0000000000..a8dd5adca9 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_opt.h @@ -0,0 +1,812 @@ +#pragma once + +#include "completer.h" +#include "last_getopt_handlers.h" + +#include <util/string/split.h> +#include <util/generic/ptr.h> +#include <util/generic/string.h> +#include <util/generic/maybe.h> +#include <util/generic/vector.h> +#include <util/string/cast.h> + +#include <stdarg.h> + +namespace NLastGetopt { + enum EHasArg { + NO_ARGUMENT, + REQUIRED_ARGUMENT, + OPTIONAL_ARGUMENT, + DEFAULT_HAS_ARG = REQUIRED_ARGUMENT + }; + + /** + * NLastGetopt::TOpt is a storage of data about exactly one program option. + * The data is: parse politics and help information. + * + * The help information consists of following: + * hidden or visible in help information + * help string + * argument name + * + * Parse politics is determined by following parameters: + * argument parse politics: no/optional/required/ + * option existence: required or optional + * handlers. See detailed documentation: <TODO:link> + * default value: if the option has argument, but the option is ommited, + * then the <default value> is used as the value of the argument + * optional value: if the option has optional-argument, the option is present in parsed string, + * but the argument is omitted, then <optional value is used> + * in case of "not given <optional value>, omited optional argument" the <default value> is used + * user value: allows to store arbitary pointer for handlers + */ + class TOpt { + public: + typedef TVector<char> TShortNames; + typedef TVector<TString> TLongNames; + + protected: + TShortNames Chars_; + TLongNames LongNames_; + + private: + typedef TMaybe<TString> TdOptVal; + typedef TVector<TSimpleSharedPtr<IOptHandler>> TOptHandlers; + + public: + bool Hidden_ = false; // is visible in help + TString ArgTitle_; // the name of argument in help output + TString Help_; // the help string + TString CompletionHelp_; // the help string that's used in completion script, a shorter version of Help_ + TString CompletionArgHelp_; // the description of argument in completion script + + EHasArg HasArg_ = DEFAULT_HAS_ARG; // the argument parsing politics + bool Required_ = false; // option existence politics + + bool AllowMultipleCompletion_ = false; // let the completer know that this option can occur more than once + + bool DisableCompletionForOptions_ = false; + bool DisableCompletionForFreeArgs_ = false; + TShortNames DisableCompletionForChar_; + TLongNames DisableCompletionForLongName_; + TVector<size_t> DisableCompletionForFreeArg_; + NComp::ICompleterPtr Completer_; + + private: + //Handlers information + const void* UserValue_ = nullptr; + TdOptVal OptionalValue_; + TdOptVal DefaultValue_; + TOptHandlers Handlers_; + + public: + /** + * Checks if given char can be a short name + * @param c char to check + */ + static bool IsAllowedShortName(unsigned char c); + + /** + * Checks if given string can be a long name + * @param name string to check + * @param c if given, the first bad charecter will be saved in c + */ + static bool IsAllowedLongName(const TString& name, unsigned char* c = nullptr); + + /** + * @return one of the expected representations of the option. + * If the option has short names, will return "-<char>" + * Otherwise will return "--<long name>" + */ + TString ToShortString() const; + + /** + * check if given string is one of the long names + * + * @param name string to check + */ + bool NameIs(const TString& name) const; + + /** + * check if given char is one of the short names + * + * @param c char to check + */ + bool CharIs(char c) const; + + /** + * If string has long names - will return one of them + * Otherwise will throw + */ + TString GetName() const; + + /** + * adds short alias for the option + * + * @param c new short name + * + * @return self + */ + TOpt& AddShortName(unsigned char c); + + /** + * return all short names of the option + */ + const TShortNames& GetShortNames() const { + return Chars_; + } + + /** + * adds long alias for the option + * + * @param name new long name + * + * @return self + */ + TOpt& AddLongName(const TString& name); + + /** + * return all long names of the option + */ + const TLongNames& GetLongNames() const { + return LongNames_; + } + + /** + * @return one of short names of the opt. If there is no short names exception is raised. + */ + char GetChar() const; + + /** + * @return one of short names of the opt. If there is no short names '\0' returned. + */ + char GetCharOr0() const; + + /** + * @returns argument parsing politics + */ + const EHasArg& GetHasArg() const { + return HasArg_; + } + + /** + * sets argument parsing politics + * + * Note: its better use one of RequiredArgument/NoArgument/OptionalArgument methods + * + * @param hasArg new argument parsing mode + * @return self + */ + TOpt& HasArg(EHasArg hasArg) { + HasArg_ = hasArg; + return *this; + } + + /** + * @returns argument title + */ + TString GetArgTitle() const { + return ArgTitle_; + } + + /** + * sets argument parsing politics into REQUIRED_ARGUMENT + * + * @param title the new name of argument in help output + * @return self + */ + TOpt& RequiredArgument(const TString& title = "") { + ArgTitle_ = title; + return HasArg(REQUIRED_ARGUMENT); + } + + /** + * sets argument parsing politics into NO_ARGUMENT + * + * @return self + */ + TOpt& NoArgument() { + return HasArg(NO_ARGUMENT); + } + + /** + * sets argument parsing politics into OPTIONAL_ARGUMENT + * for details see NLastGetopt::TOpt + * + * @param title the new name of argument in help output + * @return self + */ + TOpt& OptionalArgument(const TString& title = "") { + ArgTitle_ = title; + return HasArg(OPTIONAL_ARGUMENT); + } + + /** + * sets argument parsing politics into OPTIONAL_ARGUMENT + * sets the <optional value> into given + * + * for details see NLastGetopt::TOpt + * + * @param val the new <optional value> + * @param title the new name of argument in help output + * @return self + */ + TOpt& OptionalValue(const TString& val, const TString& title = "") { + OptionalValue_ = val; + return OptionalArgument(title); + } + + /** + * checks if "argument parsing politics" is OPTIONAL_ARGUMENT and the <optional value> is set. + */ + bool HasOptionalValue() const { + return OPTIONAL_ARGUMENT == HasArg_ && OptionalValue_; + } + + /** + * @return optional value + * throws exception if optional value wasn't set + */ + const TString& GetOptionalValue() const { + return *OptionalValue_; + } + + /** + * sets <default value> + * @return self + */ + template <typename T> + TOpt& DefaultValue(const T& val) { + DefaultValue_ = ToString(val); + return *this; + } + + /** + * checks if default value is set. + */ + bool HasDefaultValue() const { + return DefaultValue_.Defined(); + } + + /** + * @return default value + * throws exception if <default value> wasn't set + */ + const TString& GetDefaultValue() const { + return *DefaultValue_; + } + + /** + * sets the option to be required + * @return self + */ + TOpt& Required() { + Required_ = true; + return *this; + } + + /** + * sets the option to be optional + * @return self + */ + TOpt& Optional() { + Required_ = false; + return *this; + } + + /** + * @return true if the option is required + */ + bool IsRequired() const { + return Required_; + } + + /** + * sets the option to be hidden (invisible in help) + * @return self + */ + TOpt& Hidden() { + Hidden_ = true; + return *this; + } + + /** + * @return true if the option is hidden + */ + bool IsHidden() const { + return Hidden_; + } + + /** + * sets the <user value> + * @return self + * for details see NLastGetopt::TOpt + */ + TOpt& UserValue(const void* userval) { + UserValue_ = userval; + return *this; + } + + /** + * @return user value + */ + const void* UserValue() const { + return UserValue_; + } + + /** + * Set help string that appears with `--help`. Unless `CompletionHelp` is given, this message will also be used + * in completion script. In this case, don't make it too long, don't start it with a capital letter and don't + * end it with a full stop. + * + * Note that `Help`, `CompletionHelp` and `CompletionArgHelp` are not the same. `Help` is printed in program + * usage (when you call `program --help`), `CompletionHelp` is printed when completer lists available + * options, and `CompletionArgHelp` is printed when completer shows available values for the option. + * + * Example of good help message: + * + * ``` + * opts.AddLongOption('t', "timeout") + * .Help("specify query timeout in milliseconds") + * .CompletionHelp("specify query timeout") + * .CompletionArgHelp("query timeout (ms) [default=500]"); + * ``` + * + * Notice how `Help` and `CompletionArgHelp` have units in them, but `CompletionHelp` don't. + * + * Another good example is the help option: + * + * ``` + * opts.AddLongOption('h', "help") + * .Help("print this message and exit") + * .CompletionHelp("print help message and exit"); + * ``` + * + * Notice how `Help` mentions 'this message', but `CompletionHelp` mentions just 'help message'. + * + * See more on completion descriptions codestyle: + * https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide#L43 + */ + TOpt& Help(const TString& help) { + Help_ = help; + return *this; + } + + /** + * Get help string. + */ + const TString& GetHelp() const { + return Help_; + } + + /** + * Set help string that appears when argument completer lists available options. + * + * See `Help` function for info on how this is different from setting `Help` and `CompletionArgHelp`. + * + * Use shorter messages for this message. Don't start them with a capital letter and don't end them + * with a full stop. De aware that argument name and default value will not be printed by completer. + * + * In zsh, these messages will look like this: + * + * ``` + * $ program -<tab><tab> + * -- option -- + * --help -h -- print help message and exit + * --timeout -t -- specify query timeout + * ``` + */ + TOpt& CompletionHelp(const TString& help) { + CompletionHelp_ = help; + return *this; + } + + /** + * Get help string that appears when argument completer lists available options. + */ + const TString& GetCompletionHelp() const { + return CompletionHelp_ ? CompletionHelp_ : Help_; + } + + /** + * Set help string that appears when completer suggests available values. + * + * See `Help` function for info on how this is different from setting `Help` and `CompletionHelp`. + * + * In zsh, these messages will look like this: + * + * ``` + * $ program --timeout <tab><tab> + * -- query timeout (ms) [default=500] -- + * 50 100 250 500 1000 + * ``` + */ + TOpt& CompletionArgHelp(const TString& help) { + CompletionArgHelp_ = help; + return *this; + } + + /** + * @return argument help string for use in completion script. + */ + const TString& GetCompletionArgHelp() const { + return CompletionArgHelp_ ? CompletionArgHelp_ : ArgTitle_; + } + + /** + * Let the completer know that this option can occur more than once. + */ + TOpt& AllowMultipleCompletion(bool allowMultipleCompletion = true) { + AllowMultipleCompletion_ = allowMultipleCompletion; + return *this; + } + + /** + * @return true if completer will offer completion for this option multiple times. + */ + bool MultipleCompletionAllowed() const { + return AllowMultipleCompletion_; + } + + /** + * Tell the completer to disable further completion if this option is present. + * This is useful for options like `--help`. + * + * Note: this only works in zsh. + * + * @return self + */ + TOpt& IfPresentDisableCompletion(bool value = true) { + IfPresentDisableCompletionForOptions(value); + IfPresentDisableCompletionForFreeArgs(value); + return *this; + } + + /** + * Tell the completer to disable completion for all options if this option is already present in the input. + * Free arguments will still be completed. + * + * Note: this only works in zsh. + * + * @return self + */ + TOpt& IfPresentDisableCompletionForOptions(bool value = true) { + DisableCompletionForOptions_ = value; + return *this; + } + + /** + * Tell the completer to disable option `c` if this option is already present in the input. + * For example, if you have two options `-a` and `-r` that are mutually exclusive, disable `-r` for `-a` and + * disable `-a` for `-r`, like this: + * + * ``` + * opts.AddLongOption('a', "acquire").IfPresentDisableCompletionFor('r'); + * opts.AddLongOption('r', "release").IfPresentDisableCompletionFor('a'); + * ``` + * + * This way, if user enabled option `-a`, completer will not suggest option `-r`. + * + * Note that we don't have to disable all flags for a single option. That is, disabling `-r` in the above + * example disables `--release` automatically. + * + * Note: this only works in zsh. + * + * @param c char option that should be disabled when completer hits this option. + */ + TOpt& IfPresentDisableCompletionFor(char c) { + DisableCompletionForChar_.push_back(c); + return *this; + } + + /** + * Like `IfPresentDisableCompletionFor(char c)`, but for long options. + */ + TOpt& IfPresentDisableCompletionFor(const TString& name) { + DisableCompletionForLongName_.push_back(name); + return *this; + } + + /** + * Like `IfPresentDisableCompletionFor(char c)`, but for long options. + */ + TOpt& IfPresentDisableCompletionFor(const TOpt& opt); + + /** + * Tell the completer to disable completion for the given free argument if this option is present. + * + * Note: this only works in zsh. + * + * @param arg index of free arg + */ + TOpt& IfPresentDisableCompletionForFreeArg(size_t index) { + DisableCompletionForFreeArg_.push_back(index); + return *this; + } + + /** + * Assign a completer for this option. + */ + TOpt& Completer(NComp::ICompleterPtr completer) { + Completer_ = std::move(completer); + return *this; + } + + /** + * Tell the completer to disable completion for the all free arguments if this option is present. + * + * Note: this only works in zsh. + */ + TOpt& IfPresentDisableCompletionForFreeArgs(bool value = true) { + DisableCompletionForFreeArgs_ = value; + return *this; + } + + /** + * Run handlers for this option. + */ + void FireHandlers(const TOptsParser* parser) const; + + private: + TOpt& HandlerImpl(IOptHandler* handler) { + Handlers_.push_back(handler); + return *this; + } + + public: + template <typename TpFunc> + TOpt& Handler0(TpFunc func) { // functor taking no parameters + return HandlerImpl(new NPrivate::THandlerFunctor0<TpFunc>(func)); + } + + template <typename TpFunc> + TOpt& Handler1(TpFunc func) { // functor taking one parameter + return HandlerImpl(new NPrivate::THandlerFunctor1<TpFunc>(func)); + } + template <typename TpArg, typename TpFunc> + TOpt& Handler1T(TpFunc func) { + return HandlerImpl(new NPrivate::THandlerFunctor1<TpFunc, TpArg>(func)); + } + template <typename TpArg, typename TpFunc> + TOpt& Handler1T(const TpArg& def, TpFunc func) { + return HandlerImpl(new NPrivate::THandlerFunctor1<TpFunc, TpArg>(func, def)); + } + template <typename TpArg, typename TpArg2, typename TpFunc> + TOpt& Handler1T2(const TpArg2& def, TpFunc func) { + return HandlerImpl(new NPrivate::THandlerFunctor1<TpFunc, TpArg>(func, def)); + } + + TOpt& Handler(void (*f)()) { + return Handler0(f); + } + TOpt& Handler(void (*f)(const TOptsParser*)) { + return Handler1(f); + } + + TOpt& Handler(TAutoPtr<IOptHandler> handler) { + return HandlerImpl(handler.Release()); + } + + template <typename T> // T extends IOptHandler + TOpt& Handler(TAutoPtr<T> handler) { + return HandlerImpl(handler.Release()); + } + + // Stores FromString<T>(arg) in *target + // T maybe anything with FromString<T>(const TStringBuf&) defined + template <typename TpVal, typename T> + TOpt& StoreResultT(T* target) { + return Handler1T<TpVal>(NPrivate::TStoreResultFunctor<T, TpVal>(target)); + } + + template <typename T> + TOpt& StoreResult(T* target) { + return StoreResultT<T>(target); + } + + // Uses TMaybe<T> to store FromString<T>(arg) + template <typename T> + TOpt& StoreResult(TMaybe<T>* target) { + return StoreResultT<T>(target); + } + + template <typename TpVal, typename T, typename TpDef> + TOpt& StoreResultT(T* target, const TpDef& def) { + return Handler1T<TpVal>(def, NPrivate::TStoreResultFunctor<T, TpVal>(target)); + } + + template <typename T, typename TpDef> + TOpt& StoreResult(T* target, const TpDef& def) { + return StoreResultT<T>(target, def); + } + + template <typename T> + TOpt& StoreResultDef(T* target) { + DefaultValue_ = ToString(*target); + return StoreResultT<T>(target, *target); + } + + template <typename T, typename TpDef> + TOpt& StoreResultDef(T* target, const TpDef& def) { + DefaultValue_ = ToString(def); + return StoreResultT<T>(target, def); + } + + // Sugar for storing flags (option without arguments) to boolean vars + TOpt& SetFlag(bool* target) { + return DefaultValue("0").StoreResult(target, true); + } + + // Similar to store_true in Python's argparse + TOpt& StoreTrue(bool* target) { + return NoArgument().SetFlag(target); + } + + template <typename TpVal, typename T, typename TpFunc> + TOpt& StoreMappedResultT(T* target, const TpFunc& func) { + return Handler1T<TpVal>(NPrivate::TStoreMappedResultFunctor<T, TpFunc, TpVal>(target, func)); + } + + template <typename T, typename TpFunc> + TOpt& StoreMappedResult(T* target, const TpFunc& func) { + return StoreMappedResultT<T>(target, func); + } + + // Stores given value in *target if the option is present. + // TValue must be a copyable type, constructible from TParam. + // T must be a copyable type, assignable from TValue. + template <typename TValue, typename T, typename TParam> + TOpt& StoreValueT(T* target, const TParam& value) { + return Handler1(NPrivate::TStoreValueFunctor<T, TValue>(target, value)); + } + + // save value as target type + template <typename T, typename TParam> + TOpt& StoreValue(T* target, const TParam& value) { + return StoreValueT<T>(target, value); + } + + // save value as its original type (2nd template parameter) + template <typename T, typename TValue> + TOpt& StoreValue2(T* target, const TValue& value) { + return StoreValueT<TValue>(target, value); + } + + // Appends FromString<T>(arg) to *target for each argument + template <typename T> + TOpt& AppendTo(TVector<T>* target) { + return Handler1T<T>([target](auto&& value) { target->push_back(std::move(value)); }); + } + + // Appends FromString<T>(arg) to *target for each argument + template <typename T> + TOpt& InsertTo(THashSet<T>* target) { + return Handler1T<T>([target](auto&& value) { target->insert(std::move(value)); }); + } + + // Emplaces TString arg to *target for each argument + template <typename T> + TOpt& EmplaceTo(TVector<T>* target) { + return Handler1T<TString>([target](TString arg) { target->emplace_back(std::move(arg)); } ); + } + + template <class Container> + TOpt& SplitHandler(Container* target, const char delim) { + return Handler(new NLastGetopt::TOptSplitHandler<Container>(target, delim)); + } + + template <class Container> + TOpt& RangeSplitHandler(Container* target, const char elementsDelim, const char rangesDelim) { + return Handler(new NLastGetopt::TOptRangeSplitHandler<Container>(target, elementsDelim, rangesDelim)); + } + + template <class TpFunc> + TOpt& KVHandler(TpFunc func, const char kvdelim = '=') { + return Handler(new NLastGetopt::TOptKVHandler<TpFunc>(func, kvdelim)); + } + }; + + /** + * NLastGetopt::TFreeArgSpec is a storage of data about free argument. + * The data is help information and (maybe) linked named argument. + * + * The help information consists of following: + * help string + * argument name (title) + */ + struct TFreeArgSpec { + TFreeArgSpec() = default; + TFreeArgSpec(const TString& title, const TString& help = TString(), bool optional = false) + : Title_(title) + , Help_(help) + , Optional_(optional) + { + } + + TString Title_; + TString Help_; + TString CompletionArgHelp_; + + bool Optional_ = false; + NComp::ICompleterPtr Completer_ = nullptr; + + /** + * Check if this argument have default values for its title and help. + */ + bool IsDefault() const { + return Title_.empty() && Help_.empty(); + } + + /** + * Set argument title. + */ + TFreeArgSpec& Title(TString title) { + Title_ = std::move(title); + return *this; + } + + /** + * Get argument title. If title is empty, returns a default one. + */ + TStringBuf GetTitle(TStringBuf defaultTitle) const { + return Title_ ? TStringBuf(Title_) : defaultTitle; + } + + /** + * Set help string that appears with `--help`. Unless `CompletionHelp` is given, this message will also be used + * in completion script. In this case, don't make it too long, don't start it with a capital letter and don't + * end it with a full stop. + * + * See `TOpt::Help` function for more on how `Help` and `CompletionArgHelp` differ one from another. + */ + TFreeArgSpec& Help(TString help) { + Help_ = std::move(help); + return *this; + } + + /** + * Get help string that appears with `--help`. + */ + TStringBuf GetHelp() const { + return Help_; + } + + /** + * Set help string that appears when completer suggests values fot this argument. + */ + TFreeArgSpec& CompletionArgHelp(TString completionArgHelp) { + CompletionArgHelp_ = std::move(completionArgHelp); + return *this; + } + + /** + * Get help string that appears when completer suggests values fot this argument. + */ + TStringBuf GetCompletionArgHelp(TStringBuf defaultTitle) const { + return CompletionArgHelp_ ? TStringBuf(CompletionArgHelp_) : GetTitle(defaultTitle); + } + + /** + * Mark this argument as optional. This setting only affects help printing, it doesn't affect parsing. + */ + TFreeArgSpec& Optional(bool optional = true) { + Optional_ = optional; + return *this; + } + + /** + * Check if this argument is optional. + */ + bool IsOptional() const { + return Optional_; + } + + /** + * Set completer for this argument. + */ + TFreeArgSpec& Completer(NComp::ICompleterPtr completer) { + Completer_ = std::move(completer); + return *this; + } + }; +} diff --git a/library/cpp/getopt/small/last_getopt_opts.cpp b/library/cpp/getopt/small/last_getopt_opts.cpp new file mode 100644 index 0000000000..03c432849f --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_opts.cpp @@ -0,0 +1,519 @@ +#include "completer_command.h" +#include "last_getopt_opts.h" +#include "wrap.h" +#include "last_getopt_parser.h" + +#include <library/cpp/colorizer/colors.h> + +#include <util/stream/format.h> +#include <util/charset/utf8.h> + +#include <stdlib.h> + +namespace NLastGetoptPrivate { + TString& VersionString() { + static TString data; + return data; + } + TString& ShortVersionString() { + static TString data; + return data; + } +} + +namespace NLastGetopt { + static const TStringBuf SPad = " "; + + void PrintVersionAndExit(const TOptsParser*) { + Cout << (NLastGetoptPrivate::VersionString() ? NLastGetoptPrivate::VersionString() : "program version: not linked with library/cpp/getopt") << Endl; + exit(NLastGetoptPrivate::VersionString().empty()); + } + + void PrintShortVersionAndExit(const TString& appName) { + Cout << appName << " version " << (NLastGetoptPrivate::ShortVersionString() ? NLastGetoptPrivate::ShortVersionString() : "not linked with library/cpp/getopt") << Endl; + exit(NLastGetoptPrivate::ShortVersionString().empty()); + } + + // Like TString::Quote(), but does not quote digits-only string + static TString QuoteForHelp(const TString& str) { + if (str.empty()) + return str.Quote(); + for (size_t i = 0; i < str.size(); ++i) { + if (!isdigit(str[i])) + return str.Quote(); + } + return str; + } + + namespace NPrivate { + TString OptToString(char c) { + TStringStream ss; + ss << "-" << c; + return ss.Str(); + } + + TString OptToString(const TString& longOption) { + TStringStream ss; + ss << "--" << longOption; + return ss.Str(); + } + + TString OptToString(const TOpt* opt) { + return opt->ToShortString(); + } + } + + TOpts::TOpts(const TStringBuf& optstring) + : ArgPermutation_(DEFAULT_ARG_PERMUTATION) + , AllowSingleDashForLong_(false) + , AllowPlusForLong_(false) + , AllowUnknownCharOptions_(false) + , AllowUnknownLongOptions_(false) + , FreeArgsMin_(0) + , FreeArgsMax_(UNLIMITED_ARGS) + { + if (!optstring.empty()) { + AddCharOptions(optstring); + } + AddVersionOption(0); + } + + void TOpts::AddCharOptions(const TStringBuf& optstring) { + size_t p = 0; + if (optstring[p] == '+') { + ArgPermutation_ = REQUIRE_ORDER; + ++p; + } else if (optstring[p] == '-') { + ArgPermutation_ = RETURN_IN_ORDER; + ++p; + } + + while (p < optstring.size()) { + char c = optstring[p]; + p++; + EHasArg ha = NO_ARGUMENT; + if (p < optstring.size() && optstring[p] == ':') { + ha = REQUIRED_ARGUMENT; + p++; + } + if (p < optstring.size() && optstring[p] == ':') { + ha = OPTIONAL_ARGUMENT; + p++; + } + AddCharOption(c, ha); + } + } + + const TOpt* TOpts::FindLongOption(const TStringBuf& name) const { + for (const auto& Opt : Opts_) { + const TOpt* opt = Opt.Get(); + if (IsIn(opt->GetLongNames(), name)) + return opt; + } + return nullptr; + } + + TOpt* TOpts::FindLongOption(const TStringBuf& name) { + for (auto& Opt : Opts_) { + TOpt* opt = Opt.Get(); + if (IsIn(opt->GetLongNames(), name)) + return opt; + } + return nullptr; + } + + const TOpt* TOpts::FindCharOption(char c) const { + for (const auto& Opt : Opts_) { + const TOpt* opt = Opt.Get(); + if (IsIn(opt->GetShortNames(), c)) + return opt; + } + return nullptr; + } + + TOpt* TOpts::FindCharOption(char c) { + for (auto& Opt : Opts_) { + TOpt* opt = Opt.Get(); + if (IsIn(opt->GetShortNames(), c)) + return opt; + } + return nullptr; + } + + const TOpt& TOpts::GetCharOption(char c) const { + const TOpt* option = FindCharOption(c); + if (!option) + ythrow TException() << "unknown char option '" << c << "'"; + return *option; + } + + TOpt& TOpts::GetCharOption(char c) { + TOpt* option = FindCharOption(c); + if (!option) + ythrow TException() << "unknown char option '" << c << "'"; + return *option; + } + + const TOpt& TOpts::GetLongOption(const TStringBuf& name) const { + const TOpt* option = FindLongOption(name); + if (!option) + ythrow TException() << "unknown option " << name; + return *option; + } + + TOpt& TOpts::GetLongOption(const TStringBuf& name) { + TOpt* option = FindLongOption(name); + if (!option) + ythrow TException() << "unknown option " << name; + return *option; + } + + bool TOpts::HasAnyShortOption() const { + for (const auto& Opt : Opts_) { + const TOpt* opt = Opt.Get(); + if (!opt->GetShortNames().empty()) + return true; + } + return false; + } + + bool TOpts::HasAnyLongOption() const { + for (const auto& Opt : Opts_) { + TOpt* opt = Opt.Get(); + if (!opt->GetLongNames().empty()) + return true; + } + return false; + } + + void TOpts::Validate() const { + for (TOptsVector::const_iterator i = Opts_.begin(); i != Opts_.end(); ++i) { + TOpt* opt = i->Get(); + const TOpt::TShortNames& shortNames = opt->GetShortNames(); + for (auto c : shortNames) { + for (TOptsVector::const_iterator j = i + 1; j != Opts_.end(); ++j) { + TOpt* nextOpt = j->Get(); + if (nextOpt->CharIs(c)) + ythrow TConfException() << "option " + << NPrivate::OptToString(c) + << " is defined more than once"; + } + } + const TOpt::TLongNames& longNames = opt->GetLongNames(); + for (const auto& longName : longNames) { + for (TOptsVector::const_iterator j = i + 1; j != Opts_.end(); ++j) { + TOpt* nextOpt = j->Get(); + if (nextOpt->NameIs(longName)) + ythrow TConfException() << "option " + << NPrivate::OptToString(longName) + << " is defined more than once"; + } + } + } + if (FreeArgsMax_ < FreeArgsMin_) { + ythrow TConfException() << "FreeArgsMax must be >= FreeArgsMin"; + } + if (!FreeArgSpecs_.empty() && FreeArgSpecs_.rbegin()->first >= FreeArgsMax_) { + ythrow TConfException() << "Described args count is greater than FreeArgsMax. Either increase FreeArgsMax or remove unreachable descriptions"; + } + } + + TOpt& TOpts::AddOption(const TOpt& option) { + if (option.GetShortNames().empty() && option.GetLongNames().empty()) + ythrow TConfException() << "bad option: no chars, no long names"; + Opts_.push_back(new TOpt(option)); + return *Opts_.back(); + } + + TOpt& TOpts::AddCompletionOption(TString command, TString longName) { + if (TOpt* o = FindLongOption(longName)) { + return *o; + } + + return AddOption(MakeCompletionOpt(this, std::move(command), std::move(longName))); + } + + namespace { + auto MutuallyExclusiveHandler(const TOpt* cur, const TOpt* other) { + return [cur, other](const TOptsParser* p) { + if (p->Seen(other)) { + throw TUsageException() + << "option " << cur->ToShortString() + << " can't appear together with option " << other->ToShortString(); + } + }; + } + } + + void TOpts::MutuallyExclusiveOpt(TOpt& opt1, TOpt& opt2) { + opt1.Handler1(MutuallyExclusiveHandler(&opt1, &opt2)) + .IfPresentDisableCompletionFor(opt2); + opt2.Handler1(MutuallyExclusiveHandler(&opt2, &opt1)) + .IfPresentDisableCompletionFor(opt1); + } + + size_t TOpts::IndexOf(const TOpt* opt) const { + TOptsVector::const_iterator it = std::find(Opts_.begin(), Opts_.end(), opt); + if (it == Opts_.end()) + ythrow TException() << "unknown option"; + return it - Opts_.begin(); + } + + TStringBuf TOpts::GetFreeArgTitle(size_t pos) const { + if (FreeArgSpecs_.contains(pos)) { + return FreeArgSpecs_.at(pos).GetTitle(DefaultFreeArgTitle_); + } + return DefaultFreeArgTitle_; + } + + void TOpts::SetFreeArgTitle(size_t pos, const TString& title, const TString& help, bool optional) { + FreeArgSpecs_[pos] = TFreeArgSpec(title, help, optional); + } + + TFreeArgSpec& TOpts::GetFreeArgSpec(size_t pos) { + return FreeArgSpecs_[pos]; + } + + static TString FormatOption(const TOpt* option, const NColorizer::TColors& colors) { + TStringStream result; + const TOpt::TShortNames& shorts = option->GetShortNames(); + const TOpt::TLongNames& longs = option->GetLongNames(); + + const size_t nopts = shorts.size() + longs.size(); + const bool multiple = 1 < nopts; + if (multiple) + result << '{'; + for (size_t i = 0; i < nopts; ++i) { + if (multiple && 0 != i) + result << '|'; + + if (i < shorts.size()) // short + result << colors.GreenColor() << '-' << shorts[i] << colors.OldColor(); + else + result << colors.GreenColor() << "--" << longs[i - shorts.size()] << colors.OldColor(); + } + if (multiple) + result << '}'; + + static const TString metavarDef("VAL"); + const TString& title = option->GetArgTitle(); + const TString& metavar = title.empty() ? metavarDef : title; + + if (option->GetHasArg() == OPTIONAL_ARGUMENT) { + result << " [" << metavar; + if (option->HasOptionalValue()) + result << ':' << option->GetOptionalValue(); + result << ']'; + } else if (option->GetHasArg() == REQUIRED_ARGUMENT) + result << ' ' << metavar; + else + Y_ASSERT(option->GetHasArg() == NO_ARGUMENT); + + return result.Str(); + } + + void TOpts::PrintCmdLine(const TStringBuf& program, IOutputStream& os, const NColorizer::TColors& colors) const { + os << colors.BoldColor() << "Usage" << colors.OldColor() << ": "; + if (CustomUsage) { + os << CustomUsage; + } else { + os << program << " "; + } + if (CustomCmdLineDescr) { + os << CustomCmdLineDescr << Endl; + return; + } + os << "[OPTIONS]"; + + ui32 numDescribedFlags = FreeArgSpecs_.empty() ? 0 : FreeArgSpecs_.rbegin()->first + 1; + ui32 numArgsToShow = Max(FreeArgsMin_, FreeArgsMax_ == UNLIMITED_ARGS ? numDescribedFlags : FreeArgsMax_); + + for (ui32 i = 0, nonOptionalFlagsPrinted = 0; i < numArgsToShow; ++i) { + bool isOptional = nonOptionalFlagsPrinted >= FreeArgsMin_ || FreeArgSpecs_.Value(i, TFreeArgSpec()).Optional_; + + nonOptionalFlagsPrinted += !isOptional; + + os << " "; + + if (isOptional) + os << "["; + + os << GetFreeArgTitle(i); + + if (isOptional) + os << "]"; + } + + if (FreeArgsMax_ == UNLIMITED_ARGS) { + os << " [" << TrailingArgSpec_.GetTitle(DefaultFreeArgTitle_) << "]..."; + } + + os << Endl; + } + + void TOpts::PrintUsage(const TStringBuf& program, IOutputStream& osIn, const NColorizer::TColors& colors) const { + TStringStream os; + + if (!Title.empty()) + os << Title << "\n\n"; + + PrintCmdLine(program, os, colors); + + TVector<TString> leftColumn(Opts_.size()); + TVector<size_t> leftColumnSizes(leftColumn.size()); + const size_t kMaxLeftWidth = 25; + size_t leftWidth = 0; + size_t requiredOptionsCount = 0; + NColorizer::TColors disabledColors(false); + + for (size_t i = 0; i < Opts_.size(); i++) { + const TOpt* opt = Opts_[i].Get(); + if (opt->IsHidden()) + continue; + leftColumn[i] = FormatOption(opt, colors); + size_t leftColumnSize = leftColumn[i].size(); + if (colors.IsTTY()) { + leftColumnSize -= NColorizer::TotalAnsiEscapeCodeLen(leftColumn[i]); + } + leftColumnSizes[i] = leftColumnSize; + if (leftColumnSize <= kMaxLeftWidth) { + leftWidth = Max(leftWidth, leftColumnSize); + } + if (opt->IsRequired()) + requiredOptionsCount++; + } + + const TString leftPadding(leftWidth, ' '); + + for (size_t sectionId = 0; sectionId <= 1; sectionId++) { + bool requiredOptionsSection = (sectionId == 0); + + if (requiredOptionsSection) { + if (requiredOptionsCount == 0) + continue; + os << Endl << colors.BoldColor() << "Required parameters" << colors.OldColor() << ":" << Endl; + } else { + if (requiredOptionsCount == Opts_.size()) + continue; + if (requiredOptionsCount == 0) + os << Endl << colors.BoldColor() << "Options" << colors.OldColor() << ":" << Endl; + else + os << Endl << colors.BoldColor() << "Optional parameters" << colors.OldColor() << ":" << Endl; // optional options would be a tautology + } + + for (size_t i = 0; i < Opts_.size(); i++) { + const TOpt* opt = Opts_[i].Get(); + + if (opt->IsHidden()) + continue; + if (opt->IsRequired() != requiredOptionsSection) + continue; + + if (leftColumnSizes[i] > leftWidth && !opt->GetHelp().empty()) { + os << SPad << leftColumn[i] << Endl << SPad << leftPadding << ' '; + } else { + os << SPad << leftColumn[i] << ' '; + if (leftColumnSizes[i] < leftWidth) + os << TStringBuf(leftPadding.data(), leftWidth - leftColumnSizes[i]); + } + + TStringBuf help = opt->GetHelp(); + while (help && isspace(help.back())) { + help.Chop(1); + } + size_t lastLineLength = 0; + bool helpHasParagraphs = false; + if (help) { + os << Wrap(Wrap_, help, SPad + leftPadding + " ", &lastLineLength, &helpHasParagraphs); + } + + if (opt->HasDefaultValue()) { + auto quotedDef = QuoteForHelp(opt->GetDefaultValue()); + if (helpHasParagraphs) { + os << Endl << Endl << SPad << leftPadding << " "; + os << "Default: " << colors.CyanColor() << quotedDef << colors.OldColor() << "."; + } else if (help.EndsWith('.')) { + os << Endl << SPad << leftPadding << " "; + os << "Default: " << colors.CyanColor() << quotedDef << colors.OldColor() << "."; + } else if (help) { + if (SPad.size() + leftWidth + 1 + lastLineLength + 12 + quotedDef.size() > Wrap_) { + os << Endl << SPad << leftPadding << " "; + } else { + os << " "; + } + os << "(default: " << colors.CyanColor() << quotedDef << colors.OldColor() << ")"; + } else { + os << "default: " << colors.CyanColor() << quotedDef << colors.OldColor(); + } + } + + os << Endl; + + if (helpHasParagraphs) { + os << Endl; + } + } + } + + PrintFreeArgsDesc(os, colors); + + for (auto& [heading, text] : Sections) { + os << Endl << colors.BoldColor() << heading << colors.OldColor() << ":" << Endl; + + os << SPad << Wrap(Wrap_, text, SPad) << Endl; + } + + osIn << os.Str(); + } + + void TOpts::PrintUsage(const TStringBuf& program, IOutputStream& os) const { + PrintUsage(program, os, NColorizer::AutoColors(os)); + } + + void TOpts::PrintFreeArgsDesc(IOutputStream& os, const NColorizer::TColors& colors) const { + if (0 == FreeArgsMax_) + return; + + size_t leftFreeWidth = 0; + for (size_t i = 0; i < FreeArgSpecs_.size(); ++i) { + leftFreeWidth = Max(leftFreeWidth, GetFreeArgTitle(i).size()); + } + + if (!TrailingArgSpec_.IsDefault()) { + leftFreeWidth = Max(leftFreeWidth, TrailingArgSpec_.GetTitle(DefaultFreeArgTitle_).size()); + } + + leftFreeWidth = Min(leftFreeWidth, size_t(30)); + os << Endl << colors.BoldColor() << "Free args" << colors.OldColor() << ":"; + + os << " min: " << colors.GreenColor() << FreeArgsMin_ << colors.OldColor() << ","; + os << " max: " << colors.GreenColor(); + if (FreeArgsMax_ != UNLIMITED_ARGS) { + os << FreeArgsMax_; + } else { + os << "unlimited"; + } + os << colors.OldColor() << Endl; + + const size_t limit = FreeArgSpecs_.empty() ? 0 : FreeArgSpecs_.rbegin()->first; + for (size_t i = 0; i <= limit; ++i) { + if (!FreeArgSpecs_.contains(i)) { + continue; + } + + if (auto help = FreeArgSpecs_.at(i).GetHelp()) { + auto title = GetFreeArgTitle(i); + os << SPad << colors.GreenColor() << RightPad(title, leftFreeWidth, ' ') << colors.OldColor() + << SPad << help << Endl; + } + } + + if (FreeArgsMax_ == UNLIMITED_ARGS) { + auto title = TrailingArgSpec_.GetTitle(DefaultFreeArgTitle_); + if (auto help = TrailingArgSpec_.GetHelp()) { + os << SPad << colors.GreenColor() << RightPad(title, leftFreeWidth, ' ') << colors.OldColor() + << SPad << help << Endl; + } + } + } +} diff --git a/library/cpp/getopt/small/last_getopt_opts.h b/library/cpp/getopt/small/last_getopt_opts.h new file mode 100644 index 0000000000..825b99c871 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_opts.h @@ -0,0 +1,643 @@ +#pragma once + +#include "last_getopt_opt.h" + +#include <library/cpp/colorizer/fwd.h> + +#include <util/generic/map.h> + +namespace NLastGetopt { + enum EArgPermutation { + REQUIRE_ORDER, + PERMUTE, + RETURN_IN_ORDER, + DEFAULT_ARG_PERMUTATION = PERMUTE + }; + + /** + * NLastGetopt::TOpts is a storage of program options' parse rules. + * It contains information about all options, free args, some parsing options + * and rules about interaction between options. + * + * The main point for defining program options. + * + * The parsing rules determined by the following parts: + * - Arguments permutation. It is expected free args be after named args. + * This point adjusts how to treat breaking this expectation. + * if REQUIRE_ORDER is choosen, the exception during parsing will be raised, + * the special string " -- " will be treated as end of named + * options: all options after it will be parsed as free args + * if PERMUTE is choosen, arguments will be rearranged in correct order, + * if RETURN_IN_ORDER is choosen, all free args will be ommited (TODO: looks very strange) + * - Using '+' as a prefix instead '--' for long names + * - Using "-" as a prefix for both short and long names + * - Allowing unknown options + * + */ + class TOpts { + friend class TOptsParseResult; + friend class TOptsParser; + + public: + static constexpr const ui32 UNLIMITED_ARGS = Max<ui32>(); + + typedef TVector<TSimpleSharedPtr<TOpt>> TOptsVector; + TOptsVector Opts_; // infomation about named (short and long) options + TVector<std::function<void(TStringBuf)>> ArgBindings_; + + EArgPermutation ArgPermutation_ = DEFAULT_ARG_PERMUTATION; // determines how to parse positions of named and free options. See information below. + bool AllowSingleDashForLong_ = false; // + bool AllowPlusForLong_ = false; // using '+' instead '--' for long options + + //Allows unknwon options: + bool AllowUnknownCharOptions_ = false; + bool AllowUnknownLongOptions_ = false; + + ui32 Wrap_ = 80; + + private: + ui32 FreeArgsMin_; // minimal number of free args + ui32 FreeArgsMax_; // maximal number of free args + + TMap<ui32, TFreeArgSpec> FreeArgSpecs_; // mapping [free arg position] -> [free arg specification] + TFreeArgSpec TrailingArgSpec_; // spec for the trailing argument (when arguments are unlimited) + TString DefaultFreeArgTitle_ = "ARG"; // title that's used for free args without a title + + TString Title; // title of the help string + TString CustomCmdLineDescr; // user defined help string + TString CustomUsage; // user defined usage string + + TVector<std::pair<TString, TString>> Sections; // additional help entries to print after usage + + public: + /** + * Constructs TOpts from string as in getopt(3) + */ + TOpts(const TStringBuf& optstring = TStringBuf()); + + /** + * Constructs TOpts from string as in getopt(3) and + * additionally adds help option (for '?') and svn-verstion option (for 'V') + */ + static TOpts Default(const TStringBuf& optstring = TStringBuf()) { + TOpts opts(optstring); + opts.AddHelpOption(); + opts.AddVersionOption(); + return opts; + } + + /** + * Checks correctness of options' descriptions. + * Throws TConfException if validation failed. + * Check consist of: + * -not intersecting of names + * -compability of settings, that responsable for freeArgs parsing + */ + void Validate() const; + + /** + * Search for the option with given long name + * @param name long name for search + * @return ptr on result (nullptr if not found) + */ + const TOpt* FindLongOption(const TStringBuf& name) const; + + /** + * Search for the option with given short name + * @param c short name for search + * @return ptr on result (nullptr if not found) + */ + const TOpt* FindCharOption(char c) const; + + /** + * Search for the option with given long name + * @param name long name for search + * @return ptr on result (nullptr if not found) + */ + TOpt* FindLongOption(const TStringBuf& name); + + /** + * Search for the option with given short name + * @param c short name for search + * @return ptr on result (nullptr if not found) + */ + TOpt* FindCharOption(char c); + + /** + * Search for the option with given name + * @param name name for search + * @return ptr on result (nullptr if not found) + */ + /// @{ + + const TOpt* FindOption(const TStringBuf& name) const { + return FindLongOption(name); + } + + TOpt* FindOption(const TStringBuf& name) { + return FindLongOption(name); + } + + const TOpt* FindOption(char c) const { + return FindCharOption(c); + } + + TOpt* FindOption(char c) { + return FindCharOption(c); + } + + /// @} + + /** + * Sets title of the help string + * @param title title to set + */ + void SetTitle(const TString& title) { + Title = title; + } + + /** + * @return true if there is an option with given long name + * + * @param name long name for search + */ + bool HasLongOption(const TString& name) const { + return FindLongOption(name) != nullptr; + } + + /** + * @return true if there is an option with given short name + * + * @param char short name for search + */ + bool HasCharOption(char c) const { + return FindCharOption(c) != nullptr; + } + + /** + * Search for the option with given long name + * @param name long name for search + * @return ref on result (throw exception if not found) + */ + const TOpt& GetLongOption(const TStringBuf& name) const; + + /** + * Search for the option with given long name + * @param name long name for search + * @return ref on result (throw exception if not found) + */ + TOpt& GetLongOption(const TStringBuf& name); + + /** + * Search for the option with given short name + * @param c short name for search + * @return ref on result (throw exception if not found) + */ + const TOpt& GetCharOption(char c) const; + + /** + * Search for the option with given short name + * @param c short name for search + * @return ref on result (throw exception if not found) + */ + TOpt& GetCharOption(char c); + + /** + * Search for the option with given name + * @param name name for search + * @return ref on result (throw exception if not found) + */ + /// @{ + + const TOpt& GetOption(const TStringBuf& name) const { + return GetLongOption(name); + } + + TOpt& GetOption(const TStringBuf& name) { + return GetLongOption(name); + } + + const TOpt& GetOption(char c) const { + return GetCharOption(c); + } + + TOpt& GetOption(char c) { + return GetCharOption(c); + } + + /// @} + + /** + * @return true if short options exist + */ + bool HasAnyShortOption() const; + + /** + * @return true if long options exist + */ + bool HasAnyLongOption() const; + + /** + * Creates new [option description (TOpt)] as a copy of given one + * @param option source + * @return reference for created option + */ + TOpt& AddOption(const TOpt& option); + + /** + * Creates new free argument handling + * @param name name of free arg to show in help + * @param target variable address to store parsing result into + * @param help help string to show in help + */ + template <typename T> + void AddFreeArgBinding(const TString& name, T& target, const TString& help = "") { + ArgBindings_.emplace_back([&target](TStringBuf value) { + target = FromString<T>(value); + }); + + FreeArgsMax_ = Max<ui32>(FreeArgsMax_, ArgBindings_.size()); + SetFreeArgTitle(ArgBindings_.size() - 1, name, help); + } + + /** + * Creates options list from string as in getopt(3) + * + * @param optstring source + */ + void AddCharOptions(const TStringBuf& optstring); + + /** + * Creates new [option description (TOpt)] with given short name and given help string + * + * @param c short name + * @param help help string + * @return reference for created option + */ + TOpt& AddCharOption(char c, const TString& help = "") { + return AddCharOption(c, DEFAULT_HAS_ARG, help); + } + + /** + * Creates new [option description (TOpt)] with given short name and given help string + * + * @param c short name + * @param help help string + * @return reference for created option + */ + TOpt& AddCharOption(char c, EHasArg hasArg, const TString& help = "") { + TOpt option; + option.AddShortName(c); + option.Help(help); + option.HasArg(hasArg); + return AddOption(option); + } + + /** + * Creates new [option description (TOpt)] with given long name and given help string + * + * @param name long name + * @param help help string + * @return reference for created option + */ + TOpt& AddLongOption(const TString& name, const TString& help = "") { + return AddLongOption(0, name, help); + } + + /** + * Creates new [option description (TOpt)] with given long and short names and given help string + * + * @param c short name + * @param name long name + * @param help help string + * @return reference for created option + */ + TOpt& AddLongOption(char c, const TString& name, const TString& help = "") { + TOpt option; + if (c != 0) + option.AddShortName(c); + option.AddLongName(name); + option.Help(help); + return AddOption(option); + } + + /** + * Creates new [option description (TOpt)] for help printing, + * adds appropriate handler for it + * If "help" option already exist, will add given short name to it. + * + * @param c new short name for help option + */ + TOpt& AddHelpOption(char c = '?') { + if (TOpt* o = FindLongOption("help")) { + if (!o->CharIs(c)) + o->AddShortName(c); + return *o; + } + return AddLongOption(c, "help", "print usage") + .HasArg(NO_ARGUMENT) + .IfPresentDisableCompletion() + .Handler(&PrintUsageAndExit); + } + + /** + * Creates new [option description (TOpt)] for svn-revision printing, + * adds appropriate handler for it. + * If "svnversion" option already exist, will add given short name to it. + * + * @param c new short name for "svnversion" option + */ + TOpt& AddVersionOption(char c = 'V') { + if (TOpt* o = FindLongOption("svnrevision")) { + if (!o->CharIs(c)) + o->AddShortName(c); + return *o; + } + return AddLongOption(c, "svnrevision", "print svn version") + .HasArg(NO_ARGUMENT) + .IfPresentDisableCompletion() + .Handler(&PrintVersionAndExit); + } + + /** + * Creates new option for generating completion shell scripts. + * + * @param command name of command that should be completed (typically corresponds to the executable name). + */ + TOpt& AddCompletionOption(TString command, TString longName = "completion"); + + /** + * Creates or finds option with given short name + * + * @param c new short name for search/create + */ + TOpt& CharOption(char c) { + const TOpt* opt = FindCharOption(c); + if (opt != nullptr) { + return const_cast<TOpt&>(*opt); + } else { + AddCharOption(c); + return const_cast<TOpt&>(GetCharOption(c)); + } + } + + /** + * Indicate that some options can't appear together. + * + * Note: this is not transitive. + * + * Note: don't use this on options with default values. If option with default value wasn't specified, + * parser will run handlers for default value, thus triggering a false-positive exclusivity check. + */ + template <typename T1, typename T2> + void MutuallyExclusive(T1&& opt1, T2&& opt2) { + MutuallyExclusiveOpt(GetOption(std::forward<T1>(opt1)), GetOption(std::forward<T2>(opt2))); + } + + /** + * Like `MutuallyExclusive`, but accepts `TOpt`s instead of option names. + */ + void MutuallyExclusiveOpt(TOpt& opt1, TOpt& opt2); + + /** + * @return index of option + * + * @param opt pointer of option to search + */ + size_t IndexOf(const TOpt* opt) const; + + /** + * Replace help string with given + * + * @param decr new help string + */ + void SetCmdLineDescr(const TString& descr) { + CustomCmdLineDescr = descr; + } + + /** + * Replace usage string with given + * + * @param usage new usage string + */ + void SetCustomUsage(const TString& usage) { + CustomUsage = usage; + } + + /** + * Add a section to print after the main usage spec. + */ + void AddSection(TString title, TString text) { + Sections.emplace_back(std::move(title), std::move(text)); + } + + /** + * Add section with examples. + * + * @param examples text of this section + */ + void SetExamples(TString examples) { + AddSection("Examples", std::move(examples)); + } + + /** + * Set minimal number of free args + * + * @param min new value + */ + void SetFreeArgsMin(size_t min) { + FreeArgsMin_ = ui32(min); + } + + + /** + * Get current minimal number of free args + */ + ui32 GetFreeArgsMin() const { + return FreeArgsMin_; + } + + /** + * Set maximal number of free args + * + * @param max new value + */ + void SetFreeArgsMax(size_t max) { + FreeArgsMax_ = ui32(max); + FreeArgsMax_ = Max<ui32>(FreeArgsMax_, ArgBindings_.size()); + } + + /** + * Get current maximal number of free args + */ + ui32 GetFreeArgsMax() const { + return FreeArgsMax_; + } + + /** + * Get mapping for free args + */ + const TMap<ui32, TFreeArgSpec>& GetFreeArgSpecs() const { + return FreeArgSpecs_; + } + + /** + * Set exact expected number of free args + * + * @param count new value + */ + void SetFreeArgsNum(size_t count) { + FreeArgsMin_ = ui32(count); + FreeArgsMax_ = ui32(count); + } + + /** + * Set minimal and maximal number of free args + * + * @param min new value for minimal + * @param max new value for maximal + */ + void SetFreeArgsNum(size_t min, size_t max) { + FreeArgsMin_ = ui32(min); + FreeArgsMax_ = ui32(max); + } + + /** + * Set title and help string of free argument + * + * @param pos index of argument + * @param title new value for argument title + * @param help new value for help string + * @param optional indicates that the flag's help string should be rendered as for optional flag; + * does not affect actual flags parsing + */ + void SetFreeArgTitle(size_t pos, const TString& title, const TString& help = TString(), bool optional = false); + + /** + * Get free argument's spec for further modification. + */ + TFreeArgSpec& GetFreeArgSpec(size_t pos); + + /** + * Legacy, don't use. Same as `SetTrailingArgTitle`. + * Older versions of lastgetopt didn't have destinction between default title and title + * for the trailing argument. + */ + void SetFreeArgDefaultTitle(const TString& title, const TString& help = TString()) { + SetTrailingArgTitle(title, help); + } + + /** + * Set default title that will be used for all arguments that have no title. + */ + void SetDefaultFreeArgTitle(TString title) { + DefaultFreeArgTitle_ = std::move(title); + } + + /** + * Set default title that will be used for all arguments that have no title. + */ + const TString& GetDefaultFreeArgTitle() const { + return DefaultFreeArgTitle_; + } + + /** + * Set title and help for the trailing argument. + * + * This title and help are used to render the last repeated argument when max number of arguments is unlimited. + */ + /// @{ + void SetTrailingArgTitle(TString title) { + TrailingArgSpec_.Title(std::move(title)); + } + void SetTrailingArgTitle(TString title, TString help) { + TrailingArgSpec_.Title(std::move(title)); + TrailingArgSpec_.Help(std::move(help)); + } + /// @} + + /** + * Get spec for the trailing argument. + * + * This spec is used to render the last repeated argument when max number of arguments is unlimited. + */ + /// @{ + TFreeArgSpec& GetTrailingArgSpec() { + return TrailingArgSpec_; + } + const TFreeArgSpec& GetTrailingArgSpec() const { + return TrailingArgSpec_; + } + /// @} + + /** + * Set the rule of parsing single dash as prefix of long names + * + * @param value new value of the option + */ + void SetAllowSingleDashForLong(bool value) { + AllowSingleDashForLong_ = value; + } + + /** + * Wrap help text at this number of characters. 0 to disable wrapping. + */ + void SetWrap(ui32 wrap = 80) { + Wrap_ = wrap; + } + + /** + * Print usage string + * + * @param program prefix of result (path to the program) + * @param os destination stream + * @param colors colorizer + */ + void PrintUsage(const TStringBuf& program, IOutputStream& os, const NColorizer::TColors& colors) const; + + /** + * Print usage string + * + * @param program prefix of result (path to the program) + * @param os destination stream + */ + void PrintUsage(const TStringBuf& program, IOutputStream& os = Cout) const; + + /** + * Get list of options in order of definition. + */ + TVector<const TOpt*> GetOpts() const { + auto ret = TVector<const TOpt*>(Reserve(Opts_.size())); + for (auto& opt : Opts_) { + ret.push_back(opt.Get()); + } + return ret; + } + + private: + /** + * @return argument title of a free argument + * + * @param pos position of the argument + */ + TStringBuf GetFreeArgTitle(size_t pos) const; + + /** + * Print usage helper + * + * @param program prefix of result (path to the program) + * @param os destination stream + * @param colors colorizer + */ + void PrintCmdLine(const TStringBuf& program, IOutputStream& os, const NColorizer::TColors& colors) const; + + /** + * Print usage helper + * + * @param os destination stream + * @param colors colorizer + */ + void PrintFreeArgsDesc(IOutputStream& os, const NColorizer::TColors& colors) const; + }; + +} diff --git a/library/cpp/getopt/small/last_getopt_parse_result.cpp b/library/cpp/getopt/small/last_getopt_parse_result.cpp new file mode 100644 index 0000000000..f4b5607a90 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_parse_result.cpp @@ -0,0 +1,160 @@ +#include "last_getopt_parse_result.h" + +namespace NLastGetopt { + const TOptParseResult* TOptsParseResult::FindParseResult(const TdVec& vec, const TOpt* opt) { + for (const auto& r : vec) { + if (r.OptPtr() == opt) + return &r; + } + return nullptr; + } + + const TOptParseResult* TOptsParseResult::FindOptParseResult(const TOpt* opt, bool includeDefault) const { + const TOptParseResult* r = FindParseResult(Opts_, opt); + if (nullptr == r && includeDefault) + r = FindParseResult(OptsDef_, opt); + return r; + } + + const TOptParseResult* TOptsParseResult::FindLongOptParseResult(const TString& name, bool includeDefault) const { + return FindOptParseResult(&Parser_->Opts_->GetLongOption(name), includeDefault); + } + + const TOptParseResult* TOptsParseResult::FindCharOptParseResult(char c, bool includeDefault) const { + return FindOptParseResult(&Parser_->Opts_->GetCharOption(c), includeDefault); + } + + bool TOptsParseResult::Has(const TOpt* opt, bool includeDefault) const { + Y_ASSERT(opt); + return FindOptParseResult(opt, includeDefault) != nullptr; + } + + bool TOptsParseResult::Has(const TString& name, bool includeDefault) const { + return FindLongOptParseResult(name, includeDefault) != nullptr; + } + + bool TOptsParseResult::Has(char c, bool includeDefault) const { + return FindCharOptParseResult(c, includeDefault) != nullptr; + } + + const char* TOptsParseResult::Get(const TOpt* opt, bool includeDefault) const { + Y_ASSERT(opt); + const TOptParseResult* r = FindOptParseResult(opt, includeDefault); + if (!r || r->Empty()) { + try { + throw TUsageException() << "option " << opt->ToShortString() << " is unspecified"; + } catch (...) { + HandleError(); + // unreachable + throw; + } + } else { + return r->Back(); + } + } + + const char* TOptsParseResult::GetOrElse(const TOpt* opt, const char* defaultValue) const { + Y_ASSERT(opt); + const TOptParseResult* r = FindOptParseResult(opt); + if (!r || r->Empty()) { + return defaultValue; + } else { + return r->Back(); + } + } + + const char* TOptsParseResult::Get(const TString& name, bool includeDefault) const { + return Get(&Parser_->Opts_->GetLongOption(name), includeDefault); + } + + const char* TOptsParseResult::Get(char c, bool includeDefault) const { + return Get(&Parser_->Opts_->GetCharOption(c), includeDefault); + } + + const char* TOptsParseResult::GetOrElse(const TString& name, const char* defaultValue) const { + if (!Has(name)) + return defaultValue; + return Get(name); + } + + const char* TOptsParseResult::GetOrElse(char c, const char* defaultValue) const { + if (!Has(c)) + return defaultValue; + return Get(c); + } + + TOptParseResult& TOptsParseResult::OptParseResult() { + const TOpt* opt = Parser_->CurOpt(); + Y_ASSERT(opt); + TdVec& opts = Parser_->IsExplicit() ? Opts_ : OptsDef_; + if (Parser_->IsExplicit()) // default options won't appear twice + for (auto& it : opts) + if (it.OptPtr() == opt) + return it; + opts.push_back(TOptParseResult(opt)); + return opts.back(); + } + + TString TOptsParseResult::GetProgramName() const { + return Parser_->ProgramName_; + } + + void TOptsParseResult::PrintUsage(IOutputStream& os) const { + Parser_->Opts_->PrintUsage(Parser_->ProgramName_, os); + } + + size_t TOptsParseResult::GetFreeArgsPos() const { + return Parser_->Pos_; + } + + TVector<TString> TOptsParseResult::GetFreeArgs() const { + TVector<TString> v; + for (size_t i = GetFreeArgsPos(); i < Parser_->Argc_; ++i) { + v.push_back(Parser_->Argv_[i]); + } + return v; + } + + size_t TOptsParseResult::GetFreeArgCount() const { + return Parser_->Argc_ - GetFreeArgsPos(); + } + + void TOptsParseResult::Init(const TOpts* options, int argc, const char** argv) { + try { + Parser_.Reset(new TOptsParser(options, argc, argv)); + while (Parser_->Next()) { + TOptParseResult& r = OptParseResult(); + r.AddValue(Parser_->CurValOrOpt().data()); + } + + Y_ENSURE(options); + const auto freeArgs = GetFreeArgs(); + for (size_t i = 0; i < freeArgs.size(); ++i) { + if (i >= options->ArgBindings_.size()) { + break; + } + + options->ArgBindings_[i](freeArgs[i]); + } + } catch (...) { + HandleError(); + } + } + + void TOptsParseResult::HandleError() const { + Cerr << CurrentExceptionMessage() << Endl; + if (Parser_.Get()) { // parser initializing can fail (and we get here, see Init) + if (Parser_->Opts_->FindLongOption("help") != nullptr) { + Cerr << "Try '" << Parser_->ProgramName_ << " --help' for more information." << Endl; + } else { + PrintUsage(); + } + } + exit(1); + } + + void TOptsParseResultException::HandleError() const { + throw; + } + +} diff --git a/library/cpp/getopt/small/last_getopt_parse_result.h b/library/cpp/getopt/small/last_getopt_parse_result.h new file mode 100644 index 0000000000..1ab6f598c9 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_parse_result.h @@ -0,0 +1,321 @@ +#pragma once + +#include "last_getopt_opts.h" +#include "last_getopt_parser.h" + +namespace NLastGetopt { + /** + * NLastGetopt::TOptParseResult contains all arguments for exactly one TOpt, + * that have been fetched during parsing + * + * The class is a wraper over a vector of nil-terminated strings. + */ + class TOptParseResult { + public: + typedef TVector<const char*> TValues; + + public: + TOptParseResult(const TOpt* opt = nullptr) + : Opt_(opt) + { + } + + public: + const TOpt& Opt() const { + return *Opt_; + } + const TOpt* OptPtr() const { + return Opt_; + } + const TValues& Values() const { + return Values_; + } + bool Empty() const { + return Values().empty(); + } + size_t Count() const { + return Values_.size(); + } + void AddValue(const char* val) { + if (nullptr != val) + Values_.push_back(val); + } + const char* DefVal(const char* def = nullptr) const { + return Opt().HasDefaultValue() ? Opt().GetDefaultValue().c_str() : def; + } + const char* Front(const char* def = nullptr) const { + return Empty() ? DefVal(def) : Values().front(); + } + const char* Back(const char* def = nullptr) const { + return Empty() ? DefVal(def) : Values().back(); + } + + private: + const TOpt* Opt_; + TValues Values_; + }; + + /** + * NLastGetopt::TOptsParseResult contains result of parsing argc,argv be parser. + * + * In most common case constructed by argc,argv pair and rules (TOpts). + * The instance being constructed validates rules and performs parsing, stores result for futher access. + * + * If error during parsing occures, the program aborts with exit code 1. + * Note, that if PERMUTE mode is on, then data, pointed by argv can be changed. + */ + class TOptsParseResult { + private: + THolder<TOptsParser> Parser_; //The instance of parser. + + // XXX: make argc, argv + typedef TVector<TOptParseResult> TdVec; + + TdVec Opts_; //Parsing result for all options, that have been explicitly defined in argc/argv + TdVec OptsDef_; //Parsing result for options, that have been defined by default values only + + private: + TOptParseResult& OptParseResult(); + + /** + * Searchs for object in given container + * + * @param vec container + * @param opt ptr for required object + * + * @retunr ptr on corresponding TOptParseResult + */ + static const TOptParseResult* FindParseResult(const TdVec& vec, const TOpt* opt); + + protected: + /** + * Performs parsing of comand line arguments. + */ + void Init(const TOpts* options, int argc, const char** argv); + + TOptsParseResult() = default; + + public: + /** + * The action in case of parser failure. + * Allows to asjust behavior in derived classes. + * By default prints error string and aborts the program + */ + virtual void HandleError() const; + + /** + * Constructs object by parsing arguments with given rules + * + * @param options ptr on parsing rules + * @param argc + * @param argv + */ + TOptsParseResult(const TOpts* options, int argc, const char* argv[]) { + Init(options, argc, argv); + } + + /** + * Constructs object by parsing arguments with given rules + * + * @param options ptr on parsing rules + * @param argc + * @param argv + */ + TOptsParseResult(const TOpts* options, int argc, char* argv[]) { + Init(options, argc, const_cast<const char**>(argv)); + } + + virtual ~TOptsParseResult() = default; + + /** + * Search for TOptParseResult that corresponds to given option (TOpt) + * + * @param opt ptr on required object + * @param includeDefault search in results obtained from default values + * + * @return ptr on result + */ + const TOptParseResult* FindOptParseResult(const TOpt* opt, bool includeDefault = false) const; + + /** + * Search for TOptParseResult that corresponds to given long name + * + * @param name long name of required object + * @param includeDefault search in results obtained from default values + * + * @return ptr on result + */ + const TOptParseResult* FindLongOptParseResult(const TString& name, bool includeDefault = false) const; + + /** + * Search for TOptParseResult that corresponds to given short name + * + * @param c short name of required object + * @param includeDefault search in results obtained from default values + * + * @return ptr on result + */ + const TOptParseResult* FindCharOptParseResult(char c, bool includeDefault = false) const; + + /** + * @return argv[0] + */ + TString GetProgramName() const; + + /** + * Print usage string. + */ + void PrintUsage(IOutputStream& os = Cout) const; + + /** + * @return position in [premuted argv] of the first free argument + */ + size_t GetFreeArgsPos() const; + + /** + * @return number of fetched free arguments + */ + size_t GetFreeArgCount() const; + + /** + * @return all fetched free arguments + */ + TVector<TString> GetFreeArgs() const; + + /** + * @return true if given option exist in results of parsing + * + * @param opt ptr on required object + * @param includeDefault search in results obtained from default values + * + */ + bool Has(const TOpt* opt, bool includeDefault = false) const; + + /** + * @return nil terminated string on the last fetched argument of givne option + * + * @param opt ptr on required object + * @param includeDefault search in results obtained from default values + */ + const char* Get(const TOpt* opt, bool includeDefault = true) const; + + /** + * @return nil terminated string on the last fetched argument of givne option + * if option haven't been fetched, given defaultValue will be returned + * + * @param opt ptr on required object + * @param defaultValue + */ + const char* GetOrElse(const TOpt* opt, const char* defaultValue) const; + + /** + * @return true if given option exist in results of parsing + * + * @param name long name of required object + * @param includeDefault search in results obtained from default values + * + */ + bool Has(const TString& name, bool includeDefault = false) const; + + /** + * @return nil terminated string on the last fetched argument of givne option + * + * @param name long name of required object + * @param includeDefault search in results obtained from default values + */ + const char* Get(const TString& name, bool includeDefault = true) const; + + /** + * @return nil terminated string on the last fetched argument of givne option + * if option haven't been fetched, given defaultValue will be returned + * + * @param name long name of required object + * @param defaultValue + */ + const char* GetOrElse(const TString& name, const char* defaultValue) const; + + /** + * @return true if given option exist in results of parsing + * + * @param c short name of required object + * @param includeDefault search in results obtained from default values + * + */ + bool Has(char name, bool includeDefault = false) const; + + /** + * @return nil terminated string on the last fetched argument of givne option + * + * @param c short name of required object + * @param includeDefault search in results obtained from default values + */ + const char* Get(char name, bool includeDefault = true) const; + + /** + * @return nil terminated string on the last fetched argument of givne option + * if option haven't been fetched, given defaultValue will be returned + * + * @param c short name of required object + * @param defaultValue + */ + const char* GetOrElse(char name, const char* defaultValue) const; + + /** + * for givne option return parsed value of the last fetched argument + * if option haven't been fetched, HandleError action is called + * + * @param opt required option (one of: ptr, short name, long name). + * + * @return FromString<T>(last feteched argument) + */ + template <typename T, typename TKey> + T Get(const TKey opt) const { + const char* value = Get(opt); + try { + return NPrivate::OptFromString<T>(value, opt); + } catch (...) { + HandleError(); + throw; + } + } + + /** + * for givne option return parsed value of the last fetched argument + * if option haven't been fetched, given defaultValue will be returned + * + * @param opt required option (one of: ptr, short name, long name). + * @param defaultValue + * + * @return FromString<T>(last feteched argument) + */ + template <typename T, typename TKey> + T GetOrElse(const TKey opt, const T& defaultValue) const { + if (Has(opt)) + return Get<T>(opt); + else + return defaultValue; + } + }; + + /** + * NLastGetopt::TOptsParseResultException contains result of parsing argc,argv be parser. + * + * Unlike TOptsParseResult, if error during parsing occures, an exception is thrown. + * + */ + class TOptsParseResultException: public TOptsParseResult { + public: + TOptsParseResultException(const TOpts* options, int argc, const char* argv[]) { + Init(options, argc, argv); + } + TOptsParseResultException(const TOpts* options, int argc, char* argv[]) { + Init(options, argc, const_cast<const char**>(argv)); + } + virtual ~TOptsParseResultException() = default; + void HandleError() const override; + + protected: + TOptsParseResultException() = default; + }; + +} diff --git a/library/cpp/getopt/small/last_getopt_parser.cpp b/library/cpp/getopt/small/last_getopt_parser.cpp new file mode 100644 index 0000000000..7668b12a03 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_parser.cpp @@ -0,0 +1,389 @@ +#include "last_getopt_parser.h" + +#include <library/cpp/colorizer/colors.h> + +#include <util/string/escape.h> + +namespace NLastGetopt { + void TOptsParser::Init(const TOpts* opts, int argc, const char* argv[]) { + opts->Validate(); + + Opts_ = opts; + + if (argc < 1) + throw TUsageException() << "argv must have at least one argument"; + + Argc_ = argc; + Argv_ = argv; + + ProgramName_ = argv[0]; + + Pos_ = 1; + Sop_ = 0; + CurrentOpt_ = nullptr; + CurrentValue_ = nullptr; + GotMinusMinus_ = false; + Stopped_ = false; + OptsSeen_.clear(); + OptsDefault_.clear(); + } + + void TOptsParser::Init(const TOpts* opts, int argc, char* argv[]) { + Init(opts, argc, const_cast<const char**>(argv)); + } + + void TOptsParser::Swap(TOptsParser& that) { + DoSwap(Opts_, that.Opts_); + DoSwap(Argc_, that.Argc_); + DoSwap(Argv_, that.Argv_); + DoSwap(TempCurrentOpt_, that.TempCurrentOpt_); + DoSwap(ProgramName_, that.ProgramName_); + DoSwap(Pos_, that.Pos_); + DoSwap(Sop_, that.Sop_); + DoSwap(Stopped_, that.Stopped_); + DoSwap(CurrentOpt_, that.CurrentOpt_); + DoSwap(CurrentValue_, that.CurrentValue_); + DoSwap(GotMinusMinus_, that.GotMinusMinus_); + DoSwap(OptsSeen_, that.OptsSeen_); + } + + bool TOptsParser::Commit(const TOpt* currentOpt, const TStringBuf& currentValue, size_t pos, size_t sop) { + Pos_ = pos; + Sop_ = sop; + CurrentOpt_ = currentOpt; + CurrentValue_ = currentValue; + if (nullptr != currentOpt) + OptsSeen_.insert(currentOpt); + return true; + } + + bool TOptsParser::CommitEndOfOptions(size_t pos) { + Pos_ = pos; + Sop_ = 0; + Y_ASSERT(!CurOpt()); + Y_ASSERT(!CurVal()); + + Y_ASSERT(!Stopped_); + + if (Opts_->FreeArgsMin_ == Opts_->FreeArgsMax_ && Argc_ - Pos_ != Opts_->FreeArgsMin_) + throw TUsageException() << "required exactly " << Opts_->FreeArgsMin_ << " free args"; + else if (Argc_ - Pos_ < Opts_->FreeArgsMin_) + throw TUsageException() << "required at least " << Opts_->FreeArgsMin_ << " free args"; + else if (Argc_ - Pos_ > Opts_->FreeArgsMax_) + throw TUsageException() << "required at most " << Opts_->FreeArgsMax_ << " free args"; + + return false; + } + + bool TOptsParser::ParseUnknownShortOptWithinArg(size_t pos, size_t sop) { + Y_ASSERT(pos < Argc_); + const TStringBuf arg(Argv_[pos]); + Y_ASSERT(sop > 0); + Y_ASSERT(sop < arg.length()); + Y_ASSERT(EIO_NONE != IsOpt(arg)); + + if (!Opts_->AllowUnknownCharOptions_) + throw TUsageException() << "unknown option '" << EscapeC(arg[sop]) + << "' in '" << arg << "'"; + + TempCurrentOpt_.Reset(new TOpt); + TempCurrentOpt_->AddShortName(arg[sop]); + + sop += 1; + + // mimic behavior of Opt: unknown option has arg only if char is last within arg + if (sop < arg.length()) { + return Commit(TempCurrentOpt_.Get(), nullptr, pos, sop); + } + + pos += 1; + sop = 0; + if (pos == Argc_ || EIO_NONE != IsOpt(Argv_[pos])) { + return Commit(TempCurrentOpt_.Get(), nullptr, pos, 0); + } + + return Commit(TempCurrentOpt_.Get(), Argv_[pos], pos + 1, 0); + } + + bool TOptsParser::ParseShortOptWithinArg(size_t pos, size_t sop) { + Y_ASSERT(pos < Argc_); + const TStringBuf arg(Argv_[pos]); + Y_ASSERT(sop > 0); + Y_ASSERT(sop < arg.length()); + Y_ASSERT(EIO_NONE != IsOpt(arg)); + + size_t p = sop; + char c = arg[p]; + const TOpt* opt = Opts_->FindCharOption(c); + if (!opt) + return ParseUnknownShortOptWithinArg(pos, sop); + p += 1; + if (p == arg.length()) { + return ParseOptParam(opt, pos + 1); + } + if (opt->GetHasArg() == NO_ARGUMENT) { + return Commit(opt, nullptr, pos, p); + } + return Commit(opt, arg.SubStr(p), pos + 1, 0); + } + + bool TOptsParser::ParseShortOptArg(size_t pos) { + Y_ASSERT(pos < Argc_); + const TStringBuf arg(Argv_[pos]); + Y_ASSERT(EIO_NONE != IsOpt(arg)); + Y_ASSERT(!arg.StartsWith("--")); + return ParseShortOptWithinArg(pos, 1); + } + + bool TOptsParser::ParseOptArg(size_t pos) { + Y_ASSERT(pos < Argc_); + TStringBuf arg(Argv_[pos]); + const EIsOpt eio = IsOpt(arg); + Y_ASSERT(EIO_NONE != eio); + if (EIO_DDASH == eio || EIO_PLUS == eio || (Opts_->AllowSingleDashForLong_ || !Opts_->HasAnyShortOption())) { + // long option + bool singleCharPrefix = EIO_DDASH != eio; + arg.Skip(singleCharPrefix ? 1 : 2); + TStringBuf optionName = arg.NextTok('='); + const TOpt* option = Opts_->FindLongOption(optionName); + if (!option) { + if (singleCharPrefix && !arg.IsInited()) { + return ParseShortOptArg(pos); + } else if (Opts_->AllowUnknownLongOptions_) { + return false; + } else { + throw TUsageException() << "unknown option '" << optionName + << "' in '" << Argv_[pos] << "'"; + } + } + if (arg.IsInited()) { + if (option->GetHasArg() == NO_ARGUMENT) + throw TUsageException() << "option " << optionName << " must have no arg"; + return Commit(option, arg, pos + 1, 0); + } + ++pos; + return ParseOptParam(option, pos); + } else { + return ParseShortOptArg(pos); + } + } + + bool TOptsParser::ParseOptParam(const TOpt* opt, size_t pos) { + Y_ASSERT(opt); + if (opt->GetHasArg() == NO_ARGUMENT) { + return Commit(opt, nullptr, pos, 0); + } + if (pos == Argc_) { + if (opt->GetHasArg() == REQUIRED_ARGUMENT) + throw TUsageException() << "option " << opt->ToShortString() << " must have arg"; + return Commit(opt, nullptr, pos, 0); + } + const TStringBuf arg(Argv_[pos]); + if (!arg.StartsWith('-') || opt->GetHasArg() == REQUIRED_ARGUMENT) { + return Commit(opt, arg, pos + 1, 0); + } + return Commit(opt, nullptr, pos, 0); + } + + TOptsParser::EIsOpt TOptsParser::IsOpt(const TStringBuf& arg) const { + EIsOpt eio = EIO_NONE; + if (1 < arg.length()) { + switch (arg[0]) { + default: + break; + case '-': + if ('-' != arg[1]) + eio = EIO_SDASH; + else if (2 < arg.length()) + eio = EIO_DDASH; + break; + case '+': + if (Opts_->AllowPlusForLong_) + eio = EIO_PLUS; + break; + } + } + return eio; + } + + static void memrotate(void* ptr, size_t size, size_t shift) { + TTempBuf buf(shift); + memcpy(buf.Data(), (char*)ptr + size - shift, shift); + memmove((char*)ptr + shift, ptr, size - shift); + memcpy(ptr, buf.Data(), shift); + } + + bool TOptsParser::ParseWithPermutation() { + Y_ASSERT(Sop_ == 0); + Y_ASSERT(Opts_->ArgPermutation_ == PERMUTE); + + const size_t p0 = Pos_; + + size_t pc = Pos_; + + for (; pc < Argc_ && EIO_NONE == IsOpt(Argv_[pc]); ++pc) { + // count non-args + } + + if (pc == Argc_) { + return CommitEndOfOptions(Pos_); + } + + Pos_ = pc; + + bool r = ParseOptArg(Pos_); + Y_ASSERT(r); + while (Pos_ == pc) { + Y_ASSERT(Sop_ > 0); + r = ParseShortOptWithinArg(Pos_, Sop_); + Y_ASSERT(r); + } + + size_t p2 = Pos_; + + Y_ASSERT(p2 - pc >= 1); + Y_ASSERT(p2 - pc <= 2); + + memrotate(Argv_ + p0, (p2 - p0) * sizeof(void*), (p2 - pc) * sizeof(void*)); + + bool r2 = ParseOptArg(p0); + Y_ASSERT(r2); + return r2; + } + + bool TOptsParser::DoNext() { + Y_ASSERT(Pos_ <= Argc_); + + if (Pos_ == Argc_) + return CommitEndOfOptions(Pos_); + + if (GotMinusMinus_ && Opts_->ArgPermutation_ == RETURN_IN_ORDER) { + Y_ASSERT(Sop_ == 0); + return Commit(nullptr, Argv_[Pos_], Pos_ + 1, 0); + } + + if (Sop_ > 0) + return ParseShortOptWithinArg(Pos_, Sop_); + + size_t pos = Pos_; + const TStringBuf arg(Argv_[pos]); + if (EIO_NONE != IsOpt(arg)) { + return ParseOptArg(pos); + } else if (arg == "--") { + if (Opts_->ArgPermutation_ == RETURN_IN_ORDER) { + pos += 1; + if (pos == Argc_) + return CommitEndOfOptions(pos); + GotMinusMinus_ = true; + return Commit(nullptr, Argv_[pos], pos + 1, 0); + } else { + return CommitEndOfOptions(pos + 1); + } + } else if (Opts_->ArgPermutation_ == RETURN_IN_ORDER) { + return Commit(nullptr, arg, pos + 1, 0); + } else if (Opts_->ArgPermutation_ == REQUIRE_ORDER) { + return CommitEndOfOptions(Pos_); + } else { + return ParseWithPermutation(); + } + } + + bool TOptsParser::Next() { + bool r = false; + + if (OptsDefault_.empty()) { + CurrentOpt_ = nullptr; + TempCurrentOpt_.Destroy(); + + CurrentValue_ = nullptr; + + if (Stopped_) + return false; + + TOptsParser copy = *this; + + r = copy.DoNext(); + + Swap(copy); + + if (!r) { + Stopped_ = true; + // we are done; check for missing options + Finish(); + } + } + + if (!r && !OptsDefault_.empty()) { + CurrentOpt_ = OptsDefault_.front(); + CurrentValue_ = CurrentOpt_->GetDefaultValue(); + OptsDefault_.pop_front(); + r = true; + } + + if (r) { + if (CurOpt()) + CurOpt()->FireHandlers(this); + } + + return r; + } + + void TOptsParser::Finish() { + const TOpts::TOptsVector& optvec = Opts_->Opts_; + if (optvec.size() == OptsSeen_.size()) + return; + + TVector<TString> missingLong; + TVector<char> missingShort; + + TOpts::TOptsVector::const_iterator it; + for (it = optvec.begin(); it != optvec.end(); ++it) { + const TOpt* opt = (*it).Get(); + if (nullptr == opt) + continue; + if (OptsSeen_.contains(opt)) + continue; + + if (opt->IsRequired()) { + const TOpt::TLongNames& optnames = opt->GetLongNames(); + if (!optnames.empty()) + missingLong.push_back(optnames[0]); + else { + const char ch = opt->GetCharOr0(); + if (0 != ch) + missingShort.push_back(ch); + } + continue; + } + + if (opt->HasDefaultValue()) + OptsDefault_.push_back(opt); + } + + // also indicates subsequent options, if any, haven't been seen actually + OptsSeen_.clear(); + + const size_t nmissing = missingLong.size() + missingShort.size(); + if (0 == nmissing) + return; + + TUsageException usage; + usage << "The following option"; + usage << ((1 == nmissing) ? " is" : "s are"); + usage << " required:"; + for (size_t i = 0; i != missingLong.size(); ++i) + usage << " --" << missingLong[i]; + for (size_t i = 0; i != missingShort.size(); ++i) + usage << " -" << missingShort[i]; + throw usage; // don't need lineinfo, just the message + } + + void TOptsParser::PrintUsage(IOutputStream& os, const NColorizer::TColors& colors) const { + Opts_->PrintUsage(ProgramName(), os, colors); + } + + void TOptsParser::PrintUsage(IOutputStream& os) const { + PrintUsage(os, NColorizer::AutoColors(os)); + } + +} diff --git a/library/cpp/getopt/small/last_getopt_parser.h b/library/cpp/getopt/small/last_getopt_parser.h new file mode 100644 index 0000000000..2cf8a6c308 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_parser.h @@ -0,0 +1,154 @@ +#pragma once + +#include "last_getopt_opts.h" + +#include <library/cpp/colorizer/fwd.h> + +#include <util/generic/hash_set.h> +#include <util/generic/list.h> + +namespace NLastGetopt { + /** +* NLastGetopt::TOptsParser is an implementation of parsing +* argv/argv into TOptsParseResult by rules of TOpts. +* +* The class allows to make complicated handlers. +* Note, that if PERMUTE mode is on, then data, pointed by argv can be changed. +*/ + class TOptsParser { + enum EIsOpt { + EIO_NONE, //is not an option name + EIO_SDASH, //single-dashed ('-c') option name + EIO_DDASH, //double-dashed ("--opt") option name + EIO_PLUS, //plus prefix ("+opt") option name + }; + + public: // TODO: make private + const TOpts* Opts_; //rules of parsing + + // argc/argv pair + size_t Argc_; + const char** Argv_; + + private: + //the storage of last unkown options. TODO: can be moved to local-method scope + TCopyPtr<TOpt> TempCurrentOpt_; + + public: + //storage of argv[0] + TString ProgramName_; + + //state of parsing: + + size_t Pos_; // current element withing argv + size_t Sop_; // current char within arg + bool Stopped_; + bool GotMinusMinus_; //true if "--" have been seen in argv + + protected: + const TOpt* CurrentOpt_; // ptr on the last meeted option + TStringBuf CurrentValue_; // the value of the last met argument (corresponding to CurrentOpt_) + + private: + typedef THashSet<const TOpt*> TdOptSet; + TdOptSet OptsSeen_; //the set of options that have been met during parsing + + TList<const TOpt*> OptsDefault_; + + private: + void Init(const TOpts* options, int argc, const char* argv[]); + void Init(const TOpts* options, int argc, char* argv[]); + + bool CommitEndOfOptions(size_t pos); + bool Commit(const TOpt* currentOption, const TStringBuf& currentValue, size_t pos, size_t sop); + + bool ParseShortOptArg(size_t pos); + bool ParseOptArg(size_t pos); + bool ParseOptParam(const TOpt* opt, size_t pos); + bool ParseUnknownShortOptWithinArg(size_t pos, size_t sop); + bool ParseShortOptWithinArg(size_t pos, size_t sop); + bool ParseWithPermutation(); + + bool DoNext(); + void Finish(); + + EIsOpt IsOpt(const TStringBuf& arg) const; + + void Swap(TOptsParser& that); + + public: + TOptsParser(const TOpts* options, int argc, const char* argv[]) { + Init(options, argc, argv); + } + + TOptsParser(const TOpts* options, int argc, char* argv[]) { + Init(options, argc, argv); + } + + /// fetch next argument, false if no more arguments left + bool Next(); + + bool Seen(const TOpt* opt) const { + return OptsSeen_.contains(opt); + } + + bool Seen(TStringBuf name) const { + if (auto opt = Opts_->FindLongOption(name)) { + return Seen(opt); + } else { + return false; + } + } + + bool Seen(char name) const { + if (auto opt = Opts_->FindCharOption(name)) { + return Seen(opt); + } else { + return false; + } + } + + const TOpt* CurOpt() const { + return CurrentOpt_; + } + + const char* CurVal() const { + return CurrentValue_.data(); + } + + const TStringBuf& CurValStr() const { + return CurrentValue_; + } + + TStringBuf CurValOrOpt() const { + TStringBuf val(CurValStr()); + if (!val.IsInited() && CurOpt()->HasOptionalValue()) + val = CurOpt()->GetOptionalValue(); + return val; + } + + TStringBuf CurValOrDef(bool useDef = true) const { + TStringBuf val(CurValOrOpt()); + if (!val.IsInited() && useDef && CurOpt()->HasDefaultValue()) + val = CurOpt()->GetDefaultValue(); + return val; + } + + // true if this option was actually specified by the user + bool IsExplicit() const { + return nullptr == CurrentOpt_ || !OptsSeen_.empty(); + } + + bool CurrentIs(const TString& name) const { + return CurOpt()->NameIs(name); + } + + const TString& ProgramName() const { + return ProgramName_; + } + + void PrintUsage(IOutputStream& os = Cout) const; + + void PrintUsage(IOutputStream& os, const NColorizer::TColors& colors) const; + }; +} //namespace NLastGetopt diff --git a/library/cpp/getopt/small/last_getopt_support.h b/library/cpp/getopt/small/last_getopt_support.h new file mode 100644 index 0000000000..17bed3e614 --- /dev/null +++ b/library/cpp/getopt/small/last_getopt_support.h @@ -0,0 +1,178 @@ +#pragma once + +#include <util/string/cast.h> +#include <util/generic/string.h> +#include <util/generic/vector.h> +#include <util/generic/utility.h> +#include <util/generic/yexception.h> + +namespace NLastGetopt { + class TOpt; + class TOpts; + class TOptsParser; + class TOptsParseResult; + + /// base of all getopt exceptions + class TException: public yexception { + }; + + /// TOpts configuration is incorrect + class TConfException: public TException { + }; + + /// User passed incorrect arguments, parsing failed + /// Note: use `throw TUsageException()` instead of `ythrow TUsageException()` to prevent appearence of stacktrace + /// and location of the `ythrow` statment in error messages. + class TUsageException: public TException { + }; + + struct IOptHandler { + virtual void HandleOpt(const TOptsParser* parser) = 0; + virtual ~IOptHandler() = default; + }; + + namespace NPrivate { + template <typename TpFunc> + class THandlerFunctor0 + : public IOptHandler { + TpFunc Func_; + + public: + THandlerFunctor0(TpFunc func) + : Func_(func) + { + } + + void HandleOpt(const TOptsParser*) override { + Func_(); + } + }; + + template <typename TpFunc, typename TpArg = const TOptsParser*> + class THandlerFunctor1 + : public IOptHandler { + TpFunc Func_; + const TpArg Def_; + const bool HasDef_; + + public: + THandlerFunctor1(TpFunc func) + : Func_(func) + , Def_() + , HasDef_(false) + { + } + + template <typename T> + THandlerFunctor1(const TpFunc& func, const T& def) + : Func_(func) + , Def_(def) + , HasDef_(true) + { + } + + void HandleOpt(const TOptsParser* parser) override; + }; + + template <typename TpFunc> + class THandlerFunctor1<TpFunc, const TOptsParser*> + : public IOptHandler { + TpFunc Func_; + + public: + THandlerFunctor1(TpFunc func) + : Func_(func) + { + } + + void HandleOpt(const TOptsParser* parser) override { + Func_(parser); + } + }; + + template <typename T, typename TpVal = T> + class TStoreResultFunctor { + private: + T* Target_; + + public: + TStoreResultFunctor(T* target) + : Target_(target) + { + } + + void operator()(const TpVal& val) { + *Target_ = val; + } + }; + + template <typename TpTarget, typename TpFunc, typename TpVal = TpTarget> + class TStoreMappedResultFunctor { + private: + TpTarget* Target_; + const TpFunc Func_; + + public: + TStoreMappedResultFunctor(TpTarget* target, const TpFunc& func) + : Target_(target) + , Func_(func) + { + } + + void operator()(const TpVal& val) { + *Target_ = Func_(val); + } + }; + + template <typename T, typename TpVal = T> + class TStoreValueFunctor { + T* Target; + const TpVal Value; + + public: + template <typename TpArg> + TStoreValueFunctor(T* target, const TpArg& value) + : Target(target) + , Value(value) + { + } + + void operator()(const TOptsParser*) { + *Target = Value; + } + }; + + TString OptToString(char c); + TString OptToString(const TString& longOption); + TString OptToString(const TOpt* opt); + + template <typename T> + inline T OptFromStringImpl(const TStringBuf& value) { + return FromString<T>(value); + } + + template <> + inline TStringBuf OptFromStringImpl<TStringBuf>(const TStringBuf& value) { + return value; + } + + template <> + inline const char* OptFromStringImpl<const char*>(const TStringBuf& value) { + return value.data(); + } + + template <typename T, typename TSomeOpt> + T OptFromString(const TStringBuf& value, const TSomeOpt opt) { + try { + return OptFromStringImpl<T>(value); + } catch (...) { + throw TUsageException() << "failed to parse opt " << OptToString(opt) << " value " << TString(value).Quote() << ": " << CurrentExceptionMessage(); + } + } + + // wrapper of FromString<T> that prints nice message about option used + template <typename T, typename TSomeOpt> + T OptFromString(const TStringBuf& value, const TSomeOpt opt); + + } +} diff --git a/library/cpp/getopt/small/modchooser.cpp b/library/cpp/getopt/small/modchooser.cpp new file mode 100644 index 0000000000..2fa5cfd070 --- /dev/null +++ b/library/cpp/getopt/small/modchooser.cpp @@ -0,0 +1,372 @@ +#include "completer.h" +#include "completer_command.h" +#include "completion_generator.h" +#include "last_getopt.h" +#include "modchooser.h" + +#include <library/cpp/colorizer/colors.h> + +#include <util/stream/output.h> +#include <util/stream/format.h> +#include <util/generic/yexception.h> +#include <util/generic/ptr.h> +#include <util/string/builder.h> +#include <util/string/join.h> + +class PtrWrapper: public TMainClass { +public: + explicit PtrWrapper(const TMainFunctionPtr main) + : Main(main) + { + } + + int operator()(const int argc, const char** argv) override { + return Main(argc, argv); + } + +private: + TMainFunctionPtr Main; +}; + +class PtrvWrapper: public TMainClass { +public: + explicit PtrvWrapper(const TMainFunctionPtrV main) + : Main(main) + { + } + + int operator()(const int argc, const char** argv) override { + TVector<TString> nargv(argv, argv + argc); + return Main(nargv); + } + +private: + TMainFunctionPtrV Main; +}; + +class ClassWrapper: public TMainClass { +public: + explicit ClassWrapper(TMainClassV* main) + : Main(main) + { + } + + int operator()(const int argc, const char** argv) override { + TVector<TString> nargv(argv, argv + argc); + return (*Main)(nargv); + } + +private: + TMainClassV* Main; +}; + +TModChooser::TMode::TMode(const TString& name, TMainClass* main, const TString& descr, bool hidden, bool noCompletion) + : Name(name) + , Main(main) + , Description(descr) + , Hidden(hidden) + , NoCompletion(noCompletion) +{ +} + +TModChooser::TModChooser() + : ModesHelpOption("-?") // Default help option in last_getopt + , VersionHandler(nullptr) + , ShowSeparated(true) + , SvnRevisionOptionDisabled(false) + , PrintShortCommandInUsage(false) +{ +} + +TModChooser::~TModChooser() = default; + +void TModChooser::AddMode(const TString& mode, const TMainFunctionRawPtr func, const TString& description, bool hidden, bool noCompletion) { + AddMode(mode, TMainFunctionPtr(func), description, hidden, noCompletion); +} + +void TModChooser::AddMode(const TString& mode, const TMainFunctionRawPtrV func, const TString& description, bool hidden, bool noCompletion) { + AddMode(mode, TMainFunctionPtrV(func), description, hidden, noCompletion); +} + +void TModChooser::AddMode(const TString& mode, const TMainFunctionPtr func, const TString& description, bool hidden, bool noCompletion) { + Wrappers.push_back(MakeHolder<PtrWrapper>(func)); + AddMode(mode, Wrappers.back().Get(), description, hidden, noCompletion); +} + +void TModChooser::AddMode(const TString& mode, const TMainFunctionPtrV func, const TString& description, bool hidden, bool noCompletion) { + Wrappers.push_back(MakeHolder<PtrvWrapper>(func)); + AddMode(mode, Wrappers.back().Get(), description, hidden, noCompletion); +} + +void TModChooser::AddMode(const TString& mode, TMainClass* func, const TString& description, bool hidden, bool noCompletion) { + if (Modes.FindPtr(mode)) { + ythrow yexception() << "TMode '" << mode << "' already exists in TModChooser."; + } + + Modes[mode] = UnsortedModes.emplace_back(MakeHolder<TMode>(mode, func, description, hidden, noCompletion)).Get(); +} + +void TModChooser::AddMode(const TString& mode, TMainClassV* func, const TString& description, bool hidden, bool noCompletion) { + Wrappers.push_back(MakeHolder<ClassWrapper>(func)); + AddMode(mode, Wrappers.back().Get(), description, hidden, noCompletion); +} + +void TModChooser::AddGroupModeDescription(const TString& description, bool hidden, bool noCompletion) { + UnsortedModes.push_back(MakeHolder<TMode>(TString(), nullptr, description.data(), hidden, noCompletion)); +} + +void TModChooser::SetDefaultMode(const TString& mode) { + DefaultMode = mode; +} + +void TModChooser::AddAlias(const TString& alias, const TString& mode) { + if (!Modes.FindPtr(mode)) { + ythrow yexception() << "TMode '" << mode << "' not found in TModChooser."; + } + + Modes[mode]->Aliases.push_back(alias); + Modes[alias] = Modes[mode]; +} + +void TModChooser::SetDescription(const TString& descr) { + Description = descr; +} + +void TModChooser::SetModesHelpOption(const TString& helpOption) { + ModesHelpOption = helpOption; +} + +void TModChooser::SetVersionHandler(TVersionHandlerPtr handler) { + VersionHandler = handler; +} + +void TModChooser::SetSeparatedMode(bool separated) { + ShowSeparated = separated; +} + +void TModChooser::SetSeparationString(const TString& str) { + SeparationString = str; +} + +void TModChooser::SetPrintShortCommandInUsage(bool printShortCommandInUsage = false) { + PrintShortCommandInUsage = printShortCommandInUsage; +} + +void TModChooser::DisableSvnRevisionOption() { + SvnRevisionOptionDisabled = true; +} + +void TModChooser::AddCompletions(TString progName, const TString& name, bool hidden, bool noCompletion) { + if (CompletionsGenerator == nullptr) { + CompletionsGenerator = NLastGetopt::MakeCompletionMod(this, std::move(progName), name); + AddMode(name, CompletionsGenerator.Get(), "generate autocompletion files", hidden, noCompletion); + } +} + +int TModChooser::Run(const int argc, const char** argv) const { + Y_ENSURE(argc, "Can't run TModChooser with empty list of arguments."); + + bool shiftArgs = true; + TString modeName; + if (argc == 1) { + if (DefaultMode.empty()) { + PrintHelp(argv[0]); + return 0; + } else { + modeName = DefaultMode; + shiftArgs = false; + } + } else { + modeName = argv[1]; + } + + if (modeName == "-h" || modeName == "--help" || modeName == "-?") { + PrintHelp(argv[0]); + return 0; + } + if (VersionHandler && (modeName == "-v" || modeName == "--version")) { + VersionHandler(); + return 0; + } + if (!SvnRevisionOptionDisabled && modeName == "--svnrevision") { + NLastGetopt::PrintVersionAndExit(nullptr); + } + + auto modeIter = Modes.find(modeName); + if (modeIter == Modes.end() && !DefaultMode.empty()) { + modeIter = Modes.find(DefaultMode); + shiftArgs = false; + } + + if (modeIter == Modes.end()) { + Cerr << "Unknown mode " << modeName.Quote() << "." << Endl; + PrintHelp(argv[0]); + return 1; + } + + if (shiftArgs) { + TString firstArg; + TVector<const char*> nargv(Reserve(argc)); + + if (PrintShortCommandInUsage) { + firstArg = modeIter->second->Name; + } else { + firstArg = argv[0] + TString(" ") + modeIter->second->Name; + } + + nargv.push_back(firstArg.data()); + + for (int i = 2; i < argc; ++i) { + nargv.push_back(argv[i]); + } + // According to the standard, "argv[argc] shall be a null pointer" (5.1.2.2.1). + // http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1336 + nargv.push_back(nullptr); + + return (*modeIter->second->Main)(nargv.size() - 1, nargv.data()); + } else { + return (*modeIter->second->Main)(argc, argv); + } +} + +int TModChooser::Run(const TVector<TString>& argv) const { + TVector<const char*> nargv(Reserve(argv.size() + 1)); + for (auto& arg : argv) { + nargv.push_back(arg.c_str()); + } + // According to the standard, "argv[argc] shall be a null pointer" (5.1.2.2.1). + // http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1336 + nargv.push_back(nullptr); + + return Run(nargv.size() - 1, nargv.data()); +} + +size_t TModChooser::TMode::CalculateFullNameLen() const { + size_t len = Name.size(); + if (Aliases) { + len += 2; + for (auto& alias : Aliases) { + len += alias.size() + 1; + } + } + return len; +} + +TString TModChooser::TMode::FormatFullName(size_t pad) const { + TStringBuilder name; + if (Aliases) { + name << "{"; + } + + name << NColorizer::StdErr().GreenColor(); + name << Name; + name << NColorizer::StdErr().OldColor(); + + if (Aliases) { + for (const auto& alias : Aliases) { + name << "|" << NColorizer::StdErr().GreenColor() << alias << NColorizer::StdErr().OldColor(); + } + name << "}"; + } + + auto len = CalculateFullNameLen(); + if (pad > len) { + name << TString(" ") * (pad - len); + } + + return name; +} + +void TModChooser::PrintHelp(const TString& progName) const { + Cerr << Description << Endl << Endl; + Cerr << NColorizer::StdErr().BoldColor() << "Usage" << NColorizer::StdErr().OldColor() << ": " << progName << " MODE [MODE_OPTIONS]" << Endl; + Cerr << Endl; + Cerr << NColorizer::StdErr().BoldColor() << "Modes" << NColorizer::StdErr().OldColor() << ":" << Endl; + size_t maxModeLen = 0; + for (const auto& [name, mode] : Modes) { + if (name != mode->Name) + continue; // this is an alias + maxModeLen = Max(maxModeLen, mode->CalculateFullNameLen()); + } + + if (ShowSeparated) { + for (const auto& unsortedMode : UnsortedModes) + if (!unsortedMode->Hidden) { + if (unsortedMode->Name.size()) { + Cerr << " " << unsortedMode->FormatFullName(maxModeLen + 4) << unsortedMode->Description << Endl; + } else { + Cerr << SeparationString << Endl; + Cerr << unsortedMode->Description << Endl; + } + } + } else { + for (const auto& mode : Modes) { + if (mode.first != mode.second->Name) + continue; // this is an alias + + if (!mode.second->Hidden) { + Cerr << " " << mode.second->FormatFullName(maxModeLen + 4) << mode.second->Description << Endl; + } + } + } + + Cerr << Endl; + Cerr << "To get help for specific mode type '" << progName << " MODE " << ModesHelpOption << "'" << Endl; + if (VersionHandler) + Cerr << "To print program version type '" << progName << " --version'" << Endl; + if (!SvnRevisionOptionDisabled) { + Cerr << "To print svn revision type --svnrevision" << Endl; + } + return; +} + +TVersionHandlerPtr TModChooser::GetVersionHandler() const { + return VersionHandler; +} + +bool TModChooser::IsSvnRevisionOptionDisabled() const { + return SvnRevisionOptionDisabled; +} + +int TMainClassArgs::Run(int argc, const char** argv) { + return DoRun(NLastGetopt::TOptsParseResult(&GetOptions(), argc, argv)); +} + +const NLastGetopt::TOpts& TMainClassArgs::GetOptions() { + if (Opts_.Empty()) { + Opts_ = NLastGetopt::TOpts(); + RegisterOptions(Opts_.GetRef()); + } + + return Opts_.GetRef(); +} + +void TMainClassArgs::RegisterOptions(NLastGetopt::TOpts& opts) { + opts.AddHelpOption('h'); +} + +int TMainClassArgs::operator()(const int argc, const char** argv) { + return Run(argc, argv); +} + +int TMainClassModes::operator()(const int argc, const char** argv) { + return Run(argc, argv); +} + +int TMainClassModes::Run(int argc, const char** argv) { + auto& chooser = GetSubModes(); + return chooser.Run(argc, argv); +} + +const TModChooser& TMainClassModes::GetSubModes() { + if (Modes_.Empty()) { + Modes_.ConstructInPlace(); + RegisterModes(Modes_.GetRef()); + } + + return Modes_.GetRef(); +} + +void TMainClassModes::RegisterModes(TModChooser& modes) { + modes.SetModesHelpOption("-h"); +} diff --git a/library/cpp/getopt/small/modchooser.h b/library/cpp/getopt/small/modchooser.h new file mode 100644 index 0000000000..0a8de6d50b --- /dev/null +++ b/library/cpp/getopt/small/modchooser.h @@ -0,0 +1,215 @@ +#pragma once + +#include "last_getopt_opts.h" + +#include <util/generic/map.h> +#include <util/generic/string.h> +#include <util/generic/vector.h> + +#include <functional> + +//! Mode function with vector of cli arguments. +using TMainFunctionPtrV = std::function<int(const TVector<TString>&)> ; +using TMainFunctionRawPtrV = int (*)(const TVector<TString>& argv); + +//! Mode function with classic argc and argv arguments. +using TMainFunctionPtr = std::function<int(int, const char**)> ; +using TMainFunctionRawPtr = int (*)(const int argc, const char** argv); + +//! Mode class with vector of cli arguments. +class TMainClassV { +public: + virtual int operator()(const TVector<TString>& argv) = 0; + virtual ~TMainClassV() = default; +}; + +//! Mode class with classic argc and argv arguments. +class TMainClass { +public: + virtual int operator()(int argc, const char** argv) = 0; + virtual ~TMainClass() = default; +}; + +//! Function to handle '--version' parameter +typedef void (*TVersionHandlerPtr)(); + +/*! Main class for handling different modes in single tool. + * + * You can add modes for this class, use autogenerated help with + * list of modes and automaticly call necessary mode in run(). + * + * In first argv element mode get joined by space tool name and + * current mode name. + */ +class TModChooser { +public: + TModChooser(); + ~TModChooser(); + +public: + void AddMode(const TString& mode, TMainFunctionRawPtr func, const TString& description, bool hidden = false, bool noCompletion = false); + void AddMode(const TString& mode, TMainFunctionRawPtrV func, const TString& description, bool hidden = false, bool noCompletion = false); + void AddMode(const TString& mode, TMainFunctionPtr func, const TString& description, bool hidden = false, bool noCompletion = false); + void AddMode(const TString& mode, TMainFunctionPtrV func, const TString& description, bool hidden = false, bool noCompletion = false); + void AddMode(const TString& mode, TMainClass* func, const TString& description, bool hidden = false, bool noCompletion = false); + void AddMode(const TString& mode, TMainClassV* func, const TString& description, bool hidden = false, bool noCompletion = false); + + //! Hidden groups won't be displayed in 'help' block + void AddGroupModeDescription(const TString& description, bool hidden = false, bool noCompletion = false); + + //! Set default mode (if not specified explicitly) + void SetDefaultMode(const TString& mode); + + void AddAlias(const TString& alias, const TString& mode); + + //! Set main program description. + void SetDescription(const TString& descr); + + //! Set modes help option name (-? is by default) + void SetModesHelpOption(const TString& helpOption); + + //! Specify handler for '--version' parameter + void SetVersionHandler(TVersionHandlerPtr handler); + + //! Set description show mode + void SetSeparatedMode(bool separated = true); + + //! Set separation string + void SetSeparationString(const TString& str); + + //! Set short command representation in Usage block + void SetPrintShortCommandInUsage(bool printShortCommandInUsage); + + void DisableSvnRevisionOption(); + + void AddCompletions(TString progName, const TString& name = "completion", bool hidden = false, bool noCompletion = false); + + /*! Run appropriate mode. + * + * In this method following things happen: + * 1) If first argument is -h/--help/-? then print short description of + * all modes and exit with zero code. + * 2) If first argument is -v/--version and version handler is specified, + * then call it and exit with zero code. + * 3) Find mode with the same name as first argument. If it's found then + * call it and return its return code. + * 4) If appropriate mode is not found - return non-zero code. + */ + int Run(int argc, const char** argv) const; + + //! Run appropriate mode. Same as Run(const int, const char**) + int Run(const TVector<TString>& argv) const; + + void PrintHelp(const TString& progName) const; + + struct TMode { + TString Name; + TMainClass* Main; + TString Description; + bool Hidden; + bool NoCompletion; + TVector<TString> Aliases; + + TMode() + : Main(nullptr) + { + } + + TMode(const TString& name, TMainClass* main, const TString& descr, bool hidden, bool noCompletion); + + // Full name includes primary name and aliases. Also, will add ANSI colors. + size_t CalculateFullNameLen() const; + TString FormatFullName(size_t pad) const; + }; + + TVector<const TMode*> GetUnsortedModes() const { + auto ret = TVector<const TMode*>(Reserve(UnsortedModes.size())); + for (auto& mode : UnsortedModes) { + ret.push_back(mode.Get()); + } + return ret; + } + + TVersionHandlerPtr GetVersionHandler() const; + + bool IsSvnRevisionOptionDisabled() const; + +private: + //! Main program description. + TString Description; + + //! Help option for modes. + TString ModesHelpOption; + + //! Wrappers around all modes. + TVector<THolder<TMainClass>> Wrappers; + + //! Modes + TMap<TString, TMode*> Modes; + + TString DefaultMode; + + //! Handler for '--version' parameter + TVersionHandlerPtr VersionHandler; + + //! When set to true, show descriptions unsorted and display separators + bool ShowSeparated; + + //! When set to true, disables --svnrevision option, useful for opensource (git hosted) projects + bool SvnRevisionOptionDisabled; + + //! When true - will print only 'mode name' in 'Usage' block + bool PrintShortCommandInUsage; + + //! Text string used when displaying each separator + TString SeparationString; + + //! Unsorted list of options + TVector<THolder<TMode>> UnsortedModes; + + //! Mode that generates completions + THolder<TMainClass> CompletionsGenerator; +}; + +//! Mode class that allows introspecting its console arguments. +class TMainClassArgs: public TMainClass { +public: + int operator()(int argc, const char** argv) final; + +public: + //! Run this mode. + int Run(int argc, const char** argv); + + //! Get console arguments for this mode. + const NLastGetopt::TOpts& GetOptions(); + +protected: + //! Fill given empty `TOpts` with options. + virtual void RegisterOptions(NLastGetopt::TOpts& opts); + + //! Actual mode logic. Takes parsed options and returns exit code. + virtual int DoRun(NLastGetopt::TOptsParseResult&& parsedOptions) = 0; + +private: + TMaybe<NLastGetopt::TOpts> Opts_; +}; + +//! Mode class that uses sub-modes to dispatch commands further. +class TMainClassModes: public TMainClass { +public: + int operator()(int argc, const char** argv) final; + +public: + //! Run this mode. + int Run(int argc, const char** argv); + + //! Get sub-modes for this mode. + const TModChooser& GetSubModes(); + +protected: + //! Fill given modchooser with sub-modes. + virtual void RegisterModes(TModChooser& modes); + +private: + TMaybe<TModChooser> Modes_; +}; diff --git a/library/cpp/getopt/small/opt.cpp b/library/cpp/getopt/small/opt.cpp new file mode 100644 index 0000000000..744501765c --- /dev/null +++ b/library/cpp/getopt/small/opt.cpp @@ -0,0 +1,119 @@ +#include "opt.h" + +#include <util/system/progname.h> + +#include <ctype.h> + +using namespace NLastGetopt; + +namespace { + struct TOptsNoDefault: public TOpts { + TOptsNoDefault(const TStringBuf& optstring = TStringBuf()) + : TOpts(optstring) + { + } + }; + +} + +void Opt::Init(int argc, char* argv[], const char* optString, const Ion* longOptions, bool longOnly, bool isOpen) { + Ions_ = longOptions; + Err = true; + GotError_ = false; + Ind = argc; + + Opts_.Reset(new TOptsNoDefault(optString)); + for (const Ion* o = longOptions; o != nullptr && o->name != nullptr; ++o) { + TOpt* opt; + if ((unsigned)o->val < 0x80 && isalnum(o->val)) { + opt = &Opts_->CharOption(char(o->val)); + opt->AddLongName(o->name); + } else { + Opts_->AddLongOption(o->name); + opt = const_cast<TOpt*>(&Opts_->GetLongOption(o->name)); + } + opt->HasArg_ = EHasArg(o->has_arg); + opt->UserValue(o); + } + Opts_->AllowSingleDashForLong_ = longOnly; + Opts_->AllowPlusForLong_ = true; + Opts_->AllowUnknownCharOptions_ = isOpen; + Opts_->AllowUnknownLongOptions_ = false; + + OptsParser_.Reset(new TOptsParser(Opts_.Get(), argc, argv)); +} + +Opt::Opt(int argc, char* argv[], const char* optString, const Ion* longOptions, bool longOnly, bool isOpen) { + Init(argc, argv, optString, longOptions, longOnly, isOpen); +} + +Opt::Opt(int argc, const char* argv[], const char* optString, const Ion* longOptions, bool longOnly, bool isOpen) { + Init(argc, (char**)argv, optString, longOptions, longOnly, isOpen); +} + +int Opt::Get() { + return Get(nullptr); +} + +int Opt::Get(int* longOptionIndex) { + if (GotError_) + return EOF; + + Arg = nullptr; + + try { + bool r = OptsParser_->Next(); + Ind = (int)OptsParser_->Pos_; + if (!r) { + return EOF; + } else { + Arg = (char*)OptsParser_->CurVal(); + if (!OptsParser_->CurOpt()) { + // possible if RETURN_IN_ORDER + return 1; + } else { + const Ion* ion = (const Ion*)OptsParser_->CurOpt()->UserValue(); + if (longOptionIndex) { + *longOptionIndex = int(ion - Ions_); + } + char c = OptsParser_->CurOpt()->GetCharOr0(); + return c != 0 ? c : ion->val; + } + } + } catch (const NLastGetopt::TException&) { + GotError_ = true; + if (Err) + Cerr << CurrentExceptionMessage() << Endl; + return '?'; + } +} + +void Opt::DummyHelp(IOutputStream& os) { + Opts_->PrintUsage(GetProgramName(), os); +} + +int Opt::GetArgC() const { + return (int)OptsParser_->Argc_; +} + +const char** Opt::GetArgV() const { + return OptsParser_->Argv_; +} + +int opt_get_number(int& argc, char* argv[]) { + int num = -1; + for (int a = 1; a < argc; a++) { + if (*argv[a] == '-' && isdigit((ui8)argv[a][1])) { + char* ne; + num = strtol(argv[a] + 1, &ne, 10); + if (*ne) { + memmove(argv[a] + 1, ne, strlen(ne) + 1); + } else { + for (argc--; a < argc; a++) + argv[a] = argv[a + 1]; + } + break; + } + } + return num; +} diff --git a/library/cpp/getopt/small/opt.h b/library/cpp/getopt/small/opt.h new file mode 100644 index 0000000000..ecb57439bc --- /dev/null +++ b/library/cpp/getopt/small/opt.h @@ -0,0 +1,142 @@ +#pragma once + +#include "last_getopt.h" + +#include <util/generic/ptr.h> +#include <util/generic/noncopyable.h> + +// implementation of Opt class using last getopt + +/* + short-options syntax: + + opt-letter ::= + [^: ] + + opt-string ::= + '+'|'-'?({opt-letter}':'{0,2})* + + example: "AbCx:y:z::" + {A,b,C} options without argument + {x,y} options with argument + {z} option with optional argument + + 1. shortopts begins with '-' :=> RETURN_IN_ORDER + == non-option forces getopt to return 1 and to place non-option into optarg + + 2. shortopts begins with '+' :=> REQUIRE_ORDER + GetEnv(_POSIX_OPTION_ORDER) :=> REQUIRE_ORDER + == 1st non-option forces getopt to return EOF + + 3. default :=> PERMUTE + == exchange options with non-options and place all options first + + 4. '--' command line argument forces getopt to stop parsing and to return EOF + in any case + + long options should begin by '+' sign + or when (_getopt_long_only = 1) by '-' sign + + struct option { + char *name : option name + int has_arg: 0 | 1 | 2 = without | with | optional argument + int *flag : if (flag != 0) then getopt returns 0 and stores val into *flag + int val : if (flag == 0) then getopt returns val + } + + Example: + + struct option my_opts[] = { + { "delete", 0, &deletion_flag, DEL }, -- returns 0, deletion_flag := DEL + { "add", 1, NULL, 'a' }, -- returns 'a', argument in optarg + { NULL } + } +*/ + +#define OPT_RETURN_IN_ORDER "-" +#define OPT_REQUIRE_ORDER "+" +#define OPT_DONT_STORE_ARG ((void*)0) + +class Opt : TNonCopyable { +public: + enum HasArg { WithoutArg, + WithArg, + PossibleArg }; + + struct Ion { + const char* name; + HasArg has_arg; + int* flag; + int val; + }; + +private: + THolder<NLastGetopt::TOpts> Opts_; + THolder<NLastGetopt::TOptsParser> OptsParser_; + const Ion* Ions_; + bool GotError_; + + void Init(int argc, char* argv[], const char* optString, const Ion* longOptions = nullptr, bool longOnly = false, bool isOpen = false); + +public: + Opt(int argc, char* argv[], const char* optString, const Ion* longOptions = nullptr, bool longOnly = false, bool isOpen = false); + Opt(int argc, const char* argv[], const char* optString, const Ion* longOptions = nullptr, bool longOnly = false, bool isOpen = false); + + // Get() means next + int Get(); + int Get(int* longOptionIndex); + int operator()() { + return Get(); + } + + const char* GetArg() const { + return Arg; + } + + TVector<TString> GetFreeArgs() const { + return NLastGetopt::TOptsParseResult(&*Opts_, GetArgC(), GetArgV()).GetFreeArgs(); + } + + // obsolete, use GetArg() instead + char* Arg; /* option argument if any or NULL */ + + int Ind; /* command line index */ + bool Err; /* flag to print error messages */ + + int GetArgC() const; + const char** GetArgV() const; + + void DummyHelp(IOutputStream& os = Cerr); +}; + +// call before getopt. returns non-negative int, removing it from arguments (not found: -1) +// Example: returns 11 for "progname -11abc", -1 for "progname -a11" +int opt_get_number(int& argc, char* argv[]); + +#define OPTION_HANDLING_PROLOG \ + { \ + int optlet; \ + while (EOF != (optlet = opt.Get())) { \ + switch (optlet) { +#define OPTION_HANDLING_PROLOG_ANON(S) \ + { \ + Opt opt(argc, argv, (S)); \ + int optlet; \ + while (EOF != (optlet = opt.Get())) { \ + switch (optlet) { +#define OPTION_HANDLE_BEGIN(opt) case opt: { +#define OPTION_HANDLE_END \ + } \ + break; + +#define OPTION_HANDLE(opt, handle) \ + OPTION_HANDLE_BEGIN(opt) \ + handle; \ + OPTION_HANDLE_END + +#define OPTION_HANDLING_EPILOG \ + default: \ + ythrow yexception() << "unknown optlet"; \ + } \ + } \ + } diff --git a/library/cpp/getopt/small/opt2.cpp b/library/cpp/getopt/small/opt2.cpp new file mode 100644 index 0000000000..0cdc774e78 --- /dev/null +++ b/library/cpp/getopt/small/opt2.cpp @@ -0,0 +1,384 @@ +#include "opt2.h" + +#include <util/generic/hash.h> +#include <util/generic/utility.h> +#include <util/generic/yexception.h> +#include <util/str_stl.h> + +#include <stdio.h> +#include <errno.h> +#include <ctype.h> + +void Opt2::Clear() { + Specs.clear(); + memset(SpecsMap, 0, sizeof(SpecsMap)); + Pos.clear(); +} + +void Opt2::Init(int argc, char* const* argv, const char* optspec, IntRange free_args_num, const char* long_alias) { + Clear(); + Argc = argc; + Argv = argv; + HasErrors = false, BadPosCount = false, UnknownOption = 0, OptionMissingArg = 0; + UnknownLongOption = nullptr; + OptionWrongArg = 0, RequiredOptionMissing = 0; + EatArgv(optspec, long_alias); + MinArgs = Min<int>(free_args_num.Left, free_args_num.Right); + MaxArgs = Max<int>(free_args_num.Left, free_args_num.Right); + if (!HasErrors && MinArgs != -1 && ((int)Pos.size() < MinArgs || (int)Pos.size() > MaxArgs)) + BadPosCount = HasErrors = true; +} + +void Opt2::EatArgv(const char* optspec, const char* long_alias) { + // some flags + bool require_order = false; + if (*optspec == '+') { + require_order = true; + optspec++; + } + if (*optspec == '-') + ythrow yexception() << "Flag '-' can not be used in Opt2's optspec"; + // step 1 - parse optspec + for (const char* s = optspec; *s; s++) { + if (SpecsMap[(ui8)*s]) + ythrow yexception() << "Symbol '" << *s << "' is met twice in Opt2's optspec"; + if (*s == '?' || *s == '-') + ythrow yexception() << "Opt2: Symbol '" << *s << "' can not be used in optspec because it is reserved"; + Specs.push_back(Opt2Param()); + SpecsMap[(ui8)*s] = (ui8)Specs.size(); // actual index + 1 + Specs.back().opt = *s; + if (s[1] == ':') { + Specs.back().HasArg = true; + if (s[2] == ':') + ythrow yexception() << "Opt2 does not accept optional parameters (e.g. \"a::\") in optspec"; + s++; + } + } + // long_alias has a form "long-name1=A,long-name2=B", etc. + // This implementation is limited to aliasing a single long option + // with single short option (extend it if you really need). + THashMap<const char*, char> long2short; + long2short["help"] = '?'; + long_alias = long_alias ? long_alias : ""; + alias_copy = long_alias; + for (char* s = alias_copy.begin(); s && *s;) { + char* eq = strchr(s, '='); + char* comma = strchr(s, ','); + if (comma) + *comma = 0; + if (!eq || (comma && comma < eq)) + ythrow yexception() << "Opt2, long_alias: '=' is expected after " << s; + *eq++ = 0; + if (!*eq || eq[1]) + ythrow yexception() << "Opt2, long_alias: single letter must be assigned to " << s; + if (!SpecsMap[(ui8)*eq]) + ythrow yexception() << "Opt2, long_alias: trying to assign unknown option '" << *eq << "' to " << s; + Opt2Param& p = Specs[SpecsMap[(ui8)*eq] - 1]; + // If several long options aliased to some letter, only last one is shown in usage + p.LongOptName = s; + if (long2short.find(s) != long2short.end()) + ythrow yexception() << "Opt2, long_alias: " << s << " specified twice"; + long2short[s] = *eq; + s = comma ? comma + 1 : nullptr; + } + + if (Argc < 1) { + HasErrors = true; + return; + } + + // step 2 - parse argv + int ind = 1; + for (; ind != Argc; ind++) { + if (*Argv[ind] != '-') { + if (require_order) // everything now goes to Pos + break; + Pos.push_back(Argv[ind]); + continue; + } + const char* s = Argv[ind] + 1; + + if (*s == '-') { + if (!*++s) { // `--' terminates the list of options + ind++; + break; + } + // long option always spans one argv (--switch or --option-name=value) + const char* eq = strchr(s, '='); + TString lname(s, eq ? (size_t)(eq - s) : (size_t)strlen(s)); + THashMap<const char*, char>::iterator i = long2short.find(lname.data()); + if (i == long2short.end()) { + UnknownLongOption = strdup(lname.data()); // free'd in AutoUsage() + HasErrors = true; + return; + } + if (i->second == '?') { + UnknownOption = '?'; + HasErrors = true; + continue; + } + Opt2Param& p = Specs[SpecsMap[(ui8)i->second] - 1]; + p.IsFound = true; + if (p.HasArg && !eq) { + HasErrors = true; + OptionMissingArg = p.opt; // short option, indeed + return; + } + if (!p.HasArg && eq) { + HasErrors = true; + OptionWrongArg = p.opt; // short option, indeed + return; + } + if (eq) + p.ActualValue.push_back(eq + 1); + continue; + } + + for (; *s; s++) { + if (!SpecsMap[(ui8)*s]) { + UnknownOption = *s; + HasErrors = true; + if (*s == '?') + continue; + return; + } + Opt2Param& p = Specs[SpecsMap[(ui8)*s] - 1]; + p.IsFound = true; + if (p.HasArg) { + if (s[1]) + p.ActualValue.push_back(s + 1); + else { + ind++; + if (ind == Argc) { + HasErrors = true; + OptionMissingArg = *s; + p.IsFound = false; + return; + } + p.ActualValue.push_back(Argv[ind]); + } + break; + } + } + } + for (; ind != Argc; ind++) + Pos.push_back(Argv[ind]); +} + +Opt2Param& Opt2::GetInternal(char opt, const char* defValue, const char* helpUsage, bool requred) { + if (!SpecsMap[(ui8)opt]) + ythrow yexception() << "Unspecified option character '" << opt << "' asked from Opt2::Get"; + Opt2Param& p = Specs[SpecsMap[(ui8)opt] - 1]; + p.DefValue = defValue; + p.HelpUsage = helpUsage; + p.IsRequired = requred; + if (!p.IsFound && requred && !HasErrors) { + RequiredOptionMissing = opt; + HasErrors = true; + } + return p; +} + +// For options with parameters +const char* Opt2::Arg(char opt, const char* help, const char* def, bool required) { + Opt2Param& p = GetInternal(opt, def, help, required); + if (!p.HasArg) + ythrow yexception() << "Opt2::Arg called for '" << opt << "' which is an option without argument"; + return p.IsFound ? p.ActualValue.empty() ? nullptr : p.ActualValue.back() : def; +} + +// For options with parameters +const char* Opt2::Arg(char opt, const char* help, TString def, bool required) { + Opt2Param& p = GetInternal(opt, nullptr, help, required); + if (!p.HasArg) + ythrow yexception() << "Opt2::Arg called for '" << opt << "' which is an option without argument"; + p.DefValueStr = def; + p.DefValue = p.DefValueStr.begin(); + return p.IsFound ? p.ActualValue.empty() ? nullptr : p.ActualValue.back() : p.DefValue; +} + +// Options with parameters that can be specified several times +const TVector<const char*>& Opt2::MArg(char opt, const char* help) { + Opt2Param& p = GetInternal(opt, nullptr, help, false); + p.MultipleUse = true; + if (!p.HasArg) + ythrow yexception() << "Opt2::Arg called for '" << opt << "' which is an option without argument"; + return p.ActualValue; +} + +/// For options w/o parameters +bool Opt2::Has(char opt, const char* help) { + Opt2Param& p = GetInternal(opt, nullptr, help, false); + if (p.HasArg) + ythrow yexception() << "Opt2::Has called for '" << opt << "' which is an option with argument"; + return p.IsFound; +} + +// Get() + strtol, may set up HasErrors +long Opt2::Int(char opt, const char* help, long def, bool required) { + Opt2Param& p = GetInternal(opt, (char*)(uintptr_t)def, help, required); + if (!p.HasArg) + ythrow yexception() << "Opt2::Int called for '" << opt << "' which is an option without argument"; + p.IsNumeric = true; + if (!p.IsFound || p.ActualValue.empty() || !p.ActualValue.back()) + return def; + char* e; + long rv = strtol(p.ActualValue.back(), &e, 10); + if (e == p.ActualValue.back() || *e) { + OptionWrongArg = opt; + HasErrors = true; + } + return rv; +} + +// Get() + strtoul, may set up HasErrors +unsigned long Opt2::UInt(char opt, const char* help, unsigned long def, bool required) { + Opt2Param& p = GetInternal(opt, (char*)(uintptr_t)def, help, required); + if (!p.HasArg) + ythrow yexception() << "Opt2::UInt called for '" << opt << "' which is an option without argument"; + p.IsNumeric = true; + if (!p.IsFound || p.ActualValue.empty() || !p.ActualValue.back()) + return def; + char* e; + unsigned long rv = strtoul(p.ActualValue.back(), &e, 10); + if (e == p.ActualValue.back() || *e) { + OptionWrongArg = opt; + HasErrors = true; + } + return rv; +} + +// Add user defined error message and set error flag +void Opt2::AddError(const char* message) { + HasErrors = true; + if (message) + UserErrorMessages.push_back(message); +} + +int Opt2::AutoUsage(const char* free_arg_names) { + if (!HasErrors) + return 0; + FILE* where = UnknownOption == '?' ? stdout : stderr; + char req_str[256], nreq_str[256]; + int req = 0, nreq = 0; + for (int n = 0; n < (int)Specs.size(); n++) + if (Specs[n].IsRequired) + req_str[req++] = Specs[n].opt; + else + nreq_str[nreq++] = Specs[n].opt; + req_str[req] = 0, nreq_str[nreq] = 0; + const char* prog = strrchr(Argv[0], LOCSLASH_C); + prog = prog ? prog + 1 : Argv[0]; + fprintf(where, "Usage: %s%s%s%s%s%s%s%s\n", prog, req ? " -" : "", req_str, + nreq ? " [-" : "", nreq_str, nreq ? "]" : "", + free_arg_names && *free_arg_names ? " " : "", free_arg_names); + for (auto& spec : Specs) { + const char* hlp = !spec.HelpUsage.empty() ? spec.HelpUsage.data() : spec.HasArg ? "<arg>" : ""; + if (!spec.HasArg || spec.IsRequired) + fprintf(where, " -%c %s\n", spec.opt, hlp); + else if (!spec.IsNumeric) + fprintf(where, " -%c %s [Default: %s]\n", spec.opt, hlp, spec.DefValue); + else + fprintf(where, " -%c %s [Def.val: %li]\n", spec.opt, hlp, (long)(uintptr_t)spec.DefValue); + if (spec.LongOptName) + fprintf(where, " --%s%s - same as -%c\n", spec.LongOptName, spec.HasArg ? "=<argument>" : "", spec.opt); + } + if (OptionMissingArg) + fprintf(where, " *** Option '%c' is missing required argument\n", OptionMissingArg); + if (OptionWrongArg) + fprintf(where, " *** Incorrect argument for option '%c'\n", OptionWrongArg); + if (UnknownOption && UnknownOption != '?') + fprintf(where, " *** Unknown option '%c'\n", UnknownOption); + if (UnknownLongOption) { + fprintf(where, " *** Unknown long option '%s'\n", UnknownLongOption); + free(UnknownLongOption); + UnknownLongOption = nullptr; + } + if (RequiredOptionMissing) + fprintf(where, " *** Required option '%c' missing\n", RequiredOptionMissing); + if (BadPosCount && MinArgs != MaxArgs) + fprintf(where, " *** %i free argument(s) supplied, expected %i to %i\n", (int)Pos.size(), MinArgs, MaxArgs); + if (BadPosCount && MinArgs == MaxArgs) + fprintf(where, " *** %i free argument(s) supplied, expected %i\n", (int)Pos.size(), MinArgs); + for (const auto& userErrorMessage : UserErrorMessages) + fprintf(where, " *** %s\n", userErrorMessage.data()); + return UnknownOption == '?' ? 1 : 2; +} + +void Opt2::AutoUsageErr(const char* free_arg_names) { + if (AutoUsage(free_arg_names)) + exit(1); +} + +#ifdef OPT2_TEST +// TODO: convert it to unittest + +bool opt2_ut_fail = false, opt_ut_verbose = false; +const char* ut_optspec; +int ut_real(TString args, bool err_exp, const char* A_exp, int b_exp, bool a_exp, const char* p1_exp, const char* p2_exp) { + char* argv[32]; + int argc = sf(' ', argv, args.begin()); + Opt2 opt(argc, argv, ut_optspec, 2, "option-1=A,option-2=a,"); + const char* A = opt.Arg('A', "<qqq> - blah"); + int b = opt.Int('b', "<rrr> - blah", 2); + bool a = opt.Has('a', "- blah"); + /*const char *C = */ opt.Arg('C', "<ccc> - blah", 0); + + if (opt_ut_verbose) + opt.AutoUsage(""); + if (opt.HasErrors != err_exp) + return 1; + if (err_exp) + return false; + if (!A && A_exp || A && !A_exp || A && A_exp && strcmp(A, A_exp)) + return 2; + if (b != b_exp) + return 3; + if (a != a_exp) + return 4; + if (strcmp(opt.Pos[0], p1_exp)) + return 5; + if (strcmp(opt.Pos[1], p2_exp)) + return 6; + return false; +} + +void ut(const char* args, bool err_exp, const char* A_exp, int b_exp, bool a_exp, const char* p1_exp, const char* p2_exp) { + if (opt_ut_verbose) + fprintf(stderr, "Testing: %s\n", args); + if (int rv = ut_real(args, err_exp, A_exp, b_exp, a_exp, p1_exp, p2_exp)) { + opt2_ut_fail = true; + fprintf(stderr, "Test %i failed for: %s\n", rv, args); + } else { + if (opt_ut_verbose) + fprintf(stderr, "OK\n"); + } +} + +int main(int argc, char* argv[]) { + Opt2 opt(argc, argv, "v", 0); + opt_ut_verbose = opt.Has('v', "- some verboseness"); + opt.AutoUsageErr(""); + ut_optspec = "A:ab:C:"; + ut("prog -A argA -a -b 22 -C argC Pos1 Pos2", false, "argA", 22, true, "Pos1", "Pos2"); + ut("prog Pos1 -A argA -a -C argC Pos2", false, "argA", 2, true, "Pos1", "Pos2"); + ut("prog -A argA Pos1 -b22 Pos2 -C argC", false, "argA", 22, false, "Pos1", "Pos2"); + ut("prog -A argA Pos1 -b 22 Pos2 -C", true, "argA", 22, false, "Pos1", "Pos2"); + ut("prog -A argA -a -b 22 -C Pos1 Pos2", true, "argA", 22, true, "Pos1", "Pos2"); + ut("prog -A argA -a -b two -C argC Pos1 Pos2", true, "argA", 2, true, "Pos1", "Pos2"); + ut("prog -a -b 22 -C argC Pos1 Pos2", true, "argA", 22, true, "Pos1", "Pos2"); + ut("prog Pos1 --option-1=argA -a -C argC Pos2", false, "argA", 2, true, "Pos1", "Pos2"); + ut("prog Pos1 -A argA --option-1 -a -C argC Pos2", true, "argA", 2, true, "Pos1", "Pos2"); + ut("prog -A argA --option-2 -b -22 -C argC Pos1 Pos2", false, "argA", -22, true, "Pos1", "Pos2"); + ut("prog -A argA --option-2 -b -22 -- -C argC", false, "argA", -22, true, "-C", "argC"); + ut("prog -A argA --option-2=1 -b -22 -C argC Pos1 Pos2", true, "argA", -22, true, "Pos1", "Pos2"); + + ut_optspec = "+A:ab:C:"; + ut("prog -A argA --option-2 v1 -C", false, "argA", 2, true, "v1", "-C"); + ut("prog -A argA --option-2 v1 -C argC", true, "argA", 2, true, "v1", "-C"); + if (!opt2_ut_fail) + fprintf(stderr, "All OK\n"); + return opt2_ut_fail; +} + +#endif // OPT2_TEST diff --git a/library/cpp/getopt/small/opt2.h b/library/cpp/getopt/small/opt2.h new file mode 100644 index 0000000000..4d9d943237 --- /dev/null +++ b/library/cpp/getopt/small/opt2.h @@ -0,0 +1,137 @@ +#pragma once + +#include <util/system/defaults.h> +#include <util/generic/string.h> +#include <util/generic/vector.h> + +// simplified options parser +// No 'optional argument' (e.g. "a::" in spec.) support; +// Supports '+' switch (see opt.h), does not support '-'; + +/** Typical use + Opt2 opt(argc, argv, "A:b:c", 3); <- 3 more arguments expected, opt.Pos[0], etc. + ** Usage description for options is provided through functions that query values ** + const char *a = opt.Arg('A', "<var_name> - usage of -A"); <- This option is required + int b = opt.Int('b', "<var_name> - usage of -b", 2); <- This option has default value, not required + bool c = opt.Has('c', "- usage of -c"); <- switches are always optional + + ** Additional argument names are provided in AutoUsage call ** + ** AutoUsage generages 'USAGE' text automatically ** + if (opt.AutoUsage("<L> <M>")) <- Returns 1 if there was any error in getopt + return 1; + OR: opt.AutoUsageErr("<L> <M>"); <- Will terminate program for you :) +*/ + +// Note: struct Opt2Param can be moved to cpp-file +struct Opt2Param { + char opt; + bool HasArg; + bool IsFound; + bool IsNumeric; + bool IsRequired; + bool MultipleUse; + const char* DefValue; + TString DefValueStr; + TString HelpUsage; + TVector<const char*> ActualValue; + const char* LongOptName; + Opt2Param() + : HasArg(false) + , IsFound(0) + , IsNumeric(0) + , IsRequired(0) + , MultipleUse(0) + , DefValue(nullptr) + , LongOptName(nullptr) + { + } +}; + +struct IntRange { + int Left, Right; + IntRange() = delete; + IntRange(int both) + : Left(both) + , Right(both) + { + } + + IntRange(int left, int right) + : Left(left) + , Right(right) + { + } +}; + +class Opt2 { +public: + Opt2() = default; + + Opt2(int argc, char* const* argv, const char* optspec, IntRange free_args_num = -1, const char* long_alias = nullptr) { + Init(argc, argv, optspec, free_args_num, long_alias); + } + + // Init throws exception only in case of incorrect optspec. + // In other cases, consult HasErrors or call AutoUsage() + void Init(int argc, char* const* argv, const char* optspec, IntRange free_args_num = -1, const char* long_alias = nullptr); + + // In case of incorrect options, constructs and prints Usage text, + // usually to stderr (however, to stdout if '-?' switch was used), and returns 1. + int AutoUsage(const char* free_arg_names = ""); + + // same as AutoUsage but calls exit(1) instead of error code + void AutoUsageErr(const char* free_arg_names = ""); + + // For options with parameters + const char* Arg(char opt, const char* helpUsage, const char* defValue, bool required = false); + const char* Arg(char opt, const char* helpUsage) { + return Arg(opt, helpUsage, nullptr, true); + } + const char* Arg(char opt, const char* helpUsage, TString defValue, bool required = false); + + // Options with parameters that can be specified several times + const TVector<const char*>& MArg(char opt, const char* helpUsage); + + // Get() + strtol, may set up HasErrors + long Int(char opt, const char* helpUsage, long defValue, bool required = false); + long Int(char opt, const char* helpUsage) { + return Int(opt, helpUsage, 0, true); + } + + // Get() + strtoul, may set up HasErrors + unsigned long UInt(char opt, const char* helpUsage, unsigned long defValue, bool required = false); + unsigned long UInt(char opt, const char* helpUsage) { + return UInt(opt, helpUsage, 0, true); + } + + // For options w/o parameters + bool Has(char opt, const char* helpUsage); + + // Add user defined error message and set error flag + void AddError(const char* message = nullptr); + +public: + // non-option args + TVector<char*> Pos; + bool HasErrors; + +private: + bool BadPosCount; + char UnknownOption; + char* UnknownLongOption; + char OptionMissingArg; + char OptionWrongArg; + char RequiredOptionMissing; + TVector<TString> UserErrorMessages; + +protected: + int Argc; + char* const* Argv; + int MinArgs, MaxArgs; + ui8 SpecsMap[256]; + TVector<Opt2Param> Specs; + TString alias_copy; + void EatArgv(const char* optspec, const char* long_alias); + void Clear(); + Opt2Param& GetInternal(char opt, const char* defValue, const char* helpUsage, bool required); +}; diff --git a/library/cpp/getopt/small/posix_getopt.cpp b/library/cpp/getopt/small/posix_getopt.cpp new file mode 100644 index 0000000000..bd06f3499f --- /dev/null +++ b/library/cpp/getopt/small/posix_getopt.cpp @@ -0,0 +1,77 @@ +#include "posix_getopt.h" + +#include <util/generic/ptr.h> + +#include <ctype.h> + +namespace NLastGetopt { + char* optarg; + int optind; + int optopt; + int opterr; + int optreset; + + static THolder<TOpts> Opts; + static THolder<TOptsParser> OptsParser; + + int getopt_long_impl(int argc, char* const* argv, const char* optstring, + const struct option* longopts, int* longindex, bool long_only) { + if (!Opts || optreset == 1) { + optarg = nullptr; + optind = 1; + opterr = 1; + optreset = 0; + Opts.Reset(new TOpts(TOpts::Default(optstring))); + + Opts->AllowSingleDashForLong_ = long_only; + + for (const struct option* o = longopts; o != nullptr && o->name != nullptr; ++o) { + TOpt* opt; + if ((unsigned)o->val < 0x80 && isalnum(o->val)) { + opt = &Opts->CharOption(char(o->val)); + opt->AddLongName(o->name); + } else { + Opts->AddLongOption(o->name); + opt = const_cast<TOpt*>(&Opts->GetLongOption(o->name)); + } + opt->HasArg_ = EHasArg(o->has_arg); + opt->UserValue(o->flag); + } + + OptsParser.Reset(new TOptsParser(&*Opts, argc, (const char**)argv)); + } + + optarg = nullptr; + + try { + if (!OptsParser->Next()) { + return -1; + } else { + optarg = (char*)OptsParser->CurVal(); + optind = (int)OptsParser->Pos_; + if (longindex && OptsParser->CurOpt()) + *longindex = (int)Opts->IndexOf(OptsParser->CurOpt()); + return OptsParser->CurOpt() ? OptsParser->CurOpt()->GetCharOr0() : 1; + } + } catch (const NLastGetopt::TException&) { + return '?'; + } + } + + int getopt_long(int argc, char* const* argv, const char* optstring, + const struct option* longopts, int* longindex) { + return getopt_long_impl(argc, argv, optstring, longopts, longindex, false); + } + + int getopt_long_only(int argc, char* const* argv, const char* optstring, + const struct option* longopts, int* longindex) { + return getopt_long_impl(argc, argv, optstring, longopts, longindex, true); + } + + // XXX: leading colon is not supported + // XXX: updating optind by client is not supported + int getopt(int argc, char* const* argv, const char* optstring) { + return getopt_long(argc, argv, optstring, nullptr, nullptr); + } + +} diff --git a/library/cpp/getopt/small/posix_getopt.h b/library/cpp/getopt/small/posix_getopt.h new file mode 100644 index 0000000000..e6af1e0284 --- /dev/null +++ b/library/cpp/getopt/small/posix_getopt.h @@ -0,0 +1,32 @@ +#pragma once + +// implementation of posix getopt using last getopt for demonstration purposes + +#include "last_getopt.h" + +namespace NLastGetopt { + extern char* optarg; + extern int optind; + extern int optopt; + extern int opterr; + extern int optreset; + + enum { + no_argument = NO_ARGUMENT, + required_argument = REQUIRED_ARGUMENT, + optional_argument = OPTIONAL_ARGUMENT, + }; + + struct option { + const char* name; + int has_arg; + int* flag; + int val; + }; + + int getopt(int argc, char* const* argv, const char* optstring); + int getopt_long(int argc, char* const* argv, const char* optstring, + const struct option* longopts, int* longindex); + int getopt_long_only(int argc, char* const* argv, const char* optstring, + const struct option* longopts, int* longindex); +} diff --git a/library/cpp/getopt/small/wrap.cpp b/library/cpp/getopt/small/wrap.cpp new file mode 100644 index 0000000000..9fbd38842a --- /dev/null +++ b/library/cpp/getopt/small/wrap.cpp @@ -0,0 +1,99 @@ +#include "wrap.h" + +#include <library/cpp/colorizer/colors.h> + +#include <util/generic/string.h> +#include <util/stream/str.h> +#include <util/charset/utf8.h> + +#include <cctype> + +namespace NLastGetopt { + TString Wrap(ui32 width, TStringBuf text, TStringBuf indent, size_t* lastLineLen, bool* hasParagraphs) { + if (width == 0) { + return TString(text); + } + + if (width >= indent.size()) { + width -= indent.size(); + } + + if (hasParagraphs) { + *hasParagraphs = false; + } + + TString res; + auto os = TStringOutput(res); + + const char* spaceBegin = text.begin(); + const char* wordBegin = text.begin(); + const char* wordEnd = text.begin(); + const char* end = text.end(); + + size_t lenSoFar = 0; + + bool isPreParagraph = false; + + do { + spaceBegin = wordBegin = wordEnd; + + while (wordBegin < end && *wordBegin == ' ') { + wordBegin++; + } + + if (wordBegin == end) { + break; + } + + wordEnd = wordBegin; + + while (wordEnd < end && *wordEnd != ' ' && *wordEnd != '\n') { + wordEnd++; + } + + auto spaces = TStringBuf(spaceBegin, wordBegin); + auto word = TStringBuf(wordBegin, wordEnd); + + size_t spaceLen = spaces.size(); + + size_t wordLen = 0; + if (!GetNumberOfUTF8Chars(word.data(), word.size(), wordLen)) { + wordLen = word.size(); // not a utf8 string -- just use its binary size + } + wordLen -= NColorizer::TotalAnsiEscapeCodeLen(word); + + // Empty word means we've found a bunch of whitespaces followed by newline. + // We don't want to print trailing whitespaces. + if (word) { + // We can't fit this word into the line -- insert additional line break. + // We shouldn't insert line breaks if we're at the beginning of a line, hence `lenSoFar` check. + if (lenSoFar && lenSoFar + spaceLen + wordLen > width) { + os << Endl << indent << word; + lenSoFar = wordLen; + } else { + os << spaces << word; + lenSoFar += spaceLen + wordLen; + } + isPreParagraph = false; + } + + if (wordEnd != end && *wordEnd == '\n') { + os << Endl << indent; + lenSoFar = 0; + wordEnd++; + if (hasParagraphs && isPreParagraph) { + *hasParagraphs = true; + } else { + isPreParagraph = true; + } + continue; + } + } while (wordEnd < end); + + if (lastLineLen) { + *lastLineLen = lenSoFar; + } + + return res; + } +} diff --git a/library/cpp/getopt/small/wrap.h b/library/cpp/getopt/small/wrap.h new file mode 100644 index 0000000000..e98028688d --- /dev/null +++ b/library/cpp/getopt/small/wrap.h @@ -0,0 +1,16 @@ +#pragma once + +#include <util/generic/fwd.h> +#include <util/generic/strbuf.h> + +namespace NLastGetopt { + /** + * Split text to multiple lines so that each line fits the given width. + * Can work with UTF8, understands ANSI escape codes. + * + * @param indent will print this string after each newline. + * @param lastLineLen output: will set to number of unicode codepoints in the last printed line. + * @param hasParagraphs output: will set to true if there are two consecutive newlines in the text. + */ + TString Wrap(ui32 width, TStringBuf text, TStringBuf indent = "", size_t* lastLineLen = nullptr, bool* hasParagraphs = nullptr); +} diff --git a/library/cpp/getopt/small/ya.make b/library/cpp/getopt/small/ya.make new file mode 100644 index 0000000000..96de0f04b1 --- /dev/null +++ b/library/cpp/getopt/small/ya.make @@ -0,0 +1,28 @@ +LIBRARY() + +OWNER(pg) + +PEERDIR( + library/cpp/colorizer +) + +SRCS( + completer.cpp + completer_command.cpp + completion_generator.cpp + formatted_output.cpp + last_getopt.cpp + last_getopt_easy_setup.cpp + last_getopt_opt.cpp + last_getopt_opts.cpp + last_getopt_parser.cpp + last_getopt_parse_result.cpp + modchooser.cpp + opt.cpp + opt2.cpp + posix_getopt.cpp + wrap.cpp + ygetopt.cpp +) + +END() diff --git a/library/cpp/getopt/small/ygetopt.cpp b/library/cpp/getopt/small/ygetopt.cpp new file mode 100644 index 0000000000..1f52827f74 --- /dev/null +++ b/library/cpp/getopt/small/ygetopt.cpp @@ -0,0 +1,108 @@ +#include "opt.h" +#include "ygetopt.h" + +#include <util/generic/string.h> +#include <util/generic/vector.h> +#include <util/generic/yexception.h> + +class TGetOpt::TImpl: public TSimpleRefCount<TImpl> { +public: + inline TImpl(int argc, const char* const* argv, const TString& fmt) + : args(argv, argv + argc) + , format(fmt) + { + if (argc == 0) { + ythrow yexception() << "zero argc"; + } + } + + inline ~TImpl() = default; + + TVector<TString> args; + const TString format; +}; + +class TGetOpt::TIterator::TIterImpl: public TSimpleRefCount<TIterImpl> { +public: + inline TIterImpl(const TGetOpt* parent) + : Args_(parent->Impl_->args) + , ArgsPtrs_(new char*[Args_.size() + 1]) + , Format_(parent->Impl_->format) + , OptLet_(0) + , Arg_(nullptr) + { + for (size_t i = 0; i < Args_.size(); ++i) { + ArgsPtrs_.Get()[i] = Args_[i].begin(); + } + + ArgsPtrs_.Get()[Args_.size()] = nullptr; + Opt_.Reset(new Opt((int)Args_.size(), ArgsPtrs_.Get(), Format_.data())); + } + + inline ~TIterImpl() = default; + + inline void Next() { + OptLet_ = Opt_->Get(); + Arg_ = Opt_->Arg; + } + + inline char Key() const noexcept { + return (char)OptLet_; + } + + inline const char* Arg() const noexcept { + return Arg_; + } + + inline bool AtEnd() const noexcept { + return OptLet_ == EOF; + } + +private: + TVector<TString> Args_; + TArrayHolder<char*> ArgsPtrs_; + const TString Format_; + THolder<Opt> Opt_; + int OptLet_; + const char* Arg_; +}; + +TGetOpt::TIterator::TIterator() noexcept + : Impl_(nullptr) +{ +} + +TGetOpt::TIterator::TIterator(const TGetOpt* parent) + : Impl_(new TIterImpl(parent)) +{ + Next(); +} + +void TGetOpt::TIterator::Next() { + Impl_->Next(); +} + +char TGetOpt::TIterator::Key() const noexcept { + return Impl_->Key(); +} + +bool TGetOpt::TIterator::AtEnd() const noexcept { + if (Impl_.Get()) { + return Impl_->AtEnd(); + } + + return true; +} + +const char* TGetOpt::TIterator::Arg() const noexcept { + if (Impl_.Get()) { + return Impl_->Arg(); + } + + return nullptr; +} + +TGetOpt::TGetOpt(int argc, const char* const* argv, const TString& format) + : Impl_(new TImpl(argc, argv, format)) +{ +} diff --git a/library/cpp/getopt/small/ygetopt.h b/library/cpp/getopt/small/ygetopt.h new file mode 100644 index 0000000000..615d3dd18e --- /dev/null +++ b/library/cpp/getopt/small/ygetopt.h @@ -0,0 +1,72 @@ +#pragma once + +#include <util/generic/fwd.h> +#include <util/generic/ptr.h> + +class TGetOpt { +public: + class TIterator { + friend class TGetOpt; + + public: + char Key() const noexcept; + const char* Arg() const noexcept; + + inline bool HaveArg() const noexcept { + return Arg(); + } + + inline void operator++() { + Next(); + } + + inline bool operator==(const TIterator& r) const noexcept { + return AtEnd() == r.AtEnd(); + } + + inline bool operator!=(const TIterator& r) const noexcept { + return !(*this == r); + } + + inline TIterator& operator*() noexcept { + return *this; + } + + inline const TIterator& operator*() const noexcept { + return *this; + } + + inline TIterator* operator->() noexcept { + return this; + } + + inline const TIterator* operator->() const noexcept { + return this; + } + + private: + TIterator() noexcept; + TIterator(const TGetOpt* parent); + + void Next(); + bool AtEnd() const noexcept; + + private: + class TIterImpl; + TSimpleIntrusivePtr<TIterImpl> Impl_; + }; + + TGetOpt(int argc, const char* const* argv, const TString& format); + + inline TIterator Begin() const { + return TIterator(this); + } + + inline TIterator End() const noexcept { + return TIterator(); + } + +private: + class TImpl; + TSimpleIntrusivePtr<TImpl> Impl_; +}; |