mysqlshdk/shellcore/shell_prompt_options.cc (245 lines of code) (raw):
/*
* Copyright (c) 2021, 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "mysqlshdk/shellcore/shell_prompt_options.h"
#include <map>
#include <unordered_set>
#include "mysqlshdk/include/scripting/type_info/custom.h"
#include "mysqlshdk/include/scripting/type_info/generic.h"
#include "mysqlshdk/libs/utils/utils_string.h"
namespace shcore {
namespace prompt {
namespace {
constexpr const char kPromptText[] = "text";
constexpr const char kPromptPassword[] = "password";
constexpr const char kPromptConfirm[] = "confirm";
constexpr const char kPromptSelect[] = "select";
constexpr const char kPromptFileSave[] = "fileSave";
constexpr const char kPromptFileOpen[] = "fileOpen";
constexpr const char kPromptDirectory[] = "directory";
} // namespace
std::string to_string(Prompt_type type) {
switch (type) {
case Prompt_type::CONFIRM:
return kPromptConfirm;
case Prompt_type::DIRECTORY:
return kPromptDirectory;
case Prompt_type::FILEOPEN:
return kPromptFileOpen;
case Prompt_type::FILESAVE:
return kPromptFileSave;
case Prompt_type::PASSWORD:
return kPromptPassword;
case Prompt_type::SELECT:
return kPromptSelect;
case Prompt_type::TEXT:
return kPromptText;
}
throw std::logic_error("Unhandled prompt type");
}
Prompt_type to_prompt_type(const std::string &type) {
if (shcore::str_caseeq(type, kPromptConfirm)) {
return Prompt_type::CONFIRM;
} else if (shcore::str_caseeq(type, kPromptDirectory)) {
return Prompt_type::DIRECTORY;
} else if (shcore::str_caseeq(type, kPromptFileOpen)) {
return Prompt_type::FILEOPEN;
} else if (shcore::str_caseeq(type, kPromptFileSave)) {
return Prompt_type::FILESAVE;
} else if (shcore::str_caseeq(type, kPromptPassword)) {
return Prompt_type::PASSWORD;
} else if (shcore::str_caseeq(type, kPromptSelect)) {
return Prompt_type::SELECT;
} else if (shcore::str_caseeq(type, kPromptText)) {
return Prompt_type::TEXT;
} else {
throw std::runtime_error("Invalid prompt type: '" + type + "'.");
}
}
char process_label(const std::string &s, std::string *out_display,
std::string *out_clean_text) {
out_display->clear();
if (s.empty()) return 0;
char letter = 0;
char prev = 0;
for (char c : s) {
if (prev == '&') letter = c;
if (c != '&') {
if (prev == '&') {
out_display->push_back('[');
out_display->push_back(c);
out_display->push_back(']');
} else {
out_display->push_back(c);
}
out_clean_text->push_back(c);
}
prev = c;
}
return letter;
}
bool Prompt_options::icomp(const std::string &lhs, const std::string &rhs) {
return shcore::str_casecmp(lhs.c_str(), rhs.c_str()) < 0;
}
Prompt_options::Prompt_options()
: allow_custom(false), m_allowed_answers_map(Prompt_options::icomp) {}
const shcore::Option_pack_def<Prompt_options> &Prompt_options::options() {
static const auto opts =
shcore::Option_pack_def<Prompt_options>()
.optional(k_title, &Prompt_options::title)
.optional(k_description, &Prompt_options::description)
.optional(k_type, &Prompt_options::set_type)
.optional(k_yes_label, &Prompt_options::yes_label)
.optional(k_no_label, &Prompt_options::no_label)
.optional(k_alt_label, &Prompt_options::alt_label)
.optional(k_options, &Prompt_options::select_items)
.optional(k_default_value, &Prompt_options::default_value)
.on_done(&Prompt_options::on_unpacked_options);
return opts;
}
void Prompt_options::set_type(const std::string &value) {
try {
type = to_prompt_type(value);
} catch (const std::runtime_error &err) {
throw std::invalid_argument(err.what());
}
}
void Prompt_options::on_unpacked_options() {
// Sets some default values to ease the rest of the logic
if (type == Prompt_type::CONFIRM) {
if (yes_label.empty()) {
yes_label = "&Yes";
}
if (no_label.empty()) {
no_label = "&No";
}
}
if ((!yes_label.empty() || !no_label.empty() || !alt_label.empty()) &&
type != Prompt_type::CONFIRM) {
throw std::invalid_argument(
"The 'yes', 'no' and 'alt' options only can be used on 'confirm' "
"prompts.");
}
auto add_valid_answer = [this](const std::string &answer,
const std::string &real_answer,
const std::string &context) {
if (m_allowed_answers_map.find(answer) != m_allowed_answers_map.end()) {
throw std::invalid_argument(shcore::str_format(
"'%s' %s is duplicated", answer.c_str(), context.c_str()));
}
m_allowed_answers_map[answer] = real_answer;
};
if (type == Prompt_type::SELECT) {
if (select_items.empty()) {
throw std::invalid_argument("The 'options' list can not be empty.");
} else {
for (size_t index = 0; index < select_items.size(); index++) {
auto stripped = shcore::str_strip(select_items[index]);
if (stripped.empty()) {
throw std::invalid_argument(
"The 'options' list can not contain empty or blank elements.");
}
add_valid_answer(stripped, stripped, "item");
add_valid_answer(std::to_string(index + 1), stripped, "index");
}
}
} else {
if (!select_items.empty()) {
throw std::invalid_argument(
"The 'options' list only can be used on 'select' prompts.");
}
}
if (type == Prompt_type::CONFIRM) {
std::vector<std::string> labels = {yes_label, no_label};
if (!alt_label.empty()) {
labels.push_back(alt_label);
}
try {
for (const auto &label : labels) {
std::string display;
std::string clean;
char shortcut = process_label(label, &display, &clean);
add_valid_answer(label, label, "label");
if (clean != label) {
add_valid_answer(clean, label, "value");
}
if (shortcut != 0) {
add_valid_answer(std::string(&shortcut, 1), label, "shortcut");
}
}
} catch (const std::invalid_argument &error) {
throw std::invalid_argument(
shcore::str_format("Labels, shortcuts and values in 'confirm' "
"prompts must be unique: %s",
error.what()));
}
}
if (default_value) {
try {
if (type == Prompt_type::SELECT) {
auto val = default_value.as_uint();
if (val == 0 || val > select_items.size()) {
throw std::invalid_argument(
"The 'defaultValue' should be the 1 based index of the default "
"option.");
}
} else {
default_value.check_type(shcore::Value_type::String);
const auto &val = default_value.as_string();
if (type == Prompt_type::CONFIRM) {
std::string real_answer;
if (!is_valid_answer(val, &real_answer)) {
std::vector<std::string> valid_answer_list;
for (const auto &item : m_allowed_answers_map) {
valid_answer_list.push_back(item.first);
}
throw std::invalid_argument(
"Invalid 'defaultValue', allowed values include: " +
shcore::str_join(valid_answer_list, ", "));
}
// Normalizes the default value to the real answer
default_value = shcore::Value(real_answer);
}
}
} catch (const shcore::Exception &err) {
std::string error =
shcore::str_replace(err.what(), "Invalid typecast",
"Invalid data type on 'defaultValue'");
throw std::invalid_argument(error);
}
}
}
bool Prompt_options::is_valid_answer(const std::string &answer,
std::string *real_answer) const {
bool ret_val = false;
std::string user_answer(answer);
if (answer.empty()) {
if (default_value) {
user_answer = default_value.as_string();
}
}
auto set_answer = [&ret_val, real_answer](const std::string &value) {
ret_val = true;
if (real_answer) {
*real_answer = value;
}
};
// Only handles the validation for SELECT/CONFIRM prompts, others should not
// be calling this
if (type == Prompt_type::SELECT) {
try {
int option = 0;
option = std::stoi(user_answer);
// The selection is an item number
if (option > 0 && option <= static_cast<int>(select_items.size())) {
set_answer(select_items[option - 1]);
}
} catch (...) {
// NOOP - This means it was not an int
if (allow_custom) {
set_answer(user_answer);
}
}
} else if (type == Prompt_type::CONFIRM) {
if (!user_answer.empty()) {
const auto answer_index = m_allowed_answers_map.find(user_answer);
if (answer_index != m_allowed_answers_map.end()) {
set_answer(answer_index->second);
}
}
}
return ret_val;
}
} // namespace prompt
} // namespace shcore