cmdopt2: initial sketches for a command line parser

This commit is contained in:
Danny Robson 2022-03-16 13:20:26 +10:00
parent ea24909893
commit e96ee81c03
7 changed files with 658 additions and 0 deletions

View File

@ -285,6 +285,11 @@ list (
cast.hpp
cmdopt.cpp
cmdopt.hpp
cmdopt2/fwd.hpp
cmdopt2/arg.cpp
cmdopt2/arg.hpp
cmdopt2/parser.cpp
cmdopt2/parser.hpp
colour.cpp
colour.hpp
concepts.hpp
@ -700,6 +705,7 @@ if (TESTS)
bitwise
buffer/simple
cmdopt
cmdopt2
colour
concepts
comparator

98
cmdopt2/arg.cpp Normal file
View File

@ -0,0 +1,98 @@
#include "./arg.hpp"
using cruft::cmdopt2::positional;
using cruft::cmdopt2::keyword;
///////////////////////////////////////////////////////////////////////////////
positional
positional::create (char const *name)
{
return create (std::string (name));
}
//-----------------------------------------------------------------------------
positional
positional::create (std::string &&name)
{
positional res {};
res.name = name;
return res;
}
///////////////////////////////////////////////////////////////////////////////
keyword
keyword::create (char const *name)
{
return create (std::string (name));
}
//-----------------------------------------------------------------------------
keyword
keyword::create (std::string &&name)
{
keyword res {};
res.name = name;
res.long_ = name;
return res;
}
//-----------------------------------------------------------------------------
keyword
keyword::flag (void) const
{
keyword res = *this;
res.long_.reset ();
res.short_.reset ();
return res;
}
//-----------------------------------------------------------------------------
keyword
keyword::flag (char val) const
{
keyword res = *this;
res.short_ = val;
return res;
}
//-----------------------------------------------------------------------------
keyword
keyword::flag (std::string_view val) const
{
keyword res = *this;
res.long_ = val;
return res;
}
//-----------------------------------------------------------------------------
keyword
keyword::count (int &val) const
{
CHECK (!acceptor1 and !acceptor0);
keyword res = *this;
res.acceptor0 = [&val] (void) { ++val; };
return res;
}
//-----------------------------------------------------------------------------
keyword
keyword::present (bool &val) const
{
CHECK (!acceptor1 and !acceptor0);
val = false;
keyword res = *this;
res.acceptor0 = [&val] (void) { val = true; };
return res;
}

100
cmdopt2/arg.hpp Normal file
View File

@ -0,0 +1,100 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright 2022, Danny Robson <danny@nerdcruft.net>
*/
#pragma once
#include <cruft/util/debug/assert.hpp>
#include <cruft/util/parse/value.hpp>
#include <cruft/util/cast.hpp>
#include <optional>
#include <string>
#include <functional>
namespace cruft::cmdopt2 {
struct argument {
std::string name;
std::optional<std::string> description;
bool required = false;
using acceptor1_t = std::function<void(std::string_view)>;
std::optional<acceptor1_t> acceptor1;
};
template <typename BaseT>
struct ops : argument {
template <typename ValueT>
BaseT bind (ValueT&&) = delete;
template <typename ValueT>
BaseT
bind (ValueT &ref)
{
CHECK (!acceptor1);
if constexpr (std::is_same_v<ValueT, std::string>) {
acceptor1 = [&ref] (std::string_view str) { ref = str; };
} else {
acceptor1 = [&ref] (std::string_view str) { ref = parse::from_string<ValueT> (str); };
}
return get ();
}
template <typename ValueT>
BaseT
bind (std::optional<ValueT> &ref)
{
CHECK (!acceptor1);
if constexpr (std::is_same_v<ValueT, std::string>) {
acceptor1 = [&ref] (std::string_view str) { ref = str; };
} else {
acceptor1 = [&ref] (std::string_view str) { ref = parse::from_string<ValueT> (str); };
}
return get ();
}
BaseT
get (void) const
{
return reinterpret_cast<BaseT const&> (*this);
}
};
struct positional : public ops<positional> {
static positional create (char const *name);
static positional create (std::string_view name);
static positional create (std::string const &name);
static positional create (std::string &&name);
int count = 1;
};
struct keyword : public ops<keyword> {
using acceptor0_t = std::function<void(void)>;
std::optional<acceptor0_t> acceptor0;
static keyword create (char const *name);
static keyword create (std::string_view name);
static keyword create (std::string const &name);
static keyword create (std::string &&name);
keyword flag (void) const;
keyword flag (std::string_view long_) const;
keyword flag (char short_) const;
keyword count (int &) const;
keyword present (bool &) const;
std::optional<char> short_;
std::optional<std::string> long_;
};
}

18
cmdopt2/fwd.hpp Normal file
View File

@ -0,0 +1,18 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright 2022, Danny Robson <danny@nerdcruft.net>
*/
#pragma once
namespace cruft::cmdopt2 {
struct argument;
struct positional;
struct keyword;
class parser;
}

210
cmdopt2/parser.cpp Normal file
View File

@ -0,0 +1,210 @@
#include "./parser.hpp"
using cruft::cmdopt2::parser;
///////////////////////////////////////////////////////////////////////////////
cruft::cmdopt2::positional&
parser::add (positional const &arg)&
{
return m_positional.emplace_back (std::move (arg));
}
//-----------------------------------------------------------------------------
cruft::cmdopt2::keyword&
parser::add (keyword const &arg)&
{
return m_keyword.emplace_back (std::move (arg));
}
///////////////////////////////////////////////////////////////////////////////
int
parser::parse_named (int argc, const char *const *argv) const
{
auto cursor = argv[0];
if (!cursor or !*cursor)
return 0;
if (*cursor++ != '-')
return 0;
if (!*cursor)
throw std::runtime_error ("missing argument name");
if (*cursor == '-') {
++cursor;
if (!*cursor)
throw std::runtime_error ("missing long argument name");
return parse_long (argc, argv);
} else {
return parse_short (argc, argv);
}
unreachable ();
}
//-----------------------------------------------------------------------------
int
parser::parse_long (int argc, const char *const *argv) const
{
CHECK (argc >= 1);
CHECK (strlen (argv[0]) >= 2);
CHECK (argv[0][0] == '-');
CHECK (argv[0][1] == '-');
auto const eq = strchr (argv[0], '=');
std::string_view const key (argv[0] + 2, eq ?: strlen (argv[0]) + argv[0]);
auto const pos = std::find_if (
m_keyword.begin (),
m_keyword.end (),
[&] (auto const &arg)
{
return arg.long_ and *arg.long_ == key;
});
if (pos == m_keyword.end ())
throw std::runtime_error ("Unknown long argument");
if (eq) {
(*pos->acceptor1) (eq + 1);
return 1;
} else {
if (pos->acceptor0) {
CHECK (!pos->acceptor1);
(*pos->acceptor0) ();
return 1;
}
CHECK (pos->acceptor1);
if (argc < 2)
throw std::runtime_error ("Missing long arg value");
if (not argv[1] or not argv[1][0] or argv[1][0] == '-')
throw std::runtime_error ("Missing long arg value");
(*pos->acceptor1) (argv[1]);
return 2;
}
unreachable ();
}
//-----------------------------------------------------------------------------
int
parser::parse_short (int argc, const char *const *argv) const
{
(void)argc;
CHECK (argc >= 1);
CHECK (strlen (argv[0]) >= 2);
CHECK (argv[0][0] == '-');
CHECK (argv[0][1] != '-');
auto const len = strlen (argv[0]);
if (len < 1)
throw std::runtime_error ("Missing short arguments");
// Handle single short args with a potential for an associated value
if (len == 2) {
auto const pos = std::find_if (
m_keyword.begin (),
m_keyword.end (),
[&] (auto const &arg)
{
return arg.short_ and *arg.short_ == argv[0][1];
});
if (pos == m_keyword.end ())
throw std::runtime_error ("Unknown short argument");
if (pos->acceptor0) {
CHECK (!pos->acceptor1);
(*pos->acceptor0) ();
return 1;
}
CHECK (!pos->acceptor0);
if (argc < 2)
throw std::runtime_error ("Missing short argument value");
(*pos->acceptor1) (argv[1]);
return 2;
}
// Handle strings of short arguments, eg: "-vvv"
for (std::size_t i = 1; i < len; ++i) {
auto const pos = std::find_if (
m_keyword.begin (),
m_keyword.end (),
[&] (auto const &arg)
{
return arg.short_ and *arg.short_ == argv[0][1];
});
if (pos == m_keyword.end ())
throw std::runtime_error ("Unknown short argument");
if (pos->acceptor1)
throw std::runtime_error ("Missing short argument value");
(*pos->acceptor0) ();
}
return 1;
}
//-----------------------------------------------------------------------------
int
parser::parse (int const argc, const char *const *argv)
{
if (argc <= 1)
return argc;
int arg_cursor = 1;
int pos_cursor = 0;
while (arg_cursor != argc) {
auto const arg = argv[arg_cursor];
if (!arg or !*arg)
break;
if (*arg == '-') {
arg_cursor += parse_named (argc - arg_cursor, argv + arg_cursor);
continue;
}
(*m_positional[pos_cursor].acceptor1) (argv[arg_cursor++]);
}
return arg_cursor;
}
///////////////////////////////////////////////////////////////////////////////
void
parser::usage (int argc, char const * const* argv, std::ostream &os) const
{
os << "Usage:";
if (argc > 0)
os << ' ' << argv[0];
static char constexpr OPTIONAL[2] { '[', ']' };
static char constexpr REQUIRED[2] { '<', '>' };
for (auto const &arg: m_keyword) {
auto const &delimiters = arg.required ? REQUIRED : OPTIONAL;
if (arg.short_)
os << ' ' << delimiters[0] << '-' << *arg.short_ << delimiters[1];
if (arg.long_)
os << ' ' << delimiters[0] << "--" << *arg.long_ << delimiters[1];
}
for (auto const &arg: m_positional) {
auto const &delimiters = arg.required ? REQUIRED : OPTIONAL;
os << ' ' << delimiters[0] << arg.name << delimiters[1];
}
os << '\n';
}

35
cmdopt2/parser.hpp Normal file
View File

@ -0,0 +1,35 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright 2022, Danny Robson <danny@nerdcruft.net>
*/
#pragma once
#include "./arg.hpp"
#include <iosfwd>
#include <vector>
namespace cruft::cmdopt2 {
class parser {
public:
int parse (int argc, char const* const* argv);
positional& add (positional const&) &;
keyword& add (keyword const&) &;
void usage (int argc, char const * const* argv, std::ostream&) const;
private:
int parse_named (int argc, char const* const* argv) const;
int parse_short (int argc, char const* const* argv) const;
int parse_long (int argc, char const* const* argv) const;
std::vector<positional> m_positional;
std::vector<keyword> m_keyword;
};
}

191
test/cmdopt2.cpp Normal file
View File

@ -0,0 +1,191 @@
#include <cruft/util/tap.hpp>
#include <cruft/util/cmdopt2/parser.hpp>
#include <cruft/util/cmdopt2/arg.hpp>
#include <iostream>
///////////////////////////////////////////////////////////////////////////////
static void
test_combinations (cruft::TAP::logger &tap)
{
static const struct {
std::vector<char const*> args;
int foo;
std::optional<float> bar;
std::string qux;
bool verbose;
} TESTS[] = {
{
.args = { "cmd", "-f", "1", "--bar", "2", "val" },
.foo = 1,
.bar = 2,
.qux = "val",
.verbose = false,
},
{
.args = { "cmd", "--bar", "2", "-f", "1", "val" },
.foo = 1,
.bar = 2,
.qux = "val",
.verbose = false,
},
{
.args = { "cmd", "--bar", "2", "val", "-f", "1", },
.foo = 1,
.bar = 2,
.qux = "val",
.verbose = false,
},
{
.args = { "cmd", "-v", "--bar", "2", "val", "-f", "1", },
.foo = 1,
.bar = 2,
.qux = "val",
.verbose = true,
},
{
.args = { "cmd", "--bar", "2", "val", "-f", "1", },
.foo = 1,
.bar = 2,
.qux = "val",
.verbose = false,
},
{
.args = { "cmd", "val", "--bar", "2", "-f", "1", },
.foo = 1,
.bar = 2,
.qux = "val",
.verbose = false,
},
{
.args = { "cmd", "val", "-v", "-f", "1", },
.foo = 1,
.bar = {},
.qux = "val",
.verbose = true,
},
{
.args = { "cmd", "val", "-f", "1", },
.foo = 1,
.bar = {},
.qux = "val",
.verbose = true,
},
};
int foo;
std::optional<float> bar;
std::string qux;
bool verbose;
using namespace cruft::cmdopt2;
parser p;
p.add (keyword::create ("foo").flag ().flag ('f').bind (foo));
p.add (keyword::create ("bar").bind (bar));
p.add (positional::create ("qux").bind (qux));
p.add (keyword::create ("verbose").flag ().flag ('v').present (verbose));
for (auto const &t: TESTS) {
foo = decltype(foo) {};
bar = decltype(bar) {};
qux = decltype(qux) {};
verbose = false;
p.parse (int (t.args.size ()), t.args.data ());
tap.expect (
foo == t.foo and bar == t.bar and qux == t.qux,
"{}", fmt::join (t.args.begin (), t.args.end (), " ")
);
}
}
///////////////////////////////////////////////////////////////////////////////
static void test_presence (cruft::TAP::logger &tap)
{
static const struct {
std::vector<char const*> args;
int count;
bool present;
} TESTS[] = {
{
.args = { "cmd", "-v", },
.count = 1,
.present = false,
},
{
.args = { "cmd", "-vvv", },
.count = 3,
.present = false,
},
{
.args = { "cmd", "-v", "-vv"},
.count = 3,
.present = false,
},
{
.args = { "cmd", },
.count = 0,
.present = false,
},
{
.args = { "cmd", "-p"},
.count = 0,
.present = true,
},
{
.args = { "cmd", "-ppp"},
.count = 0,
.present = true,
},
{
.args = { "cmd", "-p", "-p"},
.count = 0,
.present = true,
},
{
.args = { "cmd", "-p", "-v", "-p"},
.count = 1,
.present = true,
},
{
.args = { "cmd", "-p", "-v", "-p", "-v"},
.count = 2,
.present = true,
},
};
int count;
bool present;
using namespace cruft::cmdopt2;
parser p;
p.add (keyword::create ("verbose").flag ().flag ('v').count (count));
p.add (keyword::create ("present").flag ().flag ('p').present (present));
for (auto const &t: TESTS) {
count = 0;
present = false;
p.parse (int (t.args.size ()), t.args.data ());
tap.expect (
count == t.count and present == t.present,
"{}", fmt::join (t.args.begin (), t.args.end (), " ")
);
}
}
///////////////////////////////////////////////////////////////////////////////
int main ()
{
cruft::TAP::logger tap;
test_combinations (tap);
test_presence (tap);
return tap.status ();
}