| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include "kudu/tools/tool_action.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <memory> |
| #include <optional> |
| #include <ostream> |
| #include <string> |
| #include <type_traits> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include <gflags/gflags.h> |
| #include <glog/logging.h> |
| |
| #include "kudu/gutil/strings/join.h" |
| #include "kudu/gutil/strings/split.h" |
| #include "kudu/gutil/strings/stringpiece.h" |
| #include "kudu/gutil/strings/substitute.h" |
| #include "kudu/util/flags.h" |
| #include "kudu/util/logging.h" |
| #include "kudu/util/url-coding.h" |
| |
| using std::optional; |
| using std::pair; |
| using std::string; |
| using std::unique_ptr; |
| using std::unordered_map; |
| using std::vector; |
| using strings::Substitute; |
| |
| namespace kudu { |
| namespace tools { |
| |
| namespace { |
| |
| string FakeDescribeOneFlag(const ActionArgsDescriptor::Arg& arg) { |
| string res = google::DescribeOneFlag({ |
| arg.name, // name |
| "string", // type |
| arg.description, // description |
| "", // current_value |
| "", // default_value |
| "", // filename |
| false, // has_validator_fn |
| true, // is_default |
| nullptr // flag_ptr |
| }); |
| |
| // Strip the first dash from the description; this is a positional parameter |
| // so let's make sure it looks like one. |
| string::size_type first_dash_idx = res.find('-'); |
| DCHECK_NE(string::npos, first_dash_idx); |
| return res.substr(0, first_dash_idx) + res.substr(first_dash_idx + 1); |
| } |
| |
| string BuildUsageString(const vector<Mode*>& chain) { |
| return JoinMapped(chain, [](Mode* a){ return a->name(); }, " "); |
| } |
| |
| string SpacePad(StringPiece s, int len) { |
| if (s.size() >= len) { |
| return s.ToString(); |
| } |
| return string(len - s.size(), ' ' ) + s.ToString(); |
| } |
| |
| } // anonymous namespace |
| |
| ModeBuilder::ModeBuilder(string name) |
| : name_(std::move(name)) { |
| } |
| |
| ModeBuilder& ModeBuilder::Description(const string& description) { |
| CHECK(description_.empty()); |
| description_ = description; |
| return *this; |
| } |
| |
| ModeBuilder& ModeBuilder::AddMode(unique_ptr<Mode> mode) { |
| submodes_.push_back(std::move(mode)); |
| return *this; |
| } |
| |
| ModeBuilder& ModeBuilder::AddAction(unique_ptr<Action> action) { |
| actions_.push_back(std::move(action)); |
| return *this; |
| } |
| |
| unique_ptr<Mode> ModeBuilder::Build() { |
| CHECK(!description_.empty()); |
| unique_ptr<Mode> mode(new Mode()); |
| mode->name_ = name_; |
| mode->description_ = description_; |
| mode->submodes_ = std::move(submodes_); |
| mode->actions_ = std::move(actions_); |
| return mode; |
| } |
| |
| // Get help for this mode, passing in its parent mode chain. |
| string Mode::BuildHelp(const vector<Mode*>& chain) const { |
| string msg; |
| msg += Substitute("Usage: $0 <command> [<args>]\n\n", |
| BuildUsageString(chain)); |
| msg += "<command> can be one of the following:\n"; |
| |
| vector<pair<string, string>> line_pairs; |
| int max_command_len = 0; |
| for (const auto& m : modes()) { |
| line_pairs.emplace_back(m->name(), m->description()); |
| max_command_len = std::max<int>(max_command_len, m->name().size()); |
| } |
| for (const auto& a : actions()) { |
| line_pairs.emplace_back(a->name(), a->description()); |
| max_command_len = std::max<int>(max_command_len, a->name().size()); |
| } |
| |
| for (const auto& lp : line_pairs) { |
| msg += " " + SpacePad(lp.first, max_command_len); |
| msg += " "; |
| AppendHardWrapped(lp.second, max_command_len + 5, &msg); |
| msg += "\n"; |
| } |
| |
| msg += "\n"; |
| return msg; |
| } |
| |
| string Mode::BuildHelpXML(const vector<Mode*>& chain) const { |
| string xml; |
| xml += "<mode>"; |
| xml += Substitute("<name>$0</name>", name()); |
| xml += Substitute("<description>$0</description>", |
| EscapeForHtmlToString(description())); |
| for (const auto& a : actions()) { |
| xml += a->BuildHelpXML(chain); |
| } |
| |
| for (const auto& m : modes()) { |
| vector<Mode*> m_chain(chain); |
| m_chain.push_back(m.get()); |
| xml += m->BuildHelpXML(m_chain); |
| } |
| xml += "</mode>"; |
| return xml; |
| } |
| |
| ActionBuilder::ActionBuilder(string name, ActionRunner runner) |
| : name_(std::move(name)), |
| runner_(std::move(runner)) { |
| } |
| |
| ActionBuilder& ActionBuilder::Description(const string& description) { |
| CHECK(description_.empty()); |
| description_ = description; |
| return *this; |
| } |
| |
| ActionBuilder& ActionBuilder::ExtraDescription(const string& extra_description) { |
| CHECK(!extra_description_.has_value()); |
| extra_description_ = extra_description; |
| return *this; |
| } |
| |
| ActionBuilder& ActionBuilder::ProgramName(const string& program_name) { |
| CHECK(!program_name_.has_value()); |
| program_name_ = program_name; |
| return *this; |
| } |
| |
| ActionBuilder& ActionBuilder::AddRequiredParameter( |
| const ActionArgsDescriptor::Arg& arg) { |
| args_.required.push_back(arg); |
| return *this; |
| } |
| |
| ActionBuilder& ActionBuilder::AddRequiredVariadicParameter( |
| const ActionArgsDescriptor::Arg& arg) { |
| DCHECK(!args_.variadic); |
| args_.variadic = arg; |
| return *this; |
| } |
| |
| ActionBuilder& ActionBuilder::AddOptionalParameter( |
| string param, |
| optional<std::string> default_value, |
| optional<std::string> description) { |
| #ifndef NDEBUG |
| // Make sure this gflag exists. |
| string option; |
| DCHECK(google::GetCommandLineOption(param.c_str(), &option)) << "unknown option: " << param; |
| #endif |
| args_.optional.emplace_back(ActionArgsDescriptor::Flag({ std::move(param), |
| std::move(default_value), |
| std::move(description) })); |
| return *this; |
| } |
| |
| unique_ptr<Action> ActionBuilder::Build() { |
| CHECK(!description_.empty()); |
| unique_ptr<Action> action(new Action()); |
| action->name_ = name_; |
| action->description_ = description_; |
| action->extra_description_ = extra_description_; |
| action->program_name_ = program_name_; |
| action->runner_ = runner_; |
| action->args_ = args_; |
| return action; |
| } |
| |
| Status Action::Run(const vector<Mode*>& chain, |
| const unordered_map<string, string>& required_args, |
| const vector<string>& variadic_args) const { |
| SetOptionalParameterDefaultValues(); |
| |
| // If `program_name_` is defined, initialize the logging as if the |
| // program binary name was `program_name_`, otherwise fallback to the |
| // default behavior of using argv0. |
| if (program_name_) { |
| CHECK_NE("", google::SetCommandLineOptionWithMode("log_filename", |
| program_name_->c_str(), |
| google::FlagSettingMode::SET_FLAGS_DEFAULT)); |
| } |
| kudu::InitGoogleLoggingSafe(program_name_.value_or(gflags::GetArgv0()).c_str()); |
| kudu::ValidateFlags(); |
| |
| return runner_({ chain, this, required_args, variadic_args }); |
| } |
| |
| string Action::BuildHelp(const vector<Mode*>& chain, |
| Action::HelpMode mode) const { |
| SetOptionalParameterDefaultValues(); |
| string usage_msg = Substitute("Usage: $0 $1", BuildUsageString(chain), name()); |
| string desc_msg; |
| for (const auto& param : args_.required) { |
| usage_msg += Substitute(" <$0>", param.name); |
| desc_msg += FakeDescribeOneFlag(param); |
| desc_msg += "\n"; |
| } |
| if (args_.variadic) { |
| const ActionArgsDescriptor::Arg& param = *args_.variadic; |
| usage_msg += Substitute(" <$0>...", param.name); |
| desc_msg += FakeDescribeOneFlag(param); |
| desc_msg += "\n"; |
| } |
| for (const auto& param : args_.optional) { |
| google::CommandLineFlagInfo gflag_info = |
| google::GetCommandLineFlagInfoOrDie(param.name.c_str()); |
| |
| if (param.description) { |
| gflag_info.description = *param.description; |
| } |
| |
| if (gflag_info.type == "bool") { |
| if (gflag_info.default_value == "false") { |
| usage_msg += Substitute(" [-$0]", param.name); |
| } else { |
| usage_msg += Substitute(" [-no$0]", param.name); |
| } |
| } else { |
| string noun; |
| string::size_type last_underscore_idx = param.name.rfind('_'); |
| if (last_underscore_idx != string::npos && |
| last_underscore_idx != param.name.size() - 1) { |
| noun = param.name.substr(last_underscore_idx + 1); |
| } else { |
| noun = param.name; |
| } |
| usage_msg += Substitute(" [-$0=<$1>]", param.name, noun); |
| } |
| desc_msg += google::DescribeOneFlag(gflag_info); |
| desc_msg += "\n"; |
| } |
| if (mode == USAGE_ONLY) { |
| return usage_msg; |
| } |
| string msg; |
| AppendHardWrapped(usage_msg, 8, &msg); |
| msg += "\n\n"; |
| AppendHardWrapped(description_, 0, &msg); |
| if (extra_description_) { |
| msg += "\n\n"; |
| AppendHardWrapped(*extra_description_, 0, &msg); |
| } |
| msg += "\n\n"; |
| msg += desc_msg; |
| return msg; |
| } |
| |
| string Action::BuildHelpXML(const vector<Mode*>& chain) const { |
| SetOptionalParameterDefaultValues(); |
| string usage = Substitute("$0 $1", BuildUsageString(chain), name()); |
| string xml; |
| xml += "<action>"; |
| xml += Substitute("<name>$0</name>", name()); |
| xml += Substitute("<description>$0</description>", |
| EscapeForHtmlToString(description())); |
| xml += Substitute("<extra_description>$0</extra_description>", |
| EscapeForHtmlToString(extra_description().value_or(""))); |
| for (const auto& r : args().required) { |
| usage += Substitute(" <$0>", r.name); |
| xml += "<argument>"; |
| xml += "<kind>required</kind>"; |
| xml += Substitute("<name>$0</name>", r.name); |
| xml += Substitute("<description>$0</description>", |
| EscapeForHtmlToString(r.description)); |
| xml += "<type>string</type>"; |
| xml += "</argument>"; |
| } |
| |
| if (args().variadic) { |
| const ActionArgsDescriptor::Arg& v = *args().variadic; |
| usage += Substitute(" <$0>...", v.name); |
| xml += "<argument>"; |
| xml += "<kind>variadic</kind>"; |
| xml += Substitute("<name>$0</name>", v.name); |
| xml += Substitute("<description>$0</description>", |
| EscapeForHtmlToString(v.description)); |
| xml += "<type>string</type>"; |
| xml += "</argument>"; |
| } |
| |
| for (const auto& o : args().optional) { |
| google::CommandLineFlagInfo gflag_info = |
| google::GetCommandLineFlagInfoOrDie(o.name.c_str()); |
| |
| if (o.description) { |
| gflag_info.description = *o.description; |
| } |
| |
| if (gflag_info.type == "bool") { |
| if (gflag_info.default_value == "false") { |
| usage += Substitute(" [-$0]", o.name); |
| } else { |
| usage += Substitute(" [-no$0]", o.name); |
| } |
| } else { |
| string noun; |
| string::size_type last_underscore_idx = o.name.rfind('_'); |
| if (last_underscore_idx != string::npos && |
| last_underscore_idx != o.name.size() - 1) { |
| noun = o.name.substr(last_underscore_idx + 1); |
| } else { |
| noun = o.name; |
| } |
| usage += Substitute(" [-$0=<$1>]", o.name, noun); |
| } |
| |
| xml += "<argument>"; |
| xml += "<kind>optional</kind>"; |
| xml += Substitute("<name>$0</name>", gflag_info.name); |
| xml += Substitute("<description>$0</description>", |
| EscapeForHtmlToString(gflag_info.description)); |
| xml += Substitute("<type>$0</type>", gflag_info.type); |
| xml += Substitute("<default_value>$0</default_value>", |
| EscapeForHtmlToString(gflag_info.default_value)); |
| xml += "</argument>"; |
| } |
| xml += Substitute("<usage>$0</usage>", EscapeForHtmlToString(usage)); |
| xml += "</action>"; |
| return xml; |
| } |
| |
| void Action::SetOptionalParameterDefaultValues() const { |
| for (const auto& param : args_.optional) { |
| if (param.default_value) { |
| CHECK_NE("", google::SetCommandLineOptionWithMode(param.name.c_str(), |
| param.default_value->c_str(), |
| google::FlagSettingMode::SET_FLAGS_DEFAULT)); |
| } |
| } |
| } |
| |
| void AppendHardWrapped(StringPiece to_append, |
| int continuation_indent, |
| string* dst) { |
| constexpr int kWrapColumns = 78; |
| DCHECK_LT(continuation_indent, kWrapColumns); |
| |
| if (to_append.empty() && dst->empty()) return; |
| |
| // The string we're appending to might not be already at a newline. |
| size_t last_line_length = dst->size(); |
| auto newline_pos = dst->rfind('\n'); |
| if (newline_pos != string::npos) { |
| last_line_length = dst->size() - newline_pos; |
| } |
| vector<StringPiece> lines = strings::Split(to_append, "\n"); |
| for (const auto& line : lines) { |
| // Iterate through the words deciding where to wrap. |
| vector<StringPiece> words = strings::Split(line, " "); |
| if (words.empty()) continue; |
| for (const auto& word : words) { |
| // If the next word won't fit on this line, break before we append it. |
| if (last_line_length + word.size() > kWrapColumns) { |
| dst->push_back('\n'); |
| for (int i = 0; i < continuation_indent; i++) { |
| dst->push_back(' '); |
| } |
| last_line_length = continuation_indent; |
| } |
| word.AppendToString(dst); |
| dst->push_back(' '); |
| last_line_length += word.size() + 1; |
| } |
| // Remove the extra space that we added at the end. |
| dst->resize(dst->size() - 1); |
| dst->push_back('\n'); |
| last_line_length = continuation_indent; |
| } |
| // Remove the extra Enter key that we added at the end. |
| dst->resize(dst->size() - 1); |
| } |
| } // namespace tools |
| } // namespace kudu |