1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-01-16 07:08:01 +00:00

Compare commits

...

6 Commits

Author SHA1 Message Date
allcontributors[bot]
a9e8c56499
docs: add dfleury2 as a contributor (#532)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-10-28 12:34:47 -04:00
D. Fleury
a7902531c9
fix: add multilines array support for TOML (#528)
* Adds multilines array support

* Update include/CLI/Config.hpp

Co-authored-by: David Fleury <david.fleury@expandium.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2020-10-28 12:34:24 -04:00
Philip Top
69674dc91b
fix: help all was showing up in the required list error if requirement are not met. (#530) 2020-10-28 12:33:15 -04:00
djerius
e50a75f231
feat: create pkg-config file (#523) 2020-10-28 12:06:05 -04:00
Philip Top
89926dc820
feat: support for Atomic types in flags and options (#520) 2020-10-28 11:57:47 -04:00
Henry Schreiner
d621be0565 ci: label PRs when merged 2020-10-28 11:34:45 -04:00
15 changed files with 301 additions and 20 deletions

View File

@ -422,6 +422,15 @@
"contributions": [
"code"
]
},
{
"login": "dfleury2",
"name": "D. Fleury",
"avatar_url": "https://avatars1.githubusercontent.com/u/4805384?v=4",
"profile": "https://github.com/dfleury2",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

4
.github/labeler_merged.yml vendored Normal file
View File

@ -0,0 +1,4 @@
needs changelog:
- all: ['!CHANGELOG.md']
needs README:
- all: ['!README.md']

15
.github/workflows/pr_merged.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: PR merged
on:
pull_request_target:
types: [closed]
jobs:
label-merged:
name: Changelog needed
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- uses: actions/labeler@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler_merged.yml

View File

@ -224,6 +224,8 @@ if(CLI11_INSTALL)
NAMESPACE CLI11::
FILE CLI11Targets.cmake)
include(cmake/CLI11GeneratePkgConfig.cmake)
# Register in the user cmake package registry
export(PACKAGE CLI11)
endif()

View File

@ -961,6 +961,7 @@ This project was created by [Henry Schreiner](https://github.com/henryiii) and m
<tr>
<td align="center"><a href="https://github.com/jakoblover"><img src="https://avatars0.githubusercontent.com/u/14160441?v=4" width="100px;" alt=""/><br /><sub><b>Jakob Lover</b></sub></a><br /><a href="https://github.com/CLIUtils/CLI11/commits?author=jakoblover" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ZeeD26"><img src="https://avatars2.githubusercontent.com/u/2487468?v=4" width="100px;" alt=""/><br /><sub><b>Dominik Steinberger</b></sub></a><br /><a href="https://github.com/CLIUtils/CLI11/commits?author=ZeeD26" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dfleury2"><img src="https://avatars1.githubusercontent.com/u/4805384?v=4" width="100px;" alt=""/><br /><sub><b>D. Fleury</b></sub></a><br /><a href="https://github.com/CLIUtils/CLI11/commits?author=dfleury2" title="Code">💻</a></td>
</tr>
</table>

9
cmake/CLI11.pc.in Normal file
View File

@ -0,0 +1,9 @@
prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix}
includedir=${prefix}/include
Name: CLI11
Description: C++ command line parser
Version: @PROJECT_VERSION@
Cflags: -I${includedir}

View File

@ -0,0 +1,6 @@
configure_file("cmake/CLI11.pc.in" "CLI11.pc" @ONLY)
install(FILES "${PROJECT_BINARY_DIR}/CLI11.pc"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")

View File

@ -789,7 +789,8 @@ class App {
/// Add option for flag with integer result - defaults to allowing multiple passings, but can be forced to one
/// if `multi_option_policy(CLI::MultiOptionPolicy::Throw)` is used.
template <typename T,
enable_if_t<std::is_integral<T>::value && !is_bool<T>::value, detail::enabler> = detail::dummy>
enable_if_t<std::is_constructible<T, std::int64_t>::value && !is_bool<T>::value, detail::enabler> =
detail::dummy>
Option *add_flag(std::string flag_name,
T &flag_count, ///< A variable holding the count
std::string flag_description = "") {
@ -810,7 +811,7 @@ class App {
/// that can be converted from a string
template <typename T,
enable_if_t<!detail::is_mutable_container<T>::value && !std::is_const<T>::value &&
(!std::is_integral<T>::value || is_bool<T>::value) &&
(!std::is_constructible<T, std::int64_t>::value || is_bool<T>::value) &&
!std::is_constructible<std::function<void(int)>, T>::value,
detail::enabler> = detail::dummy>
Option *add_flag(std::string flag_name,
@ -824,9 +825,9 @@ class App {
}
/// Vector version to capture multiple flags.
template <
typename T,
enable_if_t<!std::is_assignable<std::function<void(std::int64_t)>, T>::value, detail::enabler> = detail::dummy>
template <typename T,
enable_if_t<!std::is_assignable<std::function<void(std::int64_t)> &, T>::value, detail::enabler> =
detail::dummy>
Option *add_flag(std::string flag_name,
std::vector<T> &flag_results, ///< A vector of values with the flag results
std::string flag_description = "") {
@ -2252,10 +2253,13 @@ class App {
}
if(require_option_min_ > used_options || (require_option_max_ > 0 && require_option_max_ < used_options)) {
auto option_list = detail::join(options_, [](const Option_p &ptr) { return ptr->get_name(false, true); });
if(option_list.compare(0, 10, "-h,--help,") == 0) {
option_list.erase(0, 10);
}
auto option_list = detail::join(options_, [this](const Option_p &ptr) {
if(ptr.get() == help_ptr_ || ptr.get() == help_all_ptr_) {
return std::string{};
}
return ptr->get_name(false, true);
});
auto subc_list = get_subcommands([](App *app) { return ((app->get_name().empty()) && (!app->disabled_)); });
if(!subc_list.empty()) {
option_list += "," + detail::join(subc_list, [](const App *app) { return app->get_display_name(); });

View File

@ -211,7 +211,11 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
if(pos != std::string::npos) {
name = detail::trim_copy(line.substr(0, pos));
std::string item = detail::trim_copy(line.substr(pos + 1));
if(item.size() > 1 && item.front() == aStart && item.back() == aEnd) {
if(item.size() > 1 && item.front() == aStart) {
for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
detail::trim(multiline);
item += multiline;
}
items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
} else if((isDefaultArray || isINIArray) && item.find_first_of(aSep) != std::string::npos) {
items_buffer = detail::split_up(item, aSep);

View File

@ -76,10 +76,14 @@ std::string join(const T &v, Callable func, std::string delim = ",") {
std::ostringstream s;
auto beg = std::begin(v);
auto end = std::end(v);
if(beg != end)
s << func(*beg++);
auto loc = s.tellp();
while(beg != end) {
s << delim << func(*beg++);
auto nloc = s.tellp();
if(nloc > loc) {
s << delim;
loc = nloc;
}
s << func(*beg++);
}
return s.str();
}

View File

@ -964,7 +964,22 @@ bool lexical_cast(const std::string &input, T &output) {
/// wrapper types
template <typename T,
enable_if_t<classify_object<T>::value == object_category::wrapper_value, detail::enabler> = detail::dummy>
enable_if_t<classify_object<T>::value == object_category::wrapper_value &&
std::is_assignable<T &, typename T::value_type>::value,
detail::enabler> = detail::dummy>
bool lexical_cast(const std::string &input, T &output) {
typename T::value_type val;
if(lexical_cast(input, val)) {
output = val;
return true;
}
return from_stream(input, output);
}
template <typename T,
enable_if_t<classify_object<T>::value == object_category::wrapper_value &&
!std::is_assignable<T &, typename T::value_type>::value && std::is_assignable<T &, T>::value,
detail::enabler> = detail::dummy>
bool lexical_cast(const std::string &input, T &output) {
typename T::value_type val;
if(lexical_cast(input, val)) {
@ -1019,8 +1034,36 @@ bool lexical_cast(const std::string &input, T &output) {
return from_stream(input, output);
}
/// Non-string convertible from an int
template <typename T,
enable_if_t<classify_object<T>::value == object_category::other && std::is_assignable<T &, int>::value,
detail::enabler> = detail::dummy>
bool lexical_cast(const std::string &input, T &output) {
int val;
if(integral_conversion(input, val)) {
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4800)
#endif
// with Atomic<XX> this could produce a warning due to the conversion but if atomic gets here it is an old style
// so will most likely still work
output = val;
#ifdef _MSC_VER
#pragma warning(pop)
#endif
return true;
}
// LCOV_EXCL_START
// This version of cast is only used for odd cases in an older compilers the fail over
// from_stream is tested elsewhere an not relevent for coverage here
return from_stream(input, output);
// LCOV_EXCL_STOP
}
/// Non-string parsable by a stream
template <typename T, enable_if_t<classify_object<T>::value == object_category::other, detail::enabler> = detail::dummy>
template <typename T,
enable_if_t<classify_object<T>::value == object_category::other && !std::is_assignable<T &, int>::value,
detail::enabler> = detail::dummy>
bool lexical_cast(const std::string &input, T &output) {
static_assert(is_istreamable<T>::value,
"option object type must have a lexical cast overload or streaming input operator(>>) defined, if it "
@ -1043,7 +1086,7 @@ bool lexical_assign(const std::string &input, AssignTo &output) {
/// Assign a value through lexical cast operations
template <typename AssignTo,
typename ConvertTo,
enable_if_t<std::is_same<AssignTo, ConvertTo>::value &&
enable_if_t<std::is_same<AssignTo, ConvertTo>::value && std::is_assignable<AssignTo &, AssignTo>::value &&
classify_object<AssignTo>::value != object_category::string_assignable &&
classify_object<AssignTo>::value != object_category::string_constructible,
detail::enabler> = detail::dummy>
@ -1052,9 +1095,46 @@ bool lexical_assign(const std::string &input, AssignTo &output) {
output = AssignTo{};
return true;
}
return lexical_cast(input, output);
}
/// Assign a value through lexical cast operations
template <typename AssignTo,
typename ConvertTo,
enable_if_t<std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, AssignTo>::value &&
classify_object<AssignTo>::value == object_category::wrapper_value,
detail::enabler> = detail::dummy>
bool lexical_assign(const std::string &input, AssignTo &output) {
if(input.empty()) {
typename AssignTo::value_type emptyVal{};
output = emptyVal;
return true;
}
return lexical_cast(input, output);
}
/// Assign a value through lexical cast operations for int compatible values
/// mainly for atomic operations on some compilers
template <typename AssignTo,
typename ConvertTo,
enable_if_t<std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, AssignTo>::value &&
classify_object<AssignTo>::value != object_category::wrapper_value &&
std::is_assignable<AssignTo &, int>::value,
detail::enabler> = detail::dummy>
bool lexical_assign(const std::string &input, AssignTo &output) {
if(input.empty()) {
output = 0;
return true;
}
int val;
if(lexical_cast(input, val)) {
output = val;
return true;
}
return false;
}
/// Assign a value converted from a string in lexical cast to the output value directly
template <typename AssignTo,
typename ConvertTo,
@ -1366,10 +1446,11 @@ bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &outp
}
/// conversion for wrapper types
template <
typename AssignTo,
class ConvertTo,
enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value, detail::enabler> = detail::dummy>
template <typename AssignTo,
class ConvertTo,
enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value &&
std::is_assignable<ConvertTo &, ConvertTo>::value,
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
if(strings.empty() || strings.front().empty()) {
output = ConvertTo{};
@ -1383,6 +1464,26 @@ bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &outpu
return false;
}
/// conversion for wrapper types
template <typename AssignTo,
class ConvertTo,
enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value &&
!std::is_assignable<AssignTo &, ConvertTo>::value,
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
using ConvertType = typename ConvertTo::value_type;
if(strings.empty() || strings.front().empty()) {
output = ConvertType{};
return true;
}
ConvertType val;
if(lexical_conversion<typename ConvertTo::value_type, typename ConvertTo::value_type>(strings, val)) {
output = val;
return true;
}
return false;
}
/// Sum a vector of flag representations
/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is
/// by
@ -1411,5 +1512,32 @@ void sum_flag_vector(const std::vector<std::string> &flags, T &output) {
output = static_cast<T>(count);
}
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4800)
#endif
// with Atomic<XX> this could produce a warning due to the conversion but if atomic gets here it is an old style so will
// most likely still work
/// Sum a vector of flag representations
/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is
/// by
/// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most
/// common true and false strings then uses stoll to convert the rest for summing
template <typename T,
enable_if_t<!std::is_signed<T>::value && !std::is_unsigned<T>::value, detail::enabler> = detail::dummy>
void sum_flag_vector(const std::vector<std::string> &flags, T &output) {
std::int64_t count{0};
for(auto &flag : flags) {
count += detail::to_flag_value(flag);
}
std::string out = detail::to_string(count);
lexical_cast(out, output);
}
#ifdef _MSC_VER
#pragma warning(pop)
#endif
} // namespace detail
} // namespace CLI

View File

@ -135,11 +135,14 @@ TEST_F(TApp, RequireOptionsError) {
app.add_flag("-c");
app.add_flag("--q");
app.add_flag("--this,--that");
app.set_help_flag("-h,--help");
app.set_help_all_flag("--help_all");
app.require_option(1, 2);
try {
app.parse("-c --q --this --that");
} catch(const CLI::RequiredError &re) {
EXPECT_THAT(re.what(), Not(HasSubstr("-h,--help")));
EXPECT_THAT(re.what(), Not(HasSubstr("help_all")));
}
EXPECT_NO_THROW(app.parse("-c --q"));

View File

@ -150,6 +150,47 @@ TEST(StringBased, Vector) {
EXPECT_EQ("seven", output.at(2).inputs.at(2));
}
TEST(StringBased, TomlVector) {
std::stringstream ofile;
ofile << "one = [three]\n";
ofile << "two = [four]\n";
ofile << "five = [six, and, seven]\n";
ofile << "eight = [nine, \n"
"ten, eleven, twelve \n"
"]\n";
ofile << "one_more = [one, \n"
"two, three ] \n";
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(5u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
EXPECT_EQ("five", output.at(2).name);
EXPECT_EQ(3u, output.at(2).inputs.size());
EXPECT_EQ("six", output.at(2).inputs.at(0));
EXPECT_EQ("and", output.at(2).inputs.at(1));
EXPECT_EQ("seven", output.at(2).inputs.at(2));
EXPECT_EQ("eight", output.at(3).name);
EXPECT_EQ(4u, output.at(3).inputs.size());
EXPECT_EQ("nine", output.at(3).inputs.at(0));
EXPECT_EQ("ten", output.at(3).inputs.at(1));
EXPECT_EQ("eleven", output.at(3).inputs.at(2));
EXPECT_EQ("twelve", output.at(3).inputs.at(3));
EXPECT_EQ("one_more", output.at(4).name);
EXPECT_EQ(3u, output.at(4).inputs.size());
EXPECT_EQ("one", output.at(4).inputs.at(0));
EXPECT_EQ("two", output.at(4).inputs.at(1));
EXPECT_EQ("three", output.at(4).inputs.at(2));
}
TEST(StringBased, Spaces) {
std::stringstream ofile;

View File

@ -7,6 +7,7 @@
#include "app_helper.hpp"
#include <array>
#include <atomic>
#include <climits>
#include <complex>
#include <cstdint>
@ -991,6 +992,8 @@ TEST(Types, TypeName) {
EXPECT_EQ("ENUM", enum_name2);
std::string umapName = CLI::detail::type_name<std::unordered_map<int, std::tuple<std::string, double>>>();
EXPECT_EQ("[INT,[TEXT,FLOAT]]", umapName);
vclass = CLI::detail::classify_object<std::atomic<int>>::value;
}
TEST(Types, OverflowSmall) {

View File

@ -5,6 +5,7 @@
// SPDX-License-Identifier: BSD-3-Clause
#include "app_helper.hpp"
#include <atomic>
#include <complex>
#include <cstdint>
#include <cstdlib>
@ -141,6 +142,33 @@ TEST_F(TApp, BoolAndIntFlags) {
EXPECT_EQ((unsigned int)2, uflag);
}
TEST_F(TApp, atomic_bool_flags) {
std::atomic<bool> bflag{false};
std::atomic<int> iflag{0};
app.add_flag("-b", bflag);
app.add_flag("-i,--int", iflag);
args = {"-b", "-i"};
run();
EXPECT_TRUE(bflag.load());
EXPECT_EQ(1, iflag.load());
args = {"-b", "-b"};
ASSERT_NO_THROW(run());
EXPECT_TRUE(bflag.load());
bflag = false;
args = {"-iii"};
run();
EXPECT_FALSE(bflag.load());
EXPECT_EQ(3, iflag.load());
args = {"--int=notanumber"};
EXPECT_THROW(run(), CLI::ConversionError);
}
TEST_F(TApp, BoolOption) {
bool bflag{false};
app.add_option("-b", bflag);
@ -167,6 +195,26 @@ TEST_F(TApp, BoolOption) {
EXPECT_FALSE(bflag);
}
TEST_F(TApp, atomic_int_option) {
std::atomic<int> i{0};
auto aopt = app.add_option("-i,--int", i);
args = {"-i4"};
run();
EXPECT_EQ(1u, app.count("--int"));
EXPECT_EQ(1u, app.count("-i"));
EXPECT_EQ(i, 4);
EXPECT_EQ(app["-i"]->as<std::string>(), "4");
EXPECT_EQ(app["--int"]->as<double>(), 4.0);
args = {"--int", "notAnInt"};
EXPECT_THROW(run(), CLI::ConversionError);
aopt->expected(0, 1);
args = {"--int"};
run();
EXPECT_EQ(i, 0);
}
TEST_F(TApp, CharOption) {
char c1{'t'};
app.add_option("-c", c1);