cmdopt2: initial sketches for a command line parser
This commit is contained in:
parent
ea24909893
commit
e96ee81c03
@ -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
98
cmdopt2/arg.cpp
Normal 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
100
cmdopt2/arg.hpp
Normal 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
18
cmdopt2/fwd.hpp
Normal 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
210
cmdopt2/parser.cpp
Normal 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
35
cmdopt2/parser.hpp
Normal 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
191
test/cmdopt2.cpp
Normal 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 ();
|
||||
}
|
Loading…
Reference in New Issue
Block a user