diff --git a/CMakeLists.txt b/CMakeLists.txt index e5af6fc5..6499816d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/cmdopt2/arg.cpp b/cmdopt2/arg.cpp new file mode 100644 index 00000000..f8769207 --- /dev/null +++ b/cmdopt2/arg.cpp @@ -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; +} \ No newline at end of file diff --git a/cmdopt2/arg.hpp b/cmdopt2/arg.hpp new file mode 100644 index 00000000..12ec245f --- /dev/null +++ b/cmdopt2/arg.hpp @@ -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 + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + + +namespace cruft::cmdopt2 { + struct argument { + std::string name; + std::optional description; + bool required = false; + + using acceptor1_t = std::function; + std::optional acceptor1; + }; + + + template + struct ops : argument { + template + BaseT bind (ValueT&&) = delete; + + template + BaseT + bind (ValueT &ref) + { + CHECK (!acceptor1); + if constexpr (std::is_same_v) { + acceptor1 = [&ref] (std::string_view str) { ref = str; }; + } else { + acceptor1 = [&ref] (std::string_view str) { ref = parse::from_string (str); }; + } + return get (); + } + + template + BaseT + bind (std::optional &ref) + { + CHECK (!acceptor1); + + if constexpr (std::is_same_v) { + acceptor1 = [&ref] (std::string_view str) { ref = str; }; + } else { + acceptor1 = [&ref] (std::string_view str) { ref = parse::from_string (str); }; + } + + return get (); + } + + BaseT + get (void) const + { + return reinterpret_cast (*this); + } + }; + + + struct positional : public ops { + 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 { + using acceptor0_t = std::function; + std::optional 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 short_; + std::optional long_; + }; +} diff --git a/cmdopt2/fwd.hpp b/cmdopt2/fwd.hpp new file mode 100644 index 00000000..8421e9ef --- /dev/null +++ b/cmdopt2/fwd.hpp @@ -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 + */ + +#pragma once + + +namespace cruft::cmdopt2 { + struct argument; + struct positional; + struct keyword; + + class parser; +} diff --git a/cmdopt2/parser.cpp b/cmdopt2/parser.cpp new file mode 100644 index 00000000..31a34a54 --- /dev/null +++ b/cmdopt2/parser.cpp @@ -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'; +} diff --git a/cmdopt2/parser.hpp b/cmdopt2/parser.hpp new file mode 100644 index 00000000..8f46dd35 --- /dev/null +++ b/cmdopt2/parser.hpp @@ -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 + */ + +#pragma once + +#include "./arg.hpp" + +#include +#include + + +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 m_positional; + std::vector m_keyword; + }; +} diff --git a/test/cmdopt2.cpp b/test/cmdopt2.cpp new file mode 100644 index 00000000..aff0804a --- /dev/null +++ b/test/cmdopt2.cpp @@ -0,0 +1,191 @@ +#include +#include +#include + +#include + + +/////////////////////////////////////////////////////////////////////////////// +static void +test_combinations (cruft::TAP::logger &tap) +{ + static const struct { + std::vector args; + int foo; + std::optional 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 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 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 (); +} \ No newline at end of file