Skip to content

Commit

Permalink
catchorg#31 ability for subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
klaus triendl committed Oct 27, 2017
1 parent d31850a commit 42c8bad
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 6 deletions.
118 changes: 113 additions & 5 deletions include/clara.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ namespace detail {

class ExeName : public ComposableParserImpl<ExeName> {
std::shared_ptr<std::string> m_name;
std::shared_ptr<std::string> m_description;
std::shared_ptr<BoundRefBase> m_ref;

template<typename LambdaT>
Expand All @@ -585,7 +586,10 @@ namespace detail {
}

public:
ExeName() : m_name( std::make_shared<std::string>( "<executable>" ) ) {}
ExeName()
: m_name( std::make_shared<std::string>( "<executable>" ) ),
m_description( std::make_shared<std::string>() )
{}

explicit ExeName( std::string &ref ) : ExeName() {
m_ref = std::make_shared<BoundRef<std::string>>( ref );
Expand All @@ -602,15 +606,17 @@ namespace detail {
}

auto name() const -> std::string const& { return *m_name; }
auto set( std::string newName ) -> ParserResult {
auto description( std::string d ) -> void { *m_description = move(d); }
auto description() const -> std::string const& { return *m_description; }
auto set( std::string newName, bool updateRef = true ) -> ParserResult {

auto lastSlash = newName.find_last_of( "\\/" );
auto filename = ( lastSlash == std::string::npos )
? move( newName )
: newName.substr( lastSlash+1 );

*m_name = filename;
if( m_ref )
if( m_ref && updateRef )
return m_ref->setValue( filename );
else
return ParserResult::ok( ParseResultType::Matched );
Expand Down Expand Up @@ -774,6 +780,8 @@ namespace detail {
struct Parser : ParserBase {

mutable ExeName m_exeName;
bool m_isSubcmd = false;
std::vector<Parser> m_cmds;
std::vector<Opt> m_options;
std::vector<Arg> m_args;

Expand All @@ -795,6 +803,7 @@ namespace detail {
auto operator|=( Parser const &other ) -> Parser & {
m_options.insert(m_options.end(), other.m_options.begin(), other.m_options.end());
m_args.insert(m_args.end(), other.m_args.begin(), other.m_args.end());
m_cmds.insert(m_cmds.end(), other.m_cmds.begin(), other.m_cmds.end());
return *this;
}

Expand All @@ -815,6 +824,10 @@ namespace detail {
}

void writeToStream( std::ostream &os ) const {
// print banner
if( !m_exeName.description().empty() ) {
os << m_exeName.description() << std::endl << std::endl;
}
if (!m_exeName.name().empty()) {
os << "usage:\n" << " " << m_exeName.name() << " ";
bool required = true, first = true;
Expand All @@ -835,6 +848,11 @@ namespace detail {
os << "]";
if( !m_options.empty() )
os << " options";
if( !m_cmds.empty() ) {
if( !m_options.empty() )
os << " |";
os << " subcommand";
}
os << "\n";
}

Expand All @@ -859,6 +877,22 @@ namespace detail {

streamHelpColumns( getHelpColumns( m_args ), "\nwhere arguments are:" );
streamHelpColumns( getHelpColumns( m_options ), "\nwhere options are:" );

if( !m_cmds.empty() ) {
std::vector<HelpColumns> cmdCols;
{
cmdCols.reserve( m_cmds.size() );
for( auto const &cmd : m_cmds ) {
if( cmd.m_hidden )
continue;
const std::string &d = cmd.m_exeName.description();
// first line
cmdCols.push_back({ cmd.m_exeName.name(), d.substr(0, d.find('\n')) });
}
}

streamHelpColumns(cmdCols, "\nwhere subcommands are:");
}
}

friend auto operator<<( std::ostream &os, Parser const &parser ) -> std::ostream& {
Expand Down Expand Up @@ -894,18 +928,38 @@ namespace detail {
return Result::ok();
}

auto findCmd( const std::string& cmdName ) const -> Parser const* {
for( const Parser& cmd : m_cmds ) {
if( cmd.m_exeName.name() == cmdName )
return &cmd;
}
return nullptr;
}

using ParserBase::parse;

auto internalParse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult override {
if( !m_cmds.empty() && tokens ) {
std::string subCommand = tokens->token;
if( Parser const *cmd = findCmd( subCommand )) {
return cmd->parse( subCommand, tokens );
}
}

const std::size_t totalParsers = m_options.size() + m_args.size();
std::vector<ParserBase const*> parsers(totalParsers);
std::size_t i = 0;
for( auto const& opt : m_options ) parsers[i++] = &opt;
for( auto const& arg : m_args ) parsers[i++] = &arg;

m_exeName.set( exeName );
if( m_isSubcmd )
m_exeName.set( tokens->token );
else
m_exeName.set( exeName );

auto result = InternalParseResult::ok( ParseState( ParseResultType::NoMatch, tokens ) );
auto result = InternalParseResult::ok( m_isSubcmd ?
ParseState( ParseResultType::Matched, ++TokenStream( tokens ) ) :
ParseState( ParseResultType::NoMatch, tokens ) );
while( result.value().remainingTokens() ) {
bool tokenParsed = false;

Expand Down Expand Up @@ -935,12 +989,66 @@ namespace detail {
auto ComposableParserImpl<DerivedT>::operator|( T const &other ) const -> Parser {
return Parser() | static_cast<DerivedT const &>( *this ) | other;
}

struct Cmd : public Parser {
Cmd( std::string& ref, std::string cmdName ) : Parser() {
ExeName exe{ ref };
exe.set( move( cmdName ), false );
m_exeName = exe;
m_isSubcmd = true;
}

template<typename Lambda>
Cmd( Lambda const& ref, std::string cmdName ) : Parser() {
ExeName exe{ ref };
exe.set( move( cmdName ), false );
m_exeName = exe;
m_isSubcmd = true;
}

auto hidden() -> Cmd& {
m_hidden = true;
return *this;
};

auto operator()( std::string description ) -> Cmd& {
m_exeName.description( move( description ) );
return *this;
};
};

template<typename DerivedT>
auto operator,( ComposableParserImpl<DerivedT> const &l, Parser const &r ) -> Parser = delete;
template<typename DerivedT>
auto operator,( Parser const &l, ComposableParserImpl<DerivedT> const &r ) -> Parser = delete;
template<typename DerivedT, typename DerivedU>
auto operator,( ComposableParserImpl<DerivedT> const &l, ComposableParserImpl<DerivedU> const &r ) -> Parser = delete;

// concatenate parsers as subcommands;
// precondition: one or both must be subcommand parsers
inline auto operator,( Parser const &l, Parser const &r ) -> Parser {
assert( l.m_isSubcmd || r.m_isSubcmd );

Parser const *p1 = &l, *p2 = &r;
if ( p1->m_isSubcmd && !p2->m_isSubcmd ) {
std::swap( p1, p2 );
}

Parser p = p1->m_isSubcmd ? Parser{} : Parser{ *p1 };
if ( p1->m_isSubcmd )
p.m_cmds.push_back( *p1 );
p.m_cmds.push_back( *p2 );
return p;
}

} // namespace detail


// A Combined parser
using detail::Parser;

using detail::Cmd;

// A parser for options
using detail::Opt;

Expand Down
100 changes: 99 additions & 1 deletion src/ClaraTests.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "clara.hpp"
#include <clara.hpp>

#include "catch.hpp"

Expand Down Expand Up @@ -337,3 +337,101 @@ TEST_CASE( "Unrecognised opts" ) {
CHECK( !result );
CHECK_THAT( result.errorMessage(), Contains( "Unrecognised token") && Contains( "-b" ) );
}

TEST_CASE( "Subcommands" ) {
using namespace Catch::Matchers;

std::string subcommand, subArg;
bool showHelp = false, subOpt = false;

auto cli = (
// create a full parser
Parser{} | Help{ showHelp }
, Cmd{ subcommand, "subcommand" }( "Execute subcommand" )
| Arg{ subArg, "arg1" }( "Arg1" ).required()
| Opt{ subOpt }["--opt"]( "Opt" )
, Cmd{ subcommand, "internal" }( "Execute another subcommand" ).hidden()
);

REQUIRE( subcommand == "" );

SECTION( "subcommand.1" ) {
auto result = cli.parse( { "TestApp", "subcommand", "a1" } );
CHECK( result );
CHECK( result.value().type() == ParseResultType::Matched );
CHECK( !showHelp );
CHECK_THAT( subcommand, Equals( "subcommand" ) );
CHECK_THAT( subArg, Equals( "a1" ) );
CHECK( !subOpt);
}
SECTION( "subcommand.2" ) {
auto result = cli.parse( { "TestApp", "subcommand", "a1", "--opt" } );
CHECK( result );
CHECK( result.value().type() == ParseResultType::Matched );
CHECK( !showHelp );
CHECK_THAT( subcommand, Equals( "subcommand" ) );
CHECK_THAT( subArg, Equals( "a1" ) );
CHECK( subOpt );
}
SECTION( "hidden subcommand" ) {
auto result = cli.parse( { "TestApp", "internal" } );
CHECK( result );
CHECK( result.value().type() == ParseResultType::Matched );
CHECK( !showHelp );
CHECK_THAT( subcommand, Equals( "internal" ) );
CHECK_THAT( subArg, Equals( "" ) );
CHECK( !subOpt );
}
SECTION( "unmatched subcommand" ) {
auto result = cli.parse( { "TestApp", "xyz" } );
CHECK( !result );
CHECK_THAT( result.errorMessage(), Contains( "Unrecognised token" ) && Contains( "xyz" ) );
CHECK( !showHelp );
CHECK_THAT( subcommand, Equals( "" ) );
CHECK_THAT( subArg, Equals( "" ) );
CHECK( !subOpt );
}
SECTION( "app version" ) {
auto result = cli.parse( { "TestApp", "-h" } );
CHECK( result );
CHECK( showHelp );
CHECK_THAT( subcommand, Equals( "" ) );
CHECK_THAT( subArg, Equals( "" ) );
CHECK( !subOpt);
}
SECTION( "app usage" ) {
std::ostringstream oss;
oss << cli;
auto usage = oss.str();
REQUIRE(usage ==
R"(usage:
<executable> options | subcommand
where options are:
-?, -h, --help display usage information
where subcommands are:
subcommand Execute subcommand
)"
);
}
SECTION( "subcommand usage" ) {
std::cout << *cli.findCmd( "subcommand" );
std::ostringstream oss;
oss << *cli.findCmd( "subcommand" );
auto usage = oss.str();
REQUIRE(usage ==
R"(Execute subcommand
usage:
subcommand <arg1> options
where arguments are:
<arg1> Arg1
where options are:
--opt Opt
)"
);
}
}

0 comments on commit 42c8bad

Please sign in to comment.