mu: tidy up commands

Fix some typos, small errors, debug leftovers. Removed unused
mu-cmd-count.cc. Improve test-coverage. Fix manpage.

Make the errors for unknown commands a little friendlier.
This commit is contained in:
Dirk-Jan C. Binnema
2026-07-26 10:05:23 +03:00
committed by Seth Ladygo
parent 9cf9531acc
commit 6b1358ea48
15 changed files with 213 additions and 139 deletions

View File

@ -51,6 +51,18 @@ which does not give any output unless there is an error.
#+include: "prefooter.inc" :minlevel 1 #+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 * SEE ALSO
{{{man-link(mu,1)}}} {{{man-link(mu,1)}}}

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2022-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2022-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** 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. // candidate nick and a _count_ for that given nick, to uniquify them.
static std::unordered_map<std::string, size_t> nicks; using NickMap = std::unordered_map<std::string, size_t>;
static std::string static std::string
guess_nick(const Contact& contact) guess_nick(const Contact& contact, NickMap& nicks)
{ {
auto cleanup = [](const std::string& str) { auto cleanup = [](const std::string& str) {
std::string clean; std::string clean;
@ -88,13 +88,11 @@ guess_nick(const Contact& contact)
return names.first + initial; return names.first + initial;
})); }));
// uniquify. // uniquify; a second "foo" becomes "foo2", a third "foo3", ...
if (auto it = nicks.find(nick); it == nicks.cend()) if (auto it = nicks.find(nick); it == nicks.cend())
nicks.emplace(nick, 0); nicks.emplace(nick, 1);
else { else
++it->second;
nick = mu_format("{}{}", nick, ++it->second); nick = mu_format("{}{}", nick, ++it->second);
}
return nick; return nick;
} }
@ -117,12 +115,13 @@ output_plain(ItemType itype, OptContact contact, const Options& opts)
} }
static void 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) if (!contact)
return; return;
const auto nick{guess_nick(*contact)}; const auto nick{guess_nick(*contact, nicks)};
mu_print_encoded("alias {} {} <{}>\n", nick, contact->name, contact->email); 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 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()) if (!contact || contact->name.empty())
return; return;
auto nick=guess_nick(*contact); auto nick=guess_nick(*contact, nicks);
mu_print_encoded("{} \"{}\" \"{}\"\n", contact->email, nick, contact->name); mu_print_encoded("{} \"{}\" \"{}\"\n", contact->email, nick, contact->name);
@ -229,11 +229,17 @@ find_output_func(Format format)
case Format::Plain: case Format::Plain:
return output_plain; return output_plain;
case Format::MuttAlias: 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: case Format::MuttAddressBook:
return output_mutt_address_book; return output_mutt_address_book;
case Format::Wanderlust: 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: case Format::OrgContact:
return output_org_contact; return output_org_contact;
case Format::Bbdb: case Format::Bbdb:
@ -258,7 +264,6 @@ Mu::mu_cmd_cfind(const Mu::Store& store, const Mu::Options& opts)
if (!output) if (!output)
return Err(Error::Code::Internal, return Err(Error::Code::Internal,
"missing output function"); "missing output function");
nicks.clear();
const auto res = store.contacts_cache().for_each([&](const Contact& contact)->bool { const auto res = store.contacts_cache().for_each([&](const Contact& contact)->bool {
const auto itype{num == 0 ? ItemType::Header : ItemType::Normal}; 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 static void
test_mu_cfind_json() 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/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/org-contact", test_mu_cfind_org_contact);
g_test_add_func("/cmd/find/csv", test_mu_cfind_csv); 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); g_test_add_func("/cmd/find/json", test_mu_cfind_json);
return g_test_run(); return g_test_run();

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2010-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2010-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** under the terms of the GNU General Public License as published by the
@ -18,6 +18,7 @@
*/ */
#include "config.h" #include "config.h"
#include "mu-cmd.hh" #include "mu-cmd.hh"
#include "utils/mu-utils.hh" #include "utils/mu-utils.hh"
#include "utils/mu-utils-file.hh" #include "utils/mu-utils-file.hh"
@ -52,6 +53,14 @@ static Result<void>
save_parts(const Message& message, const std::string& filename_rx, save_parts(const Message& message, const std::string& filename_rx,
const Options& opts) 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{}; size_t partnum{}, saved_num{};
for (auto&& part: message.parts()) { for (auto&& part: message.parts()) {
++partnum; ++partnum;
@ -66,12 +75,9 @@ save_parts(const Message& message, const std::string& filename_rx,
else if (std::ranges::any_of(opts.extract.parts, else if (std::ranges::any_of(opts.extract.parts,
[&](auto&& num){return num==partnum;})) [&](auto&& num){return num==partnum;}))
return true; return true;
else if (!filename_rx.empty() && part.raw_filename()) { else if (rx && part.raw_filename() &&
if (auto rx = Regex::make(filename_rx); !rx) rx.matches(*part.raw_filename()))
throw rx.error();
else if (rx->matches(*part.raw_filename()))
return true; return true;
}
return false; return false;
}); });
@ -115,7 +121,7 @@ show_part(const MessagePart& part, size_t index, bool color)
const auto ctype{part.mime_type()}; const auto ctype{part.mime_type()};
fputs_encoded(ctype.value_or("<none>"), stdout); fputs_encoded(ctype.value_or("<none>"), stdout);
/* /\* disposition *\/ */ /* disposition */
color_maybe(MU_COLOR_MAGENTA); color_maybe(MU_COLOR_MAGENTA);
mu_print_encoded(" [{}]", part.is_attachment() ? "attachment" : "inline"); mu_print_encoded(" [{}]", part.is_attachment() ? "attachment" : "inline");
/* size */ /* size */
@ -194,8 +200,6 @@ get_file_size(const std::string& path)
int rv; int rv;
struct stat statbuf; struct stat statbuf;
mu_info("ppatj {}", path);
rv = stat(path.c_str(), &statbuf); rv = stat(path.c_str(), &statbuf);
if (rv != 0) { if (rv != 0) {
mu_debug ("error: {}", g_strerror (errno)); mu_debug ("error: {}", g_strerror (errno));

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2008-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2008-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** under the terms of the GNU General Public License as published by the
@ -26,8 +26,6 @@
#include <string.h> #include <string.h>
#include <errno.h> #include <errno.h>
#include <stdlib.h> #include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include "message/mu-message.hh" #include "message/mu-message.hh"
#include "mu-maildir.hh" #include "mu-maildir.hh"
@ -50,7 +48,7 @@ struct OutputInfo {
Xapian::docid docid{}; Xapian::docid docid{};
bool header{}; bool header{};
bool footer{}; bool footer{};
bool last{}; bool first{};
Option<QueryMatch&> match_info; Option<QueryMatch&> match_info;
}; };
@ -121,10 +119,9 @@ exec_cmd(const Option<Message>& msg, const OutputInfo& info, const Options& opts
if (!g_spawn_command_line_sync(cmdline.c_str(), {}, {}, &wait_status, &err)) if (!g_spawn_command_line_sync(cmdline.c_str(), {}, {}, &wait_status, &err))
return Err(Error::Code::File, &err/*consumed*/, return Err(Error::Code::File, &err/*consumed*/,
"failed to execute shell command"); "failed to execute shell command");
else if (WEXITSTATUS(wait_status) != 0) else if (!g_spawn_check_wait_status(wait_status, &err))
return Err(Error::Code::File, return Err(Error::Code::File, &err/*consumed*/,
"shell command exited with exit-code {}", "shell command failed");
WEXITSTATUS(wait_status));
return Ok(); return Ok();
} }
@ -356,9 +353,6 @@ static Result<void>
output_sexp(const Option<Message>& msg, const OutputInfo& info, const Options& opts) output_sexp(const Option<Message>& msg, const OutputInfo& info, const Options& opts)
{ {
if (msg) { 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); fputs("\n", stdout);
} }
@ -375,7 +369,7 @@ output_json(const Option<Message>& msg, const OutputInfo& info, const Options& o
} }
if (info.footer) { if (info.footer) {
mu_println("]"); mu_println("\n]");
return Ok(); return Ok();
} }
@ -385,7 +379,10 @@ output_json(const Option<Message>& msg, const OutputInfo& info, const Options& o
const Sexp::Format frm{opts.find.format == Format::Json2 ? Sexp::Format::NoColon : const Sexp::Format frm{opts.find.format == Format::Json2 ? Sexp::Format::NoColon :
Sexp::Format::Default}; 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(); return Ok();
} }
@ -414,6 +411,9 @@ output_xml(const Option<Message>& msg, const OutputInfo& info, const Options& op
return Ok(); return Ok();
} }
if (!msg)
return Ok();
mu_println("\t<message>"); mu_println("\t<message>");
print_attr_xml("from", to_string(msg->from())); print_attr_xml("from", to_string(msg->from()));
print_attr_xml("to", to_string(msg->to())); print_attr_xml("to", to_string(msg->to()));
@ -457,17 +457,13 @@ get_output_func(const Options& opts)
static Result<void> static Result<void>
output_query_results(const QueryResults& qres, const Options& opts) output_query_results(const QueryResults& qres, const Options& opts)
{ {
GError* err{};
const auto output_func{get_output_func(opts)}; 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) if (auto&& res = output_func(Nothing, FirstOutput, opts); !res)
return Err(std::move(res.error())); return Err(std::move(res.error()));
size_t n{0}; size_t printed{0};
for (auto&& item : qres) { for (auto&& item : qres) {
n++;
auto msg{item.message()}; auto msg{item.message()};
if (!msg) if (!msg)
continue; continue;
@ -479,10 +475,11 @@ output_query_results(const QueryResults& qres, const Options& opts)
{item.doc_id(), {item.doc_id(),
false, false,
false, false,
n == qres.size(), /* last? */ printed == 0, /* first? */
item.query_match()}, item.query_match()},
opts); !res) opts); !res)
return Err(std::move(res.error())); return Err(std::move(res.error()));
++printed;
} }
if (auto&& res{output_func(Nothing, LastOutput, opts)}; !res) 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 */ /* some more tests */
static void 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/02", test_mu_find_02);
g_test_add_func("/cmd/find/file", test_mu_find_file); 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/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/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/text-in-rfc822", test_mu_find_text_in_rfc822);
g_test_add_func("/cmd/find/wrong-muhome", test_mu_find_wrong_muhome); g_test_add_func("/cmd/find/wrong-muhome", test_mu_find_wrong_muhome);

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2023-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** 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) if (opts.nocolor || table.size() == 0)
return; return;
for (auto&& c = 0U; c != table.row(0).size(); ++c) { for (auto c = 0U; c != table.row(0).size(); ++c) {
switch (c) { switch (c) {
case 0: case 0:
table.column(c).format() 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() table[0][c].format()
.font_color(Color::white) .font_color(Color::white)
.font_style({FontStyle::bold}); .font_style({FontStyle::bold});
@ -232,7 +232,6 @@ topic_store(const Mu::Store& store, const Options& opts)
std::string{prop.description}}); std::string{prop.description}});
} }
} }
if (!opts.nocolor)
colorify(info, opts); colorify(info, opts);
std::cout << info << '\n'; std::cout << info << '\n';
@ -264,7 +263,6 @@ topic_mu(const Mu::Store& store, const Options& opts)
} }
} }
if (!opts.nocolor)
colorify(info, opts); colorify(info, opts);
std::cout << info << '\n'; std::cout << info << '\n';
@ -285,28 +283,31 @@ Mu::mu_cmd_info(const Mu::Store& store, const Options& opts)
else if (topic == "maildirs") else if (topic == "maildirs")
return topic_maildirs(store, opts); return topic_maildirs(store, opts);
else if (topic == "fields") { else if (topic == "fields") {
topic_fields(opts); if (auto&& res{topic_fields(opts)}; !res)
return res;
std::cout << std::endl; std::cout << std::endl;
topic_combi_fields(opts); if (auto&& res{topic_combi_fields(opts)}; !res)
return res;
std::cout << std::endl; std::cout << std::endl;
topic_flags(opts); return topic_flags(opts);
} else if (topic == "mu") { } else if (topic == "mu") {
return topic_mu(store, opts); return topic_mu(store, opts);
} else { } else {
topic_mu(store, opts); if (auto&& res{topic_mu(store, opts)}; !res)
return res;
MaybeAnsi col{!opts.nocolor}; MaybeAnsi col{!opts.nocolor};
using Color = MaybeAnsi::Color; 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}", return mu_format("{}{:<10}{} - {:>12}",
col.fg(Color::Green), t, col.reset(), d); col.fg(Color::Green), t, col.reset(), d);
}; };
mu_println("\nother info topics ('mu info <topic>'):\n{}\n{}\n{}", mu_println("\nother info topics ('mu info <topic>'):\n{}\n{}\n{}",
topic("store", "information about the message store (database)"), describe("store", "information about the message store (database)"),
topic("maildirs", "list the maildirs under the store's root-maildir"), describe("maildirs", "list the maildirs under the store's root-maildir"),
topic("fields", "information about message fields")); describe("fields", "information about message fields"));
} }
return Ok(); return Ok();

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2025-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** under the terms of the GNU General Public License as published by the
@ -55,7 +55,7 @@ label_update(Mu::Store& store, const Options& opts)
auto results{store.run_query(query)}; auto results{store.run_query(query)};
if (!results) if (!results)
return Err(Error{Error::Code::Query, 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 // seems we got some results... let's apply to each
size_t n{}; size_t n{};
@ -91,7 +91,7 @@ label_clear(Mu::Store& store, const Options& opts)
auto results{store.run_query(query)}; auto results{store.run_query(query)};
if (!results) if (!results)
return Err(Error{Error::Code::Query, 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{}; size_t n{};
for (auto&& result : *results) { for (auto&& result : *results) {

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2023-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** 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, "cur"), true, true));
g_assert_true(check_dir(join_paths(testdir2, "new"), 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, "cur"), true, true));
g_assert_true(check_dir(join_paths(testdir3, "new"), true, true)); g_assert_true(check_dir(join_paths(testdir3, "new"), true, true));

View File

@ -228,7 +228,7 @@ test_move_real()
const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")}; const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
{ {
auto store = Store::make_new(dbpath, testpath, {}); auto store = Store::make_new(dbpath, testpath, {});
assert_valid_result(res); assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/)); g_assert_true(store->indexer().start({}, true/*block*/));
} }

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2012-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2012-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** 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. // won't return unless there's an error.
return run_script(script_it->path, opts.script.params); return run_script(script_it->path, params);
} }

