blob: 7dd9e07c0d8394868563f47ad6c3f79cbd62912e [file] [log] [blame]
// 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(" &lt;$0&gt;", 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(" &lt;$0&gt;...", 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=&lt;$1&gt;]", 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