From 3062e6945f8ec1d0abf2a779fb1c3dd13342847b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20S=C5=82omi=C5=84ski?= Date: Thu, 27 Apr 2023 00:09:20 +0200 Subject: [PATCH] cmdline: Add a command-line parser and tests for it This parser accepts a command-line in a format similar to Linux's: "foo bar=baz qux=12". --- include/frg/cmdline.hpp | 133 ++++++++++++++++++++++++++++++++++++++++ meson.build | 1 + tests/tests.cpp | 49 +++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 include/frg/cmdline.hpp diff --git a/include/frg/cmdline.hpp b/include/frg/cmdline.hpp new file mode 100644 index 0000000..22cff6a --- /dev/null +++ b/include/frg/cmdline.hpp @@ -0,0 +1,133 @@ +#ifndef FRG_CMDLINE_HPP +#define FRG_CMDLINE_HPP + +#include +#include +#include + +#include + +namespace frg FRG_VISIBILITY { + +struct option { + struct fn_type { + void (*ptr)(frg::string_view, void *); + void *ctx; + bool has_arg; + }; + + frg::string_view opt; + fn_type fn; + + void apply(frg::string_view value) { + FRG_ASSERT(fn.ptr); + fn.ptr(value, fn.ctx); + } +}; + +template +auto as_number(T &here) { + return option::fn_type{ + [] (frg::string_view value, void *ctx) { + auto n = value.to_number(); + if (n) + *static_cast(ctx) = n.value(); + }, + &here, + true + }; +} + +inline auto as_string_view(frg::string_view &here) { + return option::fn_type{ + [] (frg::string_view value, void *ctx) { + *static_cast(ctx) = value; + }, + &here, + true + }; +} + +template +auto store_const(T &here) { + return option::fn_type{ + [] (frg::string_view, void *ctx) { + *static_cast(ctx) = value; + }, + &here, + false + }; +} + +inline auto store_true(bool &here) { + return store_const(here); +} + +inline auto store_false(bool &here) { + return store_const(here); +} + + +inline void parse_arguments(frg::string_view cmdline, std::ranges::range auto args) { + auto try_apply_arg = [&] (frg::string_view arg, option opt) -> bool { + auto eq = arg.find_first('='); + + if (eq == size_t(-1)) { + if (opt.fn.has_arg) + return false; + + if (opt.opt != arg) + return false; + + opt.apply({}); + } else { + if (!opt.fn.has_arg) + return false; + + auto name = arg.sub_string(0, eq); + auto val = arg.sub_string(eq + 1, arg.size() - eq -1); + + if (opt.opt != name) + return false; + + opt.apply(val); + } + + return true; + }; + + while (true) { + size_t spc = cmdline.find_first(' '); + size_t opening_quote = cmdline.find_first('\"'); + size_t closing_quote = size_t(-1); + bool quoted = false; + + if(opening_quote < spc) { + quoted = true; + + closing_quote = opening_quote + 1 + cmdline.sub_string(opening_quote + 1, cmdline.size() - opening_quote - 1).find_first('\"'); + spc = closing_quote + 1 + cmdline.sub_string(closing_quote + 1, cmdline.size() - closing_quote - 1).find_first(' '); + } + + size_t split_on = spc; + if (spc == size_t(-1)) + split_on = cmdline.size(); + + auto arg = cmdline.sub_string(quoted ? (opening_quote + 1) : 0, quoted ? (closing_quote - opening_quote - 1) : split_on); + + for (const option &opt : args) { + if (try_apply_arg(arg, opt)) + break; + } + + if (spc != size_t(-1)) { + cmdline = cmdline.sub_string(spc + 1, cmdline.size() - spc - 1); + } else { + break; + } + } +} + +} // namespace frg + +#endif // FRG_CMDLINE_HPP diff --git a/meson.build b/meson.build index 9f6df34..7b1ac93 100644 --- a/meson.build +++ b/meson.build @@ -8,6 +8,7 @@ if not get_option('frigg_no_install') 'include/frg/algorithm.hpp', 'include/frg/allocation.hpp', 'include/frg/array.hpp', + 'include/frg/cmdline.hpp', 'include/frg/container_of.hpp', 'include/frg/detection.hpp', 'include/frg/dyn_array.hpp', diff --git a/tests/tests.cpp b/tests/tests.cpp index 761fe54..73ea24f 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -525,3 +525,52 @@ TEST(bitset, count) { EXPECT_FALSE(a.all()); EXPECT_FALSE(a.none()); } + +#include +#include + +TEST(cmdline, basic_cmdline) { + bool foo = false, bar = false; + frg::string_view v1{}; + uint32_t v2 = 0; + frg::string_view v3{}; + frg::string_view v4{}; + + frg::array args = { + frg::option{"foo", frg::store_true(foo)}, + frg::option{"bar", frg::store_true(bar)}, + frg::option{"baz", frg::as_string_view(v1)}, + frg::option{"qux", frg::as_number(v2)}, + frg::option{"path1", frg::as_string_view(v3)}, + frg::option{"path2", frg::as_string_view(v4)}, + }; + + frg::parse_arguments("\"path1=a space/nospace\" foo baz=yoo qux=1234 \"path2=/a/b c/d\"", args); + + ASSERT_TRUE(foo); + ASSERT_FALSE(bar); + ASSERT_EQ(v1, "yoo"); + ASSERT_EQ(v2, 1234); + ASSERT_EQ(v3, "a space/nospace"); + ASSERT_EQ(v4, "/a/b c/d"); +} + +TEST(cmdline, multiple_option_spans) { + bool nosmp = false; + frg::string_view init_exec{}; + + frg::array cpu_args = { + frg::option{"x86.nosmp", frg::store_true(nosmp)} + }; + + frg::array init_args = { + frg::option{"init.exec", frg::as_string_view(init_exec)} + }; + + frg::array combined = { init_args, cpu_args }; + + frg::parse_arguments("x86.nosmp init.exec=/sbin/posix-subsystem", std::views::join(combined)); + + ASSERT_TRUE(nosmp); + ASSERT_EQ(init_exec, "/sbin/posix-subsystem"); +}