Skip to content

Commit

Permalink
cmdline: Add a command-line parser and tests for it
Browse files Browse the repository at this point in the history
This parser accepts a command-line in a format similar to Linux's:
"foo bar=baz qux=12".
  • Loading branch information
qookei committed Nov 1, 2024
1 parent b362a57 commit 3062e69
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 0 deletions.
133 changes: 133 additions & 0 deletions include/frg/cmdline.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#ifndef FRG_CMDLINE_HPP
#define FRG_CMDLINE_HPP

#include <frg/macros.hpp>
#include <frg/string.hpp>
#include <frg/tuple.hpp>

#include <ranges>

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 <typename T>
auto as_number(T &here) {
return option::fn_type{
[] (frg::string_view value, void *ctx) {
auto n = value.to_number<T>();
if (n)
*static_cast<T *>(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<frg::string_view *>(ctx) = value;
},
&here,
true
};
}

template<typename T, T value>
auto store_const(T &here) {
return option::fn_type{
[] (frg::string_view, void *ctx) {
*static_cast<T *>(ctx) = value;
},
&here,
false
};
}

inline auto store_true(bool &here) {
return store_const<bool, true>(here);
}

inline auto store_false(bool &here) {
return store_const<bool, false>(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
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
49 changes: 49 additions & 0 deletions tests/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,52 @@ TEST(bitset, count) {
EXPECT_FALSE(a.all());
EXPECT_FALSE(a.none());
}

#include <frg/array.hpp>
#include <frg/cmdline.hpp>

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");
}

0 comments on commit 3062e69

Please sign in to comment.