diff --git a/best/cli/BUILD b/best/cli/BUILD index 5367fe1..b3e8eec 100644 --- a/best/cli/BUILD +++ b/best/cli/BUILD @@ -52,6 +52,25 @@ cc_test( deps = [ ":cli", ":parser", + ":toy_flags", "//best/test", ] +) + +cc_library( + name = "toy_flags", + hdrs = ["toy_flags.h"], + deps = [ + ":cli", + ], + visibility = ["//visibility:private"], +) + +cc_binary( + name = "toy", + srcs = ["toy.cc"], + deps = [ + ":app", + ":toy_flags", + ] ) \ No newline at end of file diff --git a/best/cli/cli.h b/best/cli/cli.h index 13a8ffc..407d307 100644 --- a/best/cli/cli.h +++ b/best/cli/cli.h @@ -98,8 +98,9 @@ class cli final { struct alias final { /// Same restrictions as the `name` field on the corresponding tag. best::str name; - /// The visibility for this alias. - visibility vis = Public; + /// The visibility for this alias. If not specified, uses the visibility + /// of the tag it is attached to. + best::option vis; }; /// # `cli::flag` diff --git a/best/cli/cli_test.cc b/best/cli/cli_test.cc index 4703152..4733a36 100644 --- a/best/cli/cli_test.cc +++ b/best/cli/cli_test.cc @@ -22,136 +22,11 @@ #include "best/cli/cli.h" #include "best/cli/parser.h" +#include "best/cli/toy_flags.h" #include "best/test/test.h" namespace best::flag_parser_test { - -struct Subcommand { - int sub_flag; - best::str arg; - - friend constexpr auto BestReflect(auto& m, Subcommand*) { - using ::best::cli; - return m.infer()->*m.field(best::vals<&Subcommand::sub_flag>, - cli::flag{ - .letter = 's', - .arg = "INT", - .help = "a subcommand argument", - }); - } - - bool operator==(const Subcommand&) const = default; -}; - -struct Subgroup { - int eks = 0, why = 0, zed = 0; - - friend constexpr auto BestReflect(auto& m, Subgroup*) { - using ::best::cli; - return m.infer() - ->*m.field(best::vals<&Subgroup::eks>, cli::flag{ - .letter = 'x', - .arg = "INT", - .help = "a group integer", - }) - ->*m.field(best::vals<&Subgroup::why>, cli::flag{ - .letter = 'y', - .arg = "INT", - .help = "another group integer", - }) - ->*m.field(best::vals<&Subgroup::zed>, cli::flag{ - .letter = 'z', - .arg = "INT", - .help = "a third group integer", - }); - } - - bool operator==(const Subgroup&) const = default; -}; - -struct MyFlags { - int foo = 0; - best::vec bar; - best::option baz; - best::str name, addr; - - bool flag1 = false; - bool flag2 = false; - bool flag3 = false; - bool flag4 = false; - - Subcommand sub; - Subcommand sub2; - - Subgroup group; - Subgroup flattened; - - best::str arg; - best::vec args; - - friend constexpr auto BestReflect(auto& m, MyFlags*) { - using ::best::cli; - return m.infer() - ->*m.field(best::vals<&MyFlags::foo>, cli::flag{ - .letter = 'f', - .arg = "INT", - .count = cli::Required, - .help = "a required integer", - }) - ->*m.field(best::vals<&MyFlags::bar>, cli::flag{ - .arg = "INT", - .help = "repeated integer", - }) - ->*m.field(best::vals<&MyFlags::baz>, cli::flag{ - .help = "an optional integer", - }) - - ->*m.field(best::vals<&MyFlags::name>, cli::flag{ - .vis = cli::Hidden, - .help = "your name", - }, cli::alias{"my-name"}) - ->*m.field(best::vals<&MyFlags::addr>, cli::flag{ - .vis = cli::Hidden, - .help = "your address", - }, cli::alias{"my-address"}) - - ->*m.field(best::vals<&MyFlags::flag1>, cli::flag{ - .letter = 'a', - .help = "this is a flag\nnewline", - }) - ->*m.field(best::vals<&MyFlags::flag2>, cli::flag{ - .letter = 'b', - .help = "this is a flag\nnewline", - }) - ->*m.field(best::vals<&MyFlags::flag3>, cli::flag{ - .letter = 'c', - .help = "this is a flag\nnewline", - }, cli::alias{"flag3-alias"}, - cli::alias{"flag3-alias2", cli::Hidden}) - ->*m.field(best::vals<&MyFlags::flag4>, cli::flag{ - .letter = 'd', - .help = "this is a flag\nnewline", - }) - - ->*m.field(best::vals<&MyFlags::sub>, cli::subcommand{ - .help = "a subcommand", - .about = "longer help for the subcommand\nwith multiple lines", - }) - ->*m.field(best::vals<&MyFlags::sub2>, cli::subcommand{ - .help = "identical in all ways to `sub`\nexceptt for this help", - .about = "longer help for the subcommand\nwith multiple lines", - }, cli::alias{"sub3"}) - - ->*m.field(best::vals<&MyFlags::group>, cli::group{ - .name = "subgroup", - .letter = 'X', - .help = "extra options behind the -X flag", - }) - ->*m.field(best::vals<&MyFlags::flattened>, cli::group{}, cli::alias{"sub3"}); - } - - bool operator==(const MyFlags&) const = default; -}; +using ::best::cli_toy::MyFlags; best::test Flags = [](auto& t) { best::parse_flags("test", {"--help"}).err()->print_and_exit(); diff --git a/best/cli/parser.cc b/best/cli/parser.cc index a475c6f..37c85b0 100644 --- a/best/cli/parser.cc +++ b/best/cli/parser.cc @@ -173,7 +173,9 @@ void cli::init() { // into the lookup tables. for (auto [idx, f] : impl_->flags.iter().enumerate()) { - for (auto& [name, vis] : f.about.names) { + auto has_letter = f.tag->letter != '\0'; + for (auto [name_idx, pair] : f.about.names.iter().enumerate()) { + auto& [name, vis] = pair; if (vis == Delete) continue; normalize(name, f.about); @@ -182,12 +184,11 @@ void cli::init() { f.about.strukt->path(), f.about.field, name); } - bool is_letter = idx == 0 && f.tag->letter != '\0'; impl_->sorted_flags.push(impl::entry{ .key = name, .idx = idx, - .is_letter = is_letter, - .is_alias = idx > size_t(is_letter), + .is_letter = name_idx == 0 && has_letter, + .is_alias = name_idx > size_t(has_letter), .vis = vis, }); } @@ -236,30 +237,35 @@ void cli::init() { update(g.about); } - for (auto& [name, vis] : g->about.names) { + auto has_letter = g->tag->letter != '\0'; + for (auto [name_idx, pair] : g->about.names.iter().enumerate()) { + auto& [name, vis] = pair; if (vis == Delete) continue; - normalize(name, g->about); - if (name == "help" || name == "help-hidden" || name == "version") { - best::wtf("field {}::{}'s name ({:?}) is reserved and may not be used", - g->about.strukt->path(), g->about.field, name); - } - - bool is_letter = idx == 0 && g->tag->letter != '\0'; - impl_->sorted_flags.push(impl::entry{ - .key = name, - .idx = idx, - .is_group = true, - .is_letter = is_letter, - .is_alias = idx > size_t(is_letter), - }); + bool is_flatten = !has_letter && name.is_empty(); + if (!is_flatten) { + normalize(name, g->about); + if (name == "help" || name == "help-hidden" || name == "version") { + best::wtf( + "field {}::{}'s name ({:?}) is reserved and may not be used", + g->about.strukt->path(), g->about.field, name); + } - // Letter names for groups are parsed in a different way that does not - // require generating keys for them other than the one above. - if (is_letter) continue; + impl_->sorted_flags.push(impl::entry{ + .key = name, + .idx = idx, + .is_group = true, + .is_letter = name_idx == 0 && has_letter, + .is_alias = name_idx > size_t(has_letter), + }); + + // Letter names for groups are parsed in a different way that does not + // require generating keys for them other than the one above. + if (name_idx == 0 && has_letter) continue; + } for (auto entry : g->child->impl_->sorted_flags) { - if (entry.is_letter) continue; + if (!is_flatten && entry.is_letter) continue; if (!name.is_empty()) { entry.key = best::format("{}.{}", name, entry.key); } @@ -360,6 +366,41 @@ best::result cli::parse( arg = split->second(); } + auto push_group = [&](size_t idx, bool update_arg = + true) -> best::result { + // We need to parse a flag from the next argument, of the form e.g. + // -C opt-level=3. + + if (update_arg) { + // This type of flag cannot use a = argument. + if (arg) { + return best::err( + best::format("{0}: fatal: unexpected argument after {1}", + ctx.exe, *next), + /*is_fatal=*/true); + } + + auto next_arg = iter.next(); + if (!next_arg) { + return best::err( + best::format("{0}: fatal: expected sub-flag after {1}", ctx.exe, + *next), + /*is_fatal=*/true); + } + + // Update flag and arg. + flag = *next_arg; + if (auto split = flag.split_once("=")) { + flag = split->first(); + arg = split->second(); + } + } + + // And nest. + ctx.cur = ctx.sub->impl_->groups[idx].child; + return best::ok(); + }; + if (is_letter) { // This may be a run of short flags, like -xvzf file, or a single short // flag group, like -Copt-level. To discover this, we need to peel off @@ -378,8 +419,11 @@ best::result cli::parse( if (!e || !e->is_letter) break; if (e->is_group) { // This is a nesting of the form -Copt-level. Unlike the case below, - // we do not need to update flag/arg. - ctx.cur = ctx.sub->impl_->groups[e->idx].child; + // we do not need to update flag/arg if there remain runes to be + // consumed. + BEST_GUARD(push_group(e->idx, runes->rest().is_empty())); + flag = runes->rest(); + continue; } const auto& f = ctx.cur->impl_->flags[e->idx]; @@ -423,34 +467,7 @@ best::result cli::parse( if (auto e = ctx.cur->impl_->find_flag(flag)) { if (e->is_group) { - // We need to parse a flag from the next argument, of the form e.g. - // -C opt-level=3. - - // This type of flag cannot use a = argument. - if (arg) { - return best::err( - best::format("{0}: fatal: unexpected argument after {1}", - ctx.exe, *next), - /*is_fatal=*/true); - } - - auto next = iter.next(); - if (!next) { - return best::err( - best::format("{0}: fatal: expected sub-flag after {1}", - ctx.exe, *next), - /*is_fatal=*/true); - } - - // Update flag and arg. - flag = *next; - if (auto split = flag.split_once("=")) { - flag = split->first(); - arg = split->second(); - } - - // And nest. - ctx.cur = ctx.sub->impl_->groups[e->idx].child; + BEST_GUARD(push_group(e->idx)); continue; } @@ -486,7 +503,7 @@ best::result cli::parse( best::format("{0}: fatal: unknown flag {1:?}\n" "{0}: you can use `--` if you meant to pass " "this as a positional argument", - ctx.exe, next), + ctx.exe, *next), /*is_fatal=*/true); } } @@ -566,7 +583,7 @@ best::strbuf cli::usage(best::pretext exe, bool hidden) const { // Append all of the letters. bool needs_dash = true; for (const auto& e : impl_->sorted_flags) { - if (!e.is_letter) continue; + if (!e.is_letter || !visible(e.vis, hidden)) continue; if (std::exchange(needs_dash, false)) { out.push(" -"); } @@ -627,27 +644,25 @@ best::strbuf cli::usage(best::pretext exe, bool hidden) const { // This is how wide the column for a flag to be in-line with its // help is. - constexpr size_t Width = 32; + constexpr size_t Width = 28; // Next, print all of the subcommands. bool first = true; for (const auto& e : impl_->sorted_subs) { if (!visible(e.vis, hidden) || e.is_alias) continue; - if (!std::exchange(first, false)) { + if (std::exchange(first, false)) { out.push("# Subcommands\n"); } - out.push(" "); + indent(6); out.push(e.key); - size_t extra = Width - width_of(e.key) - 2; + size_t extra = Width - width_of(e.key) - 6; if (extra <= Width) { - indent(extra); - out.push(" "); + indent(extra + 2); } else { out.push("\n"); - indent(Width); - out.push(" "); + indent(Width + 2); } bool first = true; @@ -687,7 +702,7 @@ best::strbuf cli::usage(best::pretext exe, bool hidden) const { } } - if (has_letter != '\0' && !e.is_letter) continue; + if (has_letter && !e.is_letter) continue; bool is_visible = false; for (auto& [name, vis] : about->names) { diff --git a/best/cli/parser.h b/best/cli/parser.h index b804063..77ef564 100644 --- a/best/cli/parser.h +++ b/best/cli/parser.h @@ -149,15 +149,14 @@ void cli::type_erase_field(auto f) { about.names.push(best::format("{}", group.letter), group.vis); } - best::str name = group.name; - if (name.is_empty()) name = about.field; - about.names.push(best::strbuf(name), group.vis); + about.names.push(best::strbuf(group.name), group.vis); } aliases.each([&](auto alias) { static_assert(sizeof(alias) == 0 || poses.is_empty(), "cannot apply cli::alias to a positional"); - about.names.push(best::strbuf(alias.name), alias.vis); + about.names.push(best::strbuf(alias.name), + alias.vis.value_or(about.names[0].second())); }); if constexpr (!flags.is_empty()) { diff --git a/best/cli/toy.cc b/best/cli/toy.cc new file mode 100644 index 0000000..2b3a599 --- /dev/null +++ b/best/cli/toy.cc @@ -0,0 +1,8 @@ +#include "best/cli/app.h" +#include "best/cli/toy_flags.h" + +//! A very simply binary for demonstrating best's CLI library in action. + +namespace best::cli_toy { +best::app CliToy = [](MyFlags& flags) { best::println("{:#?}", flags); }; +} // namespace best::cli_toy \ No newline at end of file diff --git a/best/cli/toy_flags.h b/best/cli/toy_flags.h new file mode 100644 index 0000000..194cbad --- /dev/null +++ b/best/cli/toy_flags.h @@ -0,0 +1,155 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + Licensed under the Apache License, Version 2.0 (the "License"); you may not + use this file except in compliance with the License. You may obtain a copy + of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_CLI_TEST_FLAGS_H_ +#define BEST_CLI_TEST_FLAGS_H_ + +#include "best/cli/cli.h" + +//! Flags for helping test the CLI library. + +namespace best::cli_toy { +struct Subcommand { + int sub_flag = 0; + best::str arg; + + friend constexpr auto BestReflect(auto& m, Subcommand*) { + using ::best::cli; + return m.infer()->*m.field(best::vals<&Subcommand::sub_flag>, + cli::flag{ + .letter = 's', + .arg = "INT", + .help = "a subcommand argument", + }); + } + + bool operator==(const Subcommand&) const = default; +}; + +struct Subgroup { + int eks = 0, why = 0, zed = 0; + + friend constexpr auto BestReflect(auto& m, Subgroup*) { + using ::best::cli; + return m.infer() + ->*m.field(best::vals<&Subgroup::eks>, cli::flag{ + .letter = 'x', + .arg = "INT", + .help = "a group integer", + }) + ->*m.field(best::vals<&Subgroup::why>, cli::flag{ + .letter = 'y', + .arg = "INT", + .help = "another group integer", + }) + ->*m.field(best::vals<&Subgroup::zed>, cli::flag{ + .letter = 'z', + .arg = "INT", + .help = "a third group integer", + }); + } + + bool operator==(const Subgroup&) const = default; +}; + +struct MyFlags { + int foo = 0; + best::vec bar; + best::option baz; + best::str name, addr; + + bool flag1 = false; + bool flag2 = false; + bool flag3 = false; + bool flag4 = false; + + Subcommand sub; + Subcommand sub2; + + Subgroup group; + Subgroup flattened; + + best::str arg; + best::vec args; + + friend constexpr auto BestReflect(auto& m, MyFlags*) { + using ::best::cli; + return m.infer() + ->*m.field(best::vals<&MyFlags::foo>, cli::flag{ + .letter = 'f', + .arg = "INT", + .count = cli::Required, + .help = "a required integer", + }) + ->*m.field(best::vals<&MyFlags::bar>, cli::flag{ + .arg = "INT", + .help = "repeated integer", + }) + ->*m.field(best::vals<&MyFlags::baz>, cli::flag{ + .help = "an optional integer", + }) + + ->*m.field(best::vals<&MyFlags::name>, cli::flag{ + .vis = cli::Hidden, + .help = "your name", + }, cli::alias{"my-name"}) + ->*m.field(best::vals<&MyFlags::addr>, cli::flag{ + .vis = cli::Hidden, + .help = "your address", + }, cli::alias{"my-address"}) + + ->*m.field(best::vals<&MyFlags::flag1>, cli::flag{ + .letter = 'a', + .help = "this is a flag\nnewline", + }) + ->*m.field(best::vals<&MyFlags::flag2>, cli::flag{ + .letter = 'b', + .help = "this is a flag\nnewline", + }) + ->*m.field(best::vals<&MyFlags::flag3>, cli::flag{ + .letter = 'c', + .help = "this is a flag\nnewline", + }, cli::alias{"flag3-alias"}, + cli::alias{"flag3-alias2", cli::Hidden}) + ->*m.field(best::vals<&MyFlags::flag4>, cli::flag{ + .letter = 'd', + .help = "this is a flag\nnewline", + }) + + ->*m.field(best::vals<&MyFlags::sub>, cli::subcommand{ + .help = "a subcommand", + .about = "longer help for the subcommand\nwith multiple lines", + }) + ->*m.field(best::vals<&MyFlags::sub2>, cli::subcommand{ + .help = "identical in all ways to `sub`\nexceptt for this help", + .about = "longer help for the subcommand\nwith multiple lines", + }, cli::alias{"sub3"}) + + ->*m.field(best::vals<&MyFlags::group>, cli::group{ + .name = "subgroup", + .letter = 'X', + .help = "extra options behind the -X flag", + }) + ->*m.field(best::vals<&MyFlags::flattened>, cli::group{}); + } + + bool operator==(const MyFlags&) const = default; +}; +} // namespace best::cli_toy +#endif // BEST_CLI_TEST_FLAGS_H_ diff --git a/best/text/format.h b/best/text/format.h index 400a39f..f365922 100644 --- a/best/text/format.h +++ b/best/text/format.h @@ -433,7 +433,7 @@ void formatter::write(const best::string_type auto& string) { size_t watermark = 0; for (auto [idx, r] : string.rune_indices()) { if (r != '\n') continue; - if (idx != watermark + 1) { + if (idx != watermark + 1 && idx > 0) { update_indent(); out_->push_lossy(string[{.start = watermark, .end = idx - 1}]); } diff --git a/best/text/str.h b/best/text/str.h index 2d69f77..ac14bb7 100644 --- a/best/text/str.h +++ b/best/text/str.h @@ -848,13 +848,14 @@ class pretext::rune_index_iter_impl final { friend best::iter; friend best::iter; - constexpr explicit rune_index_iter_impl(rune_try_iter iter) : iter_(iter) {} + constexpr explicit rune_index_iter_impl(rune_try_iter iter) + : iter_(iter), size_(iter->rest().size()) {} constexpr best::option> next(); constexpr best::size_hint size_hint() const { return iter_.size_hint(); } constexpr size_t count() && { return BEST_MOVE(iter_).count(); } rune_try_iter iter_; - size_t idx_ = 0; + size_t size_; }; template @@ -1157,13 +1158,10 @@ text::rune_index_iter_impl::next() { template constexpr best::option> pretext::rune_index_iter_impl::next() { - size_t cur_len = iter_->rest().size(); auto next = iter_.next(); BEST_GUARD(next); - size_t idx = idx_; - idx_ += cur_len - iter_->rest().size(); - return {{idx, next->ok().value_or(rune::Replacement)}}; + return {{size_ - rest().size(), next->ok().value_or(rune::Replacement)}}; } template constexpr best::option>