Skip to content

Commit

Permalink
Updates for ignition style
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Carroll <[email protected]>
  • Loading branch information
mjcarroll committed Mar 17, 2022
1 parent 837ae0a commit efc6d66
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 24 deletions.
2 changes: 2 additions & 0 deletions cli/include/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ else()
INTERFACE
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/cli/include/external-cli>")
endif()

add_subdirectory(ignition/utils)
128 changes: 104 additions & 24 deletions cli/include/ignition/utils/cli/IgnitionFormatter.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Open Source Robotics Foundation
* Copyright (C) 2022 Open Source Robotics Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,29 +20,85 @@

#include <algorithm>
#include <string>
#include <sstream>
#include <vector>
#include <unordered_map>

#include "ignition/utils/cli/App.hpp"
#include "ignition/utils/cli/FormatterFwd.hpp"

namespace CLI
{
//////////////////////////////////////////////////
/// \brief CLI Formatter class that implements custom Ignition-specific
/// formatting.
///
/// More information on custom formatters:
/// https://cliutils.github.io/CLI11/book/chapters/formatting.html
class IgnitionFormatter: public Formatter {
class IgnitionFormatter: public CLI::Formatter {

//////////////////////////////////////////////////
public: explicit IgnitionFormatter(const CLI::App *_app)
{
// find needs/needed_by for root options
for (const CLI::Option *appOpt: _app->get_options())
{
for(const CLI::Option *needsOpt : appOpt->get_needs())
{
this->needed_by.insert({needsOpt->get_name(), appOpt->get_name()});
this->needs.insert({appOpt->get_name(), needsOpt->get_name()});
}
}

// find needs/needed_by for subcommand (or command group) options
auto subcommands = _app->get_subcommands([](const CLI::App*){return true;});
for (const CLI::App *sub : subcommands)
{
// find needs/needed_by for root options
for (const CLI::Option *subOpt: sub->get_options())
{
for(const CLI::Option *needsOpt : subOpt->get_needs())
{
this->needed_by.insert({needsOpt->get_name(), subOpt->get_name()});
this->needs.insert({subOpt->get_name(), needsOpt->get_name()});
}
}
}
}

//////////////////////////////////////////////////
public: std::string make_option_name(
const CLI::Option *opt, bool is_positional) const override {
if(is_positional)
return opt->get_name(true, false);
if (is_positional)
return opt->get_name(true, false);

std::stringstream out;

auto snames = opt->get_snames();
auto lnames = opt->get_lnames();

std::vector<std::string> sname_list;
std::transform(snames.begin(), snames.end(), std::back_inserter(sname_list),
[](const std::string &sname) { return "-" + sname; });

std::vector<std::string> lname_list;
std::transform(lnames.begin(), lnames.end(), std::back_inserter(lname_list),
[](const std::string &lname) { return "--" + lname; });

// If no short options, just use long
if (sname_list.empty())
{
out << CLI::detail::join(lname_list);
}
else
{
out << CLI::detail::join(sname_list);
// Put lnames in brackets to look like ruby formatting
if (!lnames.empty())
{
out << " [" << CLI::detail::join(lname_list) << "]";
}
}

return opt->get_name(false, true);
return out.str();
}


Expand All @@ -55,36 +111,60 @@ public: std::string make_option_opts(const CLI::Option *opt) const override {
out << " " << get_label(opt->get_type_name());
if(!opt->get_default_str().empty())
out << "=" << opt->get_default_str();
if(opt->get_expected_max() == detail::expected_max_vector_size)
if(opt->get_expected_max() == CLI::detail::expected_max_vector_size)
out << " ...";
else if(opt->get_expected_min() > 1)
out << " x " << opt->get_expected();

if(opt->get_required())
out << " " << get_label("REQUIRED");
}
if(!opt->get_envname().empty())
out << " (" << get_label("Env") << ":" << opt->get_envname() << ")";
if(!opt->get_needs().empty()) {
out << " " << get_label("Needs") << ":";
for(const Option *op : opt->get_needs())
out << " " << op->get_name();
}
if(!opt->get_excludes().empty()) {
out << " " << get_label("Excludes") << ":";
for(const Option *op : opt->get_excludes())
out << " " << op->get_name();
}
return out.str();

}


//////////////////////////////////////////////////
std::string make_option_desc(const CLI::Option *opt) const override {
return opt->get_description();
std::stringstream out;

out << opt->get_description();

if (opt->get_required())
{
out << "\nREQUIRED";
}

auto range = this->needs.equal_range(opt->get_name());
std::for_each(
range.first,
range.second,
[&out](const auto &opt_name)
{
out << "\nRequires: " << opt_name.second;
});

range = this->needed_by.equal_range(opt->get_name());
std::for_each(
range.first,
range.second,
[&out](const auto &opt_name)
{
out << "\nRequired by: " << opt_name.second;
});

if (!opt->get_excludes().empty()) {
out << "\n" << get_label("Excludes") << ":";
for(const CLI::Option *op : opt->get_excludes())
out << " " << op->get_name();
}

return out.str() + '\n';
}

/// \brief Track dependent options
private: std::unordered_multimap<std::string, std::string> needs;

/// \brief Track dependent options (inverse)
private: std::unordered_multimap<std::string, std::string> needed_by;
};
} // namespace CLI

