Skip to content

Commit

Permalink
[MISC] Add quotes to strings and paths (#224)
Browse files Browse the repository at this point in the history
* [MISC] Add quotes to string and path options

* [MISC] Also remove period after default message

* [TEST] Add API patches
  • Loading branch information
eseiler authored Feb 7, 2024
1 parent fb9552d commit 0ef21e2
Show file tree
Hide file tree
Showing 15 changed files with 699 additions and 73 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ OPTIONS
Eating Numbers
-i, --int (signed 32 bit integer)
Desc. Default: 0.
Desc. Default: 0
Common options
-h, --help
Expand All @@ -144,7 +144,7 @@ OPTIONS
Export the help page information. Value must be one of [html, man,
ctd, cwl].
--version-check (bool)
Whether to check for the newest app version. Default: true.
Whether to check for the newest app version. Default: true
VERSION
Last update:
Expand Down
103 changes: 91 additions & 12 deletions include/sharg/detail/format_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class format_base
*
* \details Special characters considered are `"`, `\`, `&`, `<` and `>`.
*/
std::string escape_special_xml_chars(std::string const & original)
static std::string escape_special_xml_chars(std::string const & original)
{
std::string escaped;
escaped.reserve(original.size()); // will be at least as long
Expand Down Expand Up @@ -183,6 +183,62 @@ class format_base

return tmp;
}

/*!\brief Returns the default message for the help page.
* \tparam option_type The type of the option.
* \tparam default_type The type of the default value.
* \param[in] option The option to get the default message for.
* \param[in] value The default value to get the default message for.
* \returns The default message for the help page (" Default: <default-value>. ").
* \details
* `value` is either `config.default_message`, or the same as `option`.
* If the `option_type` is a std::string or std::filesystem::path, the value is quoted.
* If the `option_type` is a container of std::string or std::filesystem::path, each individual value is quoted;
* if a `config.default_message` is provided, it will not be quoted.
*/
template <typename option_type, typename default_type>
static std::string get_default_message(option_type const & SHARG_DOXYGEN_ONLY(option), default_type const & value)
{
static_assert(std::same_as<option_type, default_type> || std::same_as<default_type, std::string>);

std::stringstream message{};
message << " Default: ";

if constexpr (detail::is_container_option<option_type>)
{
// If we have a list of strings, we want to quote each string.
if constexpr (std::same_as<std::ranges::range_value_t<default_type>, std::string>)
{
auto view = std::views::transform(value,
[](auto const & val)
{
return std::quoted(val);
});
message << detail::to_string(view);
}
else // Otherwise we just print the list or the default_message without quotes.
{
message << detail::to_string(value);
}
}
else
{
static constexpr bool option_is_string = std::same_as<option_type, std::string>;
static constexpr bool option_is_path = std::same_as<option_type, std::filesystem::path>;
static constexpr bool value_is_string = std::same_as<default_type, std::string>;

// Quote: std::string (from value + default_message), and std::filesystem::path (default_message).
// std::filesystem::path is quoted by the STL's operator<< in detail::to_string.
static constexpr bool needs_string_quote = option_is_string || (option_is_path && value_is_string);

if constexpr (needs_string_quote)
message << std::quoted(value);
else
message << detail::to_string(value);
}

return message.str();
}
};

/*!\brief The format that contains all helper functions needed in all formats for
Expand Down Expand Up @@ -227,12 +283,14 @@ class format_help_base : public format_base
{
std::string id = prep_id_for_help(config.short_id, config.long_id) + " " + option_type_and_list_info(value);
std::string info{config.description};

if (config.default_message.empty())
info += ((config.required) ? std::string{" "} : detail::to_string(" Default: ", value, ". "));
info += ((config.required) ? std::string{} : get_default_message(value, value));
else
info += detail::to_string(" Default: ", config.default_message, ". ");
info += get_default_message(value, config.default_message);

info += config.validator.get_help_page_message();
if (auto const & validator_message = config.validator.get_help_page_message(); !validator_message.empty())
info += ". " + validator_message;

store_help_page_element(
[this, id, info]()
Expand Down Expand Up @@ -262,20 +320,41 @@ class format_help_base : public format_base
template <typename option_type, typename validator_t>
void add_positional_option(option_type & value, config<validator_t> const & config)
{
// a list at the end may be empty and thus have a default value
auto positional_default_message = [&value]() -> std::string
{
if constexpr (detail::is_container_option<option_type>)
{
return get_default_message(value, value);
}
else
{
(void)value; // Silence unused variable warning.
return {};
}
};

auto positional_validator_message = [&config]() -> std::string
{
if (auto const & validator_message = config.validator.get_help_page_message(); !validator_message.empty())
return ". " + validator_message;
else
return {};
};

positional_option_calls.push_back(
[this, &value, description = config.description, validator = config.validator]()
[this,
&value,
default_message = positional_default_message(),
validator_message = positional_validator_message(),
description = config.description]()
{
++positional_option_count;
derived_t().print_list_item(detail::to_string("\\fBARGUMENT-",
positional_option_count,
"\\fP ",
option_type_and_list_info(value)),
description +
// a list at the end may be empty and thus have a default value
((detail::is_container_option<option_type>)
? detail::to_string(" Default: ", value, ". ")
: std::string{" "})
+ validator.get_help_page_message());
description + default_message + validator_message);
});
}

Expand Down Expand Up @@ -343,7 +422,7 @@ class format_help_base : public format_base
+ detail::supported_exports + ".");
if (version_check_dev_decision == update_notifications::on)
derived_t().print_list_item("\\fB--version-check\\fP (bool)",
"Whether to check for the newest app version. Default: true.");
"Whether to check for the newest app version. Default: true");

if (!meta.examples.empty())
{
Expand Down
44 changes: 34 additions & 10 deletions include/sharg/detail/format_tdl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,14 @@ class format_tdl : format_base
void add_option(option_type & value, config<validator_t> const & config)
{
auto description = config.description;
description += (config.required ? std::string{" "} : detail::to_string(" Default: ", value, ". "));
description += config.validator.get_help_page_message();

if (config.default_message.empty())
description += ((config.required) ? std::string{} : get_default_message(value, value));
else
description += get_default_message(value, config.default_message);

if (auto const & validator_message = config.validator.get_help_page_message(); !validator_message.empty())
description += ". " + validator_message;

auto tags = std::set<std::string>{};
if (config.required)
Expand Down Expand Up @@ -263,19 +269,37 @@ class format_tdl : format_base
template <typename option_type, typename validator_t>
void add_positional_option(option_type & value, config<validator_t> const & config)
{
std::string msg = config.validator.get_help_page_message();
// a list at the end may be empty and thus have a default value
auto positional_default_message = [&value]() -> std::string
{
if constexpr (detail::is_container_option<option_type>)
{
return get_default_message(value, value);
}
else
{
(void)value; // Silence unused variable warning.
return {};
}
};

auto positional_validator_message = [&config]() -> std::string
{
if (auto const & validator_message = config.validator.get_help_page_message(); !validator_message.empty())
return ". " + validator_message;
else
return {};
};

positional_option_calls.push_back(
[this, &value, config, msg](std::string_view)
[this,
config,
default_message = positional_default_message(),
validator_message = positional_validator_message()](std::string_view)
{
auto id = "positional_" + std::to_string(positional_option_count);
++positional_option_count;
auto description =
config.description +
// a list at the end may be empty and thus have a default value
((detail::is_container_option<option_type>) ? detail::to_string(" Default: ", value, ". ")
: std::string{" "})
+ msg;
auto description = config.description + default_message + validator_message;

parameters.push_back(tdl::Node{
.name = id,
Expand Down
13 changes: 10 additions & 3 deletions include/sharg/detail/to_string.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ static std::string const supported_exports =
"[html, man]";
#endif

//!\brief Concept for views whose value type is ostreamable.
template <typename container_t>
concept is_ostreamable_view = std::ranges::view<container_t> && ostreamable<std::ranges::range_value_t<container_t>>;

/*!\brief Streams all parameters via std::ostringstream and returns a concatenated string.
* \ingroup misc
* \tparam value_types Must be sharg::ostreamable (stream << value).
Expand All @@ -42,7 +46,11 @@ std::string to_string(value_types &&... values)

auto print = [&stream](auto && val)
{
if constexpr (is_container_option<std::remove_cvref_t<decltype(val)>>)
using value_t = std::remove_cvref_t<decltype(val)>;

// When passing a `std::vector<std::string> | std::views::transform(...)` which returns `std::quoted(str)` for
// each element, the `std::quoted`'s return value does not model a range, but is ostreamable.
if constexpr (is_container_option<value_t> || is_ostreamable_view<value_t>)
{
if (val.empty())
{
Expand All @@ -58,8 +66,7 @@ std::string to_string(value_types &&... values)
stream << ']';
}
}
else if constexpr (std::is_same_v<std::remove_cvref_t<decltype(val)>, int8_t>
|| std::is_same_v<std::remove_cvref_t<decltype(val)>, uint8_t>)
else if constexpr (std::is_same_v<value_t, int8_t> || std::is_same_v<value_t, uint8_t>)
{
stream << static_cast<int16_t>(val);
}
Expand Down
88 changes: 88 additions & 0 deletions test/api_stability/1.1.1/0001-API-Update-TDL.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
From 724ad34dea87caf2dde5f19af51d7b5527acf3a2 Mon Sep 17 00:00:00 2001
From: Simon Gene Gottlieb <[email protected]>
Date: Thu, 5 Oct 2023 13:43:30 +0200
Subject: [PATCH 1/2] [API] Update TDL

---
test/unit/detail/format_ctd_test.cpp | 2 +-
test/unit/detail/format_cwl_test.cpp | 22 +++++++++++-----------
2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/test/unit/detail/format_ctd_test.cpp b/test/unit/detail/format_ctd_test.cpp
index fa6a5b6..352acaa 100644
--- a/test/unit/detail/format_ctd_test.cpp
+++ b/test/unit/detail/format_ctd_test.cpp
@@ -110,7 +110,7 @@ TEST_F(format_ctd_test, empty_information)
// Create the dummy parser.
sharg::parser parser{"default", argv.size(), argv.data()};
parser.info.date = "December 01, 1994";
- parser.info.version = "1.1.1";
+ parser.info.version = "1.1.2-rc.1";
parser.info.man_page_title = "default_man_page_title";
parser.info.short_description = "A short description here.";

diff --git a/test/unit/detail/format_cwl_test.cpp b/test/unit/detail/format_cwl_test.cpp
index d2bb143..5591a88 100644
--- a/test/unit/detail/format_cwl_test.cpp
+++ b/test/unit/detail/format_cwl_test.cpp
@@ -20,12 +20,12 @@ TEST(format_cwl_test, empty_information)
parser.info.man_page_title = "default_man_page_title";
parser.info.short_description = "A short description here.";

- std::string expected_short = "inputs:\n"
- " {}\n"
- "outputs:\n"
- " {}\n"
- "label: default\n"
+ std::string expected_short = "label: default\n"
"doc: \"\"\n"
+ "inputs:\n"
+ " []\n"
+ "outputs:\n"
+ " []\n"
"cwlVersion: v1.2\n"
"class: CommandLineTool\n"
"baseCommand:\n"
@@ -75,7 +75,9 @@ TEST(format_cwl_test, full_information)
parser.info.examples.push_back("example");
parser.info.examples.push_back("example2");

- std::string expected_short = "inputs:\n"
+ std::string expected_short = "label: default\n"
+ "doc: \"description\\ndescription2\\n\"\n"
+ "inputs:\n"
" int:\n"
" doc: \"this is a int option. Default: 5. \"\n"
" type: long?\n"
@@ -87,9 +89,7 @@ TEST(format_cwl_test, full_information)
" inputBinding:\n"
" prefix: --jint\n"
"outputs:\n"
- " {}\n"
- "label: default\n"
- "doc: \"description\\ndescription2\\n\"\n"
+ " []\n"
"cwlVersion: v1.2\n"
"class: CommandLineTool\n"
"baseCommand:\n"
@@ -193,6 +193,8 @@ TEST(format_cwl_test, subparser)
sub_parser.info.examples.push_back("example2");

std::string expected_short =
+ "label: default-index\n"
+ "doc: \"\"\n"
"inputs:\n"
" int:\n"
" doc: \"this is a int option. Default: 5. \"\n"
@@ -251,8 +253,6 @@ TEST(format_cwl_test, subparser)
" type: Directory?\n"
" outputBinding:\n"
" glob: $(inputs.path05)\n"
- "label: default-index\n"
- "doc: \"\"\n"
"cwlVersion: v1.2\n"
"class: CommandLineTool\n"
"baseCommand:\n"
--
2.43.0

Loading

0 comments on commit 0ef21e2

Please sign in to comment.