View File

@ -184,7 +184,8 @@ Mu::mu_cmd_server(const Mu::Options& opts) try {
if (line.find_first_not_of(" \t") == std::string::npos) if (line.find_first_not_of(" \t") == std::string::npos)
continue; // skip whitespace-only lines continue; // skip whitespace-only lines
do_quit = server.invoke(line) ? false : true; if (!server.invoke(line))
do_quit = true;
save_line(line); save_line(line);
} }

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2023-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** 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}; VFlags::EnableKeyserverLookups: VFlags::None};
auto ctx{MimeCryptoContext::make_gpg()}; auto ctx{MimeCryptoContext::make_gpg()};
if (!ctx) if (!ctx) {
if (!opts.quiet)
mu_println("cannot verify: {}", ctx.error().what());
return false; return false;
}
const auto sigs{sigpart.verify(*ctx, vflags)}; const auto sigs{sigpart.verify(*ctx, vflags)};
Mu::MaybeAnsi col{!opts.nocolor}; Mu::MaybeAnsi col{!opts.nocolor};
if (!sigs || sigs->empty()) { if (!sigs || sigs->empty()) {
if (!opts.quiet) if (!opts.quiet) {
if (!sigs)
mu_println("verification failed: {}", sigs.error().what());
else
mu_println("cannot find signatures in part"); mu_println("cannot find signatures in part");
}
return true; return false;
} }
bool valid{true}; bool valid{true};
@ -104,7 +111,7 @@ verify(const MimeMultipartSigned& sigpart, const Options& opts)
if (opts.verbose) if (opts.verbose)
print_signature(sig, opts); print_signature(sig, opts);
if (none_of(sig.status() & MimeSignature::Status::Green)) if (none_of(status & MimeSignature::Status::Green))
valid = false; valid = false;
} }
@ -159,7 +166,7 @@ Mu::mu_cmd_verify(const Options& opts)
all_ok = false; all_ok = false;
} }
// when no messages provided, read from stdin // when no messages are provided, read from stdin
if (opts.verify.files.empty()) { if (opts.verify.files.empty()) {
const auto msgtxt = read_from_stdin(); const auto msgtxt = read_from_stdin();
if (!msgtxt) if (!msgtxt)

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2023-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** 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); mu_print("{}", VIEW_TERMINATOR);
} }
// no files? read from stding // no files? read from stdin
if (opts.view.files.empty()) { if (opts.view.files.empty()) {
const auto msgtxt = read_from_stdin(); const auto msgtxt = read_from_stdin();
if (!msgtxt) if (!msgtxt)

View File

@ -56,16 +56,6 @@ cmd_fields(const Options& opts)
} }
static Result<void>
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<void> static Result<void>
cmd_scm(const Store& store, const Options& opts) cmd_scm(const Store& store, const Options& opts)
{ {
@ -87,42 +77,23 @@ static void
show_usage(void) show_usage(void)
{ {
mu_println("usage: mu command [options] [parameters]"); mu_println("usage: mu command [options] [parameters]");
mu_println("where command is one of index, find, cfind, view, mkdir, " mu_println("where command is one of add, cfind, extract, find, index, "
"extract, add, remove, script, verify or server"); "info, init, labels, mkdir, move, remove, scm, script, "
"server, verify or view");
mu_println("see the mu, mu-<command> or mu-easy manpages for " mu_println("see the mu, mu-<command> or mu-easy manpages for "
"more information"); "more information");
} }
using ReadOnlyStoreFunc = std::function<Result<void>(const Store&, const Options&)>; /* open the store with the given store-options and pass it to func; works for
using WritableStoreFunc = std::function<Result<void>(Store&, const Options&)>; * both funcs taking a const Store& and a Store& (the latter requires
* store_opts with Store::Options::Writable) */
template<typename StoreFunc>
static Result<void> static Result<void>
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))}; auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb), store_opts)};
if (!store)
return Err(store.error());
return func(store.value(), opts);
}
static Result<void> // 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<void>
with_writable_store(const WritableStoreFunc func, const Options& opts)
{
auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb),
Store::Options::Writable)};
if (!store) if (!store)
return Err(store.error()); return Err(store.error());
@ -158,32 +129,30 @@ Mu::mu_cmd_execute(const Options& opts) try {
*/ */
case Options::SubCommand::Cfind: case Options::SubCommand::Cfind:
return with_readonly_store(mu_cmd_cfind, opts); return with_store(mu_cmd_cfind, opts);
case Options::SubCommand::Find: case Options::SubCommand::Find:
return cmd_find(opts); return with_store(mu_cmd_find, opts);
case Options::SubCommand::Info: case Options::SubCommand::Info:
return with_readonly_store(mu_cmd_info, opts); return with_store(mu_cmd_info, opts);
case Options::SubCommand::Scm: case Options::SubCommand::Scm:
return with_readonly_store(cmd_scm, opts); return with_store(cmd_scm, opts);
/* writable store */ /* writable store */
case Options::SubCommand::Add: 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: 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: 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 * read-only _or_ writable store
*/ */
case Options::SubCommand::Labels: case Options::SubCommand::Labels:
if (opts.labels.read_only) return with_store(mu_cmd_labels, opts,
return with_readonly_store2(mu_cmd_labels, opts); opts.labels.read_only ?
else Store::Options::None : Store::Options::Writable);
return with_writable_store(mu_cmd_labels, opts);
/* /*
* commands instantiate store themselves * commands instantiate store themselves
*/ */

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2008-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2008-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** under the terms of the GNU General Public License as published by the
@ -201,4 +201,4 @@ Result<void> mu_cmd_execute(const Options& opts);
} // namespace Mu } // namespace Mu
#endif /*__MU_CMD_H__*/ #endif /*MU_CMD_HH__*/

View File

@ -1044,6 +1044,16 @@ There is NO WARRANTY, to the extent permitted by law.)");
mu_println("{}", app.help("", CLI::AppFormatMode::All)); mu_println("{}", app.help("", CLI::AppFormatMode::All));
} catch (const CLI::CallForVersion&) { } catch (const CLI::CallForVersion&) {
mu_println("version {}", PACKAGE_VERSION); 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) { } catch (const CLI::ParseError& pe) {
return Err(Error::Code::InvalidArgument, "{}", return Err(Error::Code::InvalidArgument, "{}",
friendly_error(pe)); friendly_error(pe));
@ -1223,6 +1233,35 @@ test_number_option(void)
"--after requires a non-negative number"); "--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 int
main(int argc, char* argv[]) 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/choice-errors", test_choice_option_errors);
g_test_add_func("/options/sortfield", test_sortfield_option); g_test_add_func("/options/sortfield", test_sortfield_option);
g_test_add_func("/options/number", test_number_option); g_test_add_func("/options/number", test_number_option);
g_test_add_func("/options/unknown-command", test_unknown_command);
return g_test_run(); return g_test_run();
} }