#endif // IGNITION_UTILS_CLI_IGNITION_FORMATTER_HPP_
7 changes: 7 additions & 0 deletions cli/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ set(ign_utils_cli_target_name ${ign_utils_cli_target_name} PARENT_SCOPE)
if(NOT IGN_UTILS_VENDOR_CLI11)
target_link_libraries(${ign_utils_cli_target_name} INTERFACE CLI11::CLI11)
endif()

ign_get_libsources_and_unittests(sources gtest_sources)
ign_build_tests(TYPE UNIT
SOURCES ${gtest_sources}
LIB_DEPS
${ign_utils_cli_target_name}
)
171 changes: 171 additions & 0 deletions cli/src/cli_TEST.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Copyright (C) 2022 Open Source Robotics Foundation
*
* Licensed 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 <gtest/gtest.h>

#include <memory>

#include <ignition/utils/cli/CLI.hpp>
#include <ignition/utils/cli/IgnitionFormatter.hpp>

/////////////////////////////////////////////////
struct TestOptions
{
bool fooFlag;
int barFlag;

int intOption;
float floatOption;
std::string stringOption;
std::vector<std::string> multistringOption;

std::string needsOption;
std::string neededByOption;

std::string requiredOption;

int excludesOptionA;
int excludesOptionB;

float defaultValueOption {6.28f};
};

/////////////////////////////////////////////////
std::shared_ptr<TestOptions> addFlags(CLI::App &_app)
{
auto opt = std::make_shared<TestOptions>();
// Example of adding basic flags
_app.add_flag("-f,--foo", opt->fooFlag, "Just a flag");
_app.add_flag("-b,--bar", opt->barFlag, "Another flag");

// Example of adding basic options
_app.add_option("--int", opt->intOption, "Option that takes integer");
_app.add_option("--float", opt->floatOption, "Option that takes float");
_app.add_option("--string", opt->stringOption,
"Option that takes string.\n But also more description\n Another line");

auto multistringOpt = _app.add_option("--stringV",
opt->multistringOption, "Takes several strings");
multistringOpt->expected(2, 5);
multistringOpt->delimiter(',');

// Example of adding required option
_app.add_option("--required",
opt->requiredOption,
"This is a required option.")->required();

// Example of adding dependent options
auto neededByOpt = _app.add_option("--needed-by",
opt->neededByOption,
"This is an option another option needs.");
auto needsOpt = _app.add_option("--needs",
opt->needsOption,
"This is an option that needs another option.");
needsOpt->needs(neededByOpt);

// Example of adding mutually-exclusive options
auto excludesA = _app.add_option("--excludesA", opt->excludesOptionA,
"Only A or B can be used.");
auto excludesB = _app.add_option("--excludesB", opt->excludesOptionB,
"Only A or B can be used.");
excludesA->excludes(excludesB);
excludesB->excludes(excludesA);

_app.add_option("--default-value", opt->defaultValueOption,
"Option with default value", true);

return opt;
}

/////////////////////////////////////////////////
TEST(cli, flags)
{
CLI::App app("Test app");

auto opt = addFlags(app);

std::vector<std::string> argv = {"--foo", "--bar", "-b", "--required=1"};

app.callback([opt](){
EXPECT_TRUE(opt->fooFlag);
EXPECT_EQ(2, opt->barFlag);
});

EXPECT_NO_THROW(app.parse(argv));
}

/////////////////////////////////////////////////
TEST(cli, options)
{
CLI::App app("Test app");

auto opt = addFlags(app);

std::vector<std::string> argv = {
"--required=1",
"--int=50",
"--float=3.14",
"--string=baz",
"--stringV=foo,bar,baz",
};

app.callback([opt](){
EXPECT_EQ(50, opt->intOption);
EXPECT_EQ("baz", opt->stringOption);
EXPECT_FLOAT_EQ(3.14f, opt->floatOption);
EXPECT_EQ(3u, opt->multistringOption.size()) << opt->multistringOption[0];
});

app.formatter(std::make_shared<IgnitionFormatter>(&app));
EXPECT_NO_THROW(app.help());
EXPECT_NO_THROW(app.parse(argv));
}

/////////////////////////////////////////////////
TEST(cli, help_text)
{
CLI::App app("Test app");

auto opt = addFlags(app);
app.callback([opt](){});

app.formatter(std::make_shared<IgnitionFormatter>(&app));
std::cout << app.help();
}

/////////////////////////////////////////////////
TEST(cli, config_text)
{
CLI::App app("Test app");

auto opt = addFlags(app);
app.callback([opt](){});

std::vector<std::string> argv = {
"--int=50",
"--float=3.14",
"--string=bing",
"--needs=foo",
"--needed-by=bar",
"--required=baz"
};

EXPECT_NO_THROW(app.parse(argv));

std::cout << app.config_to_str(true, true);
}

0 comments on commit efc6d66

Please sign in to comment.