diff --git a/man/mu-verify.1.org b/man/mu-verify.1.org index e253ff07..04b62b18 100644 --- a/man/mu-verify.1.org +++ b/man/mu-verify.1.org @@ -51,6 +51,18 @@ which does not give any output unless there is an error. #+include: "prefooter.inc" :minlevel 1 +* EXIT CODE + +This command returns 0 when all signatures could be verified successfully, or a +non-zero exit code otherwise. + + 0. success; all signatures were verified + 1. verification failed; at least one signature was bad, or could not be + verified. The latter includes messages without any signed parts, + signed parts without verifiable signatures and failures to set up the + crypto machinery for verification. + + * SEE ALSO {{{man-link(mu,1)}}} diff --git a/mu/mu-cmd-cfind.cc b/mu/mu-cmd-cfind.cc index 25c75fc9..be43c6ee 100644 --- a/mu/mu-cmd-cfind.cc +++ b/mu/mu-cmd-cfind.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2025 Dirk-Jan C. Binnema +** Copyright (C) 2022-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -51,9 +51,9 @@ guess_first_last_name(const std::string& name) // candidate nick and a _count_ for that given nick, to uniquify them. -static std::unordered_map nicks; +using NickMap = std::unordered_map; static std::string -guess_nick(const Contact& contact) +guess_nick(const Contact& contact, NickMap& nicks) { auto cleanup = [](const std::string& str) { std::string clean; @@ -88,13 +88,11 @@ guess_nick(const Contact& contact) return names.first + initial; })); - // uniquify. + // uniquify; a second "foo" becomes "foo2", a third "foo3", ... if (auto it = nicks.find(nick); it == nicks.cend()) - nicks.emplace(nick, 0); - else { - ++it->second; + nicks.emplace(nick, 1); + else nick = mu_format("{}{}", nick, ++it->second); - } return nick; } @@ -117,12 +115,13 @@ output_plain(ItemType itype, OptContact contact, const Options& opts) } static void -output_mutt_alias(ItemType itype, OptContact contact, const Options& opts) +output_mutt_alias(ItemType itype, OptContact contact, const Options& opts, + NickMap& nicks) { if (!contact) return; - const auto nick{guess_nick(*contact)}; + const auto nick{guess_nick(*contact, nicks)}; mu_print_encoded("alias {} {} <{}>\n", nick, contact->name, contact->email); } @@ -138,12 +137,13 @@ output_mutt_address_book(ItemType itype, OptContact contact, const Options& opts } static void -output_wanderlust(ItemType itype, OptContact contact, const Options& opts) +output_wanderlust(ItemType itype, OptContact contact, const Options& opts, + NickMap& nicks) { if (!contact || contact->name.empty()) return; - auto nick=guess_nick(*contact); + auto nick=guess_nick(*contact, nicks); mu_print_encoded("{} \"{}\" \"{}\"\n", contact->email, nick, contact->name); @@ -229,11 +229,17 @@ find_output_func(Format format) case Format::Plain: return output_plain; case Format::MuttAlias: - return output_mutt_alias; + return [nicks = NickMap{}](ItemType itype, OptContact contact, + const Options& opts) mutable { + output_mutt_alias(itype, contact, opts, nicks); + }; case Format::MuttAddressBook: return output_mutt_address_book; case Format::Wanderlust: - return output_wanderlust; + return [nicks = NickMap{}](ItemType itype, OptContact contact, + const Options& opts) mutable { + output_wanderlust(itype, contact, opts, nicks); + }; case Format::OrgContact: return output_org_contact; case Format::Bbdb: @@ -258,7 +264,6 @@ Mu::mu_cmd_cfind(const Mu::Store& store, const Mu::Options& opts) if (!output) return Err(Error::Code::Internal, "missing output function"); - nicks.clear(); const auto res = store.contacts_cache().for_each([&](const Contact& contact)->bool { const auto itype{num == 0 ? ItemType::Header : ItemType::Normal}; @@ -446,6 +451,24 @@ test_mu_cfind_csv(void) } +static void +test_guess_nick() +{ + NickMap nicks; + + g_assert_cmpstr(guess_nick(Contact{"foo@example.com", "Foo Bar"}, nicks).c_str(), + ==, "FooB"); + g_assert_cmpstr(guess_nick(Contact{"foo2@example.com", "Foo Bar"}, nicks).c_str(), + ==, "FooB2"); + g_assert_cmpstr(guess_nick(Contact{"foo3@example.com", "Foo Bar"}, nicks).c_str(), + ==, "FooB3"); + + g_assert_cmpstr(guess_nick(Contact{"cuux@example.com", "Cuux"}, nicks).c_str(), + ==, "Cuux"); + g_assert_cmpstr(guess_nick(Contact{"bar@example.com", ""}, nicks).c_str(), + ==, "bar"); +} + static void test_mu_cfind_json() { @@ -496,6 +519,7 @@ main(int argc, char* argv[]) g_test_add_func("/cmd/find/mutt-ab", test_mu_cfind_mutt_ab); g_test_add_func("/cmd/find/org-contact", test_mu_cfind_org_contact); g_test_add_func("/cmd/find/csv", test_mu_cfind_csv); + g_test_add_func("/cmd/find/guess-nick", test_guess_nick); g_test_add_func("/cmd/find/json", test_mu_cfind_json); return g_test_run(); diff --git a/mu/mu-cmd-extract.cc b/mu/mu-cmd-extract.cc index 36319fec..a0233da9 100644 --- a/mu/mu-cmd-extract.cc +++ b/mu/mu-cmd-extract.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2010-2023 Dirk-Jan C. Binnema +** Copyright (C) 2010-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -18,6 +18,7 @@ */ #include "config.h" + #include "mu-cmd.hh" #include "utils/mu-utils.hh" #include "utils/mu-utils-file.hh" @@ -52,6 +53,14 @@ static Result save_parts(const Message& message, const std::string& filename_rx, const Options& opts) { + Regex rx{}; + if (!filename_rx.empty()) { + if (auto&& res{Regex::make(filename_rx)}; !res) + return Err(res.error()); + else + rx = std::move(*res); + } + size_t partnum{}, saved_num{}; for (auto&& part: message.parts()) { ++partnum; @@ -66,12 +75,9 @@ save_parts(const Message& message, const std::string& filename_rx, else if (std::ranges::any_of(opts.extract.parts, [&](auto&& num){return num==partnum;})) return true; - else if (!filename_rx.empty() && part.raw_filename()) { - if (auto rx = Regex::make(filename_rx); !rx) - throw rx.error(); - else if (rx->matches(*part.raw_filename())) - return true; - } + else if (rx && part.raw_filename() && + rx.matches(*part.raw_filename())) + return true; return false; }); @@ -115,7 +121,7 @@ show_part(const MessagePart& part, size_t index, bool color) const auto ctype{part.mime_type()}; fputs_encoded(ctype.value_or(""), stdout); - /* /\* disposition *\/ */ + /* disposition */ color_maybe(MU_COLOR_MAGENTA); mu_print_encoded(" [{}]", part.is_attachment() ? "attachment" : "inline"); /* size */ @@ -194,8 +200,6 @@ get_file_size(const std::string& path) int rv; struct stat statbuf; - mu_info("ppatj {}", path); - rv = stat(path.c_str(), &statbuf); if (rv != 0) { mu_debug ("error: {}", g_strerror (errno)); diff --git a/mu/mu-cmd-find.cc b/mu/mu-cmd-find.cc index a85bdc8a..f62d8f46 100644 --- a/mu/mu-cmd-find.cc +++ b/mu/mu-cmd-find.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2008-2025 Dirk-Jan C. Binnema +** Copyright (C) 2008-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -26,8 +26,6 @@ #include #include #include -#include -#include #include "message/mu-message.hh" #include "mu-maildir.hh" @@ -50,7 +48,7 @@ struct OutputInfo { Xapian::docid docid{}; bool header{}; bool footer{}; - bool last{}; + bool first{}; Option match_info; }; @@ -121,10 +119,9 @@ exec_cmd(const Option& msg, const OutputInfo& info, const Options& opts if (!g_spawn_command_line_sync(cmdline.c_str(), {}, {}, &wait_status, &err)) return Err(Error::Code::File, &err/*consumed*/, "failed to execute shell command"); - else if (WEXITSTATUS(wait_status) != 0) - return Err(Error::Code::File, - "shell command exited with exit-code {}", - WEXITSTATUS(wait_status)); + else if (!g_spawn_check_wait_status(wait_status, &err)) + return Err(Error::Code::File, &err/*consumed*/, + "shell command failed"); return Ok(); } @@ -356,10 +353,7 @@ static Result output_sexp(const Option& msg, const OutputInfo& info, const Options& opts) { if (msg) { - if (const auto sexp{msg->sexp()}; !sexp.empty()) - fputs(sexp.to_string().c_str(), stdout); - else - fputs(msg->sexp().to_string().c_str(), stdout); + fputs(msg->sexp().to_string().c_str(), stdout); fputs("\n", stdout); } @@ -375,7 +369,7 @@ output_json(const Option& msg, const OutputInfo& info, const Options& o } if (info.footer) { - mu_println("]"); + mu_println("\n]"); return Ok(); } @@ -385,7 +379,10 @@ output_json(const Option& msg, const OutputInfo& info, const Options& o const Sexp::Format frm{opts.find.format == Format::Json2 ? Sexp::Format::NoColon : Sexp::Format::Default}; - mu_println("{}{}", msg->sexp().to_json_string(frm), info.last ? "" : ","); + /* separate objects with a ",\n" _before_ each non-first item; we cannot + * use a trailing comma, since we don't know up-front whether an item is + * the last one (some may be skipped) */ + mu_print("{}{}", info.first ? "" : ",\n", msg->sexp().to_json_string(frm)); return Ok(); } @@ -414,6 +411,9 @@ output_xml(const Option& msg, const OutputInfo& info, const Options& op return Ok(); } + if (!msg) + return Ok(); + mu_println("\t"); print_attr_xml("from", to_string(msg->from())); print_attr_xml("to", to_string(msg->to())); @@ -457,17 +457,13 @@ get_output_func(const Options& opts) static Result output_query_results(const QueryResults& qres, const Options& opts) { - GError* err{}; const auto output_func{get_output_func(opts)}; - if (!output_func) - return Err(Error::Code::Query, &err, "failed to find output function"); if (auto&& res = output_func(Nothing, FirstOutput, opts); !res) return Err(std::move(res.error())); - size_t n{0}; + size_t printed{0}; for (auto&& item : qres) { - n++; auto msg{item.message()}; if (!msg) continue; @@ -479,10 +475,11 @@ output_query_results(const QueryResults& qres, const Options& opts) {item.doc_id(), false, false, - n == qres.size(), /* last? */ + printed == 0, /* first? */ item.query_match()}, opts); !res) return Err(std::move(res.error())); + ++printed; } if (auto&& res{output_func(Nothing, LastOutput, opts)}; !res) @@ -630,6 +627,24 @@ test_mu_find_maildir_special(void) } +static void +test_mu_find_json(void) +{ + auto res = run_command({MU_PROGRAM, "find", "--muhome", test_mu_home, + "--format", "json", "mime:message/rfc822"}); + assert_valid_result(res); + g_assert_cmpuint(res->exit_code, ==, 0); + + /* a JSON array with two objects, separated by ",\n"; no trailing + * comma before the closing bracket */ + const auto& out{res->standard_out}; + g_assert_true(out.starts_with("[\n")); + g_assert_true(out.ends_with("\n]\n")); + g_assert_true(out.find(",\n") != std::string::npos); + g_assert_true(out.find(",\n]") == std::string::npos); + g_assert_cmpuint(count_nl(out), ==, 4); +} + /* some more tests */ static void @@ -721,6 +736,7 @@ main(int argc, char* argv[]) g_test_add_func("/cmd/find/02", test_mu_find_02); g_test_add_func("/cmd/find/file", test_mu_find_file); g_test_add_func("/cmd/find/mime", test_mu_find_mime); + g_test_add_func("/cmd/find/json", test_mu_find_json); g_test_add_func("/cmd/find/links", test_mu_find_links); g_test_add_func("/cmd/find/text-in-rfc822", test_mu_find_text_in_rfc822); g_test_add_func("/cmd/find/wrong-muhome", test_mu_find_wrong_muhome); diff --git a/mu/mu-cmd-info.cc b/mu/mu-cmd-info.cc index 2c2eb437..876e01e6 100644 --- a/mu/mu-cmd-info.cc +++ b/mu/mu-cmd-info.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023 Dirk-Jan C. Binnema +** Copyright (C) 2023-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -41,7 +41,7 @@ colorify(Table& table, const Options& opts) if (opts.nocolor || table.size() == 0) return; - for (auto&& c = 0U; c != table.row(0).size(); ++c) { + for (auto c = 0U; c != table.row(0).size(); ++c) { switch (c) { case 0: table.column(c).format() @@ -85,7 +85,7 @@ colorify(Table& table, const Options& opts) } } - for (auto&& c = 0U; c != table.row(0).size(); ++c) + for (auto c = 0U; c != table.row(0).size(); ++c) table[0][c].format() .font_color(Color::white) .font_style({FontStyle::bold}); @@ -232,8 +232,7 @@ topic_store(const Mu::Store& store, const Options& opts) std::string{prop.description}}); } } - if (!opts.nocolor) - colorify(info, opts); + colorify(info, opts); std::cout << info << '\n'; @@ -264,8 +263,7 @@ topic_mu(const Mu::Store& store, const Options& opts) } } - if (!opts.nocolor) - colorify(info, opts); + colorify(info, opts); std::cout << info << '\n'; @@ -285,28 +283,31 @@ Mu::mu_cmd_info(const Mu::Store& store, const Options& opts) else if (topic == "maildirs") return topic_maildirs(store, opts); else if (topic == "fields") { - topic_fields(opts); + if (auto&& res{topic_fields(opts)}; !res) + return res; std::cout << std::endl; - topic_combi_fields(opts); + if (auto&& res{topic_combi_fields(opts)}; !res) + return res; std::cout << std::endl; - topic_flags(opts); + return topic_flags(opts); } else if (topic == "mu") { return topic_mu(store, opts); } else { - topic_mu(store, opts); + if (auto&& res{topic_mu(store, opts)}; !res) + return res; MaybeAnsi col{!opts.nocolor}; using Color = MaybeAnsi::Color; - auto topic = [&](auto&& t, auto&& d)->std::string { + auto describe = [&](auto&& t, auto&& d)->std::string { return mu_format("{}{:<10}{} - {:>12}", col.fg(Color::Green), t, col.reset(), d); }; mu_println("\nother info topics ('mu info '):\n{}\n{}\n{}", - topic("store", "information about the message store (database)"), - topic("maildirs", "list the maildirs under the store's root-maildir"), - topic("fields", "information about message fields")); + describe("store", "information about the message store (database)"), + describe("maildirs", "list the maildirs under the store's root-maildir"), + describe("fields", "information about message fields")); } return Ok(); diff --git a/mu/mu-cmd-labels.cc b/mu/mu-cmd-labels.cc index 95c8c7e3..251ab9a6 100644 --- a/mu/mu-cmd-labels.cc +++ b/mu/mu-cmd-labels.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2025 Dirk-Jan C. Binnema +** Copyright (C) 2025-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -38,12 +38,12 @@ label_update(Mu::Store& store, const Options& opts) // are valid. DeltaLabelVec deltas{}; for (auto&& delta_label : opts.labels.delta_labels) { - if (const auto res = parse_delta_label(delta_label); !res) - return Err(Error{Error::Code::InvalidArgument, - "invalid delta-label '{}': {}", delta_label, - res.error().what()}); - else - deltas.emplace_back(std::move(*res)); + if (const auto res = parse_delta_label(delta_label); !res) + return Err(Error{Error::Code::InvalidArgument, + "invalid delta-label '{}': {}", delta_label, + res.error().what()}); + else + deltas.emplace_back(std::move(*res)); } if (!opts.labels.query) @@ -55,7 +55,7 @@ label_update(Mu::Store& store, const Options& opts) auto results{store.run_query(query)}; if (!results) return Err(Error{Error::Code::Query, - "failed to run query '{}': {}", query, *results.error().what()}); + "failed to run query '{}': {}", query, results.error().what()}); // seems we got some results... let's apply to each size_t n{}; @@ -91,7 +91,7 @@ label_clear(Mu::Store& store, const Options& opts) auto results{store.run_query(query)}; if (!results) return Err(Error{Error::Code::Query, - "failed to run query '{}': {}", query, *results.error().what()}); + "failed to run query '{}': {}", query, results.error().what()}); size_t n{}; for (auto&& result : *results) { diff --git a/mu/mu-cmd-mkdir.cc b/mu/mu-cmd-mkdir.cc index b91bdec4..d6a73c84 100644 --- a/mu/mu-cmd-mkdir.cc +++ b/mu/mu-cmd-mkdir.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023 Dirk-Jan C. Binnema +** Copyright (C) 2023-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -72,7 +72,7 @@ test_mkdir_multi() g_assert_true(check_dir(join_paths(testdir2, "cur"), true, true)); g_assert_true(check_dir(join_paths(testdir2, "new"), true, true)); - g_assert_true(check_dir(join_paths(testdir3, "tmp"), true, true)); + g_assert_true(check_dir(join_paths(testdir2, "tmp"), true, true)); g_assert_true(check_dir(join_paths(testdir3, "cur"), true, true)); g_assert_true(check_dir(join_paths(testdir3, "new"), true, true)); diff --git a/mu/mu-cmd-move.cc b/mu/mu-cmd-move.cc index dcbc9b32..eb4b0254 100644 --- a/mu/mu-cmd-move.cc +++ b/mu/mu-cmd-move.cc @@ -24,7 +24,7 @@ #include "mu-maildir.hh" #include "message/mu-message-file.hh" - #include +#include using namespace Mu; @@ -228,7 +228,7 @@ test_move_real() const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")}; { auto store = Store::make_new(dbpath, testpath, {}); - assert_valid_result(res); + assert_valid_result(store); g_assert_true(store->indexer().start({}, true/*block*/)); } diff --git a/mu/mu-cmd-script.cc b/mu/mu-cmd-script.cc index c0642963..b91f340b 100644 --- a/mu/mu-cmd-script.cc +++ b/mu/mu-cmd-script.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2012-2022 Dirk-Jan C. Binnema +** Copyright (C) 2012-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -45,5 +45,5 @@ Mu::mu_cmd_script(const Options& opts) } // won't return unless there's an error. - return run_script(script_it->path, opts.script.params); + return run_script(script_it->path, params); } diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc index bd6868aa..f8d843d0 100644 --- a/mu/mu-cmd-server.cc +++ b/mu/mu-cmd-server.cc @@ -184,7 +184,8 @@ Mu::mu_cmd_server(const Mu::Options& opts) try { if (line.find_first_not_of(" \t") == std::string::npos) continue; // skip whitespace-only lines - do_quit = server.invoke(line) ? false : true; + if (!server.invoke(line)) + do_quit = true; save_line(line); } diff --git a/mu/mu-cmd-verify.cc b/mu/mu-cmd-verify.cc index 7fbb4b9f..afe40768 100644 --- a/mu/mu-cmd-verify.cc +++ b/mu/mu-cmd-verify.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023 Dirk-Jan C. Binnema +** Copyright (C) 2023-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -79,18 +79,25 @@ verify(const MimeMultipartSigned& sigpart, const Options& opts) VFlags::EnableKeyserverLookups: VFlags::None}; auto ctx{MimeCryptoContext::make_gpg()}; - if (!ctx) + if (!ctx) { + if (!opts.quiet) + mu_println("cannot verify: {}", ctx.error().what()); return false; + } const auto sigs{sigpart.verify(*ctx, vflags)}; Mu::MaybeAnsi col{!opts.nocolor}; if (!sigs || sigs->empty()) { - if (!opts.quiet) - mu_println("cannot find signatures in part"); + if (!opts.quiet) { + if (!sigs) + mu_println("verification failed: {}", sigs.error().what()); + else + mu_println("cannot find signatures in part"); + } - return true; + return false; } bool valid{true}; @@ -104,7 +111,7 @@ verify(const MimeMultipartSigned& sigpart, const Options& opts) if (opts.verbose) print_signature(sig, opts); - if (none_of(sig.status() & MimeSignature::Status::Green)) + if (none_of(status & MimeSignature::Status::Green)) valid = false; } @@ -159,7 +166,7 @@ Mu::mu_cmd_verify(const Options& opts) all_ok = false; } - // when no messages provided, read from stdin + // when no messages are provided, read from stdin if (opts.verify.files.empty()) { const auto msgtxt = read_from_stdin(); if (!msgtxt) diff --git a/mu/mu-cmd-view.cc b/mu/mu-cmd-view.cc index e1a140c3..5e528447 100644 --- a/mu/mu-cmd-view.cc +++ b/mu/mu-cmd-view.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023 Dirk-Jan C. Binnema +** Copyright (C) 2023-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -186,7 +186,7 @@ Mu::mu_cmd_view(const Options& opts) mu_print("{}", VIEW_TERMINATOR); } - // no files? read from stding + // no files? read from stdin if (opts.view.files.empty()) { const auto msgtxt = read_from_stdin(); if (!msgtxt) diff --git a/mu/mu-cmd.cc b/mu/mu-cmd.cc index dbb9a95a..2b1c2bae 100644 --- a/mu/mu-cmd.cc +++ b/mu/mu-cmd.cc @@ -56,16 +56,6 @@ cmd_fields(const Options& opts) } -static Result -cmd_find(const Options& opts) -{ - auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb))}; - if (!store) - return Err(store.error()); - else - return mu_cmd_find(*store, opts); -} - static Result cmd_scm(const Store& store, const Options& opts) { @@ -77,7 +67,7 @@ cmd_scm(const Store& store, const Options& opts) return Mu::Scm::run_script(store, opts, *opts.scm.script_path); else if (opts.scm.eval) return Mu::Scm::run_eval(store, opts, *opts.scm.eval); - else + else return Mu::Scm::run_repl(store, opts, opts.scm.socket_path.value_or("")); #endif /*BUILD_SCM*/ } @@ -87,42 +77,23 @@ static void show_usage(void) { mu_println("usage: mu command [options] [parameters]"); - mu_println("where command is one of index, find, cfind, view, mkdir, " - "extract, add, remove, script, verify or server"); + mu_println("where command is one of add, cfind, extract, find, index, " + "info, init, labels, mkdir, move, remove, scm, script, " + "server, verify or view"); mu_println("see the mu, mu- or mu-easy manpages for " "more information"); } -using ReadOnlyStoreFunc = std::function(const Store&, const Options&)>; -using WritableStoreFunc = std::function(Store&, const Options&)>; - +/* open the store with the given store-options and pass it to func; works for + * both funcs taking a const Store& and a Store& (the latter requires + * store_opts with Store::Options::Writable) */ +template static Result -with_readonly_store(const ReadOnlyStoreFunc& func, const Options& opts) +with_store(const StoreFunc& func, const Options& opts, + Store::Options store_opts = Store::Options::None) { - auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb))}; - if (!store) - return Err(store.error()); - - return func(store.value(), opts); -} - -static Result // overloading does not work. -with_readonly_store2(const WritableStoreFunc& func, const Options& opts) -{ - auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb))}; - if (!store) - return Err(store.error()); - - return func(store.value(), opts); -} - - -static Result -with_writable_store(const WritableStoreFunc func, const Options& opts) -{ - auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb), - Store::Options::Writable)}; + auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb), store_opts)}; if (!store) return Err(store.error()); @@ -158,32 +129,30 @@ Mu::mu_cmd_execute(const Options& opts) try { */ case Options::SubCommand::Cfind: - return with_readonly_store(mu_cmd_cfind, opts); + return with_store(mu_cmd_cfind, opts); case Options::SubCommand::Find: - return cmd_find(opts); + return with_store(mu_cmd_find, opts); case Options::SubCommand::Info: - return with_readonly_store(mu_cmd_info, opts); + return with_store(mu_cmd_info, opts); case Options::SubCommand::Scm: - return with_readonly_store(cmd_scm, opts); - + return with_store(cmd_scm, opts); /* writable store */ case Options::SubCommand::Add: - return with_writable_store(mu_cmd_add, opts); + return with_store(mu_cmd_add, opts, Store::Options::Writable); case Options::SubCommand::Remove: - return with_writable_store(mu_cmd_remove, opts); + return with_store(mu_cmd_remove, opts, Store::Options::Writable); case Options::SubCommand::Move: - return with_writable_store(mu_cmd_move, opts); + return with_store(mu_cmd_move, opts, Store::Options::Writable); /* * read-only _or_ writable store */ case Options::SubCommand::Labels: - if (opts.labels.read_only) - return with_readonly_store2(mu_cmd_labels, opts); - else - return with_writable_store(mu_cmd_labels, opts); + return with_store(mu_cmd_labels, opts, + opts.labels.read_only ? + Store::Options::None : Store::Options::Writable); /* * commands instantiate store themselves */ diff --git a/mu/mu-cmd.hh b/mu/mu-cmd.hh index e5ddb624..58cf324f 100644 --- a/mu/mu-cmd.hh +++ b/mu/mu-cmd.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2008-2025 Dirk-Jan C. Binnema +** Copyright (C) 2008-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -201,4 +201,4 @@ Result mu_cmd_execute(const Options& opts); } // namespace Mu -#endif /*__MU_CMD_H__*/ +#endif /*MU_CMD_HH__*/ diff --git a/mu/mu-options.cc b/mu/mu-options.cc index 23a8e631..99fdbea3 100644 --- a/mu/mu-options.cc +++ b/mu/mu-options.cc @@ -1044,6 +1044,16 @@ There is NO WARRANTY, to the extent permitted by law.)"); mu_println("{}", app.help("", CLI::AppFormatMode::All)); } catch (const CLI::CallForVersion&) { mu_println("version {}", PACKAGE_VERSION); + } catch (const CLI::ExtrasError& xe) { + /* a first unexpected non-option argument without any subcommand + * is (likely) a mistyped or unknown command */ + if (const auto extras{app.remaining()}; + app.get_subcommands().empty() && !extras.empty() && + !extras.front().starts_with('-')) + return Err(Error::Code::InvalidArgument, + "'{}' is not a mu command. See 'mu --help'", + extras.front()); + return Err(Error::Code::InvalidArgument, "{}", xe.what()); } catch (const CLI::ParseError& pe) { return Err(Error::Code::InvalidArgument, "{}", friendly_error(pe)); @@ -1223,6 +1233,35 @@ test_number_option(void) "--after requires a non-negative number"); } +static void +test_unknown_command(void) +{ + constexpr auto errmsg = "'flimflam' is not a mu command. See 'mu --help'"; + + const auto unknown = test_make_options({"mu", "flimflam"}); + g_assert_false(!!unknown); + assert_equal(unknown.error().what(), errmsg); + + // also with a global option in front + const auto with_opt = test_make_options({"mu", "--quiet", "flimflam"}); + g_assert_false(!!with_opt); + assert_equal(with_opt.error().what(), errmsg); + + // an unknown _option_ keeps CLI11's message + const auto unknown_opt = test_make_options({"mu", "--flimflam"}); + g_assert_false(!!unknown_opt); + g_assert_true(std::string{unknown_opt.error().what()} + .find("--flimflam") != std::string::npos); + g_assert_true(std::string{unknown_opt.error().what()} + .find("is not a mu command") == std::string::npos); + + // extra arguments to a real subcommand are not rewritten + const auto sub_extra = test_make_options({"mu", "index", "extra"}); + g_assert_false(!!sub_extra); + g_assert_true(std::string{sub_extra.error().what()} + .find("is not a mu command") == std::string::npos); +} + int main(int argc, char* argv[]) { @@ -1234,6 +1273,7 @@ main(int argc, char* argv[]) g_test_add_func("/options/choice-errors", test_choice_option_errors); g_test_add_func("/options/sortfield", test_sortfield_option); g_test_add_func("/options/number", test_number_option); + g_test_add_func("/options/unknown-command", test_unknown_command); return g_test_run(); }