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:
@ -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)}}}
|
||||
|
||||
@ -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
|
||||
** 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<std::string, size_t> nicks;
|
||||
using NickMap = std::unordered_map<std::string, size_t>;
|
||||
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();
|
||||
|
||||
@ -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
|
||||
** 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<void>
|
||||
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()))
|
||||
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("<none>"), 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));
|
||||
|
||||
@ -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
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
@ -26,8 +26,6 @@
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#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<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))
|
||||
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,9 +353,6 @@ static Result<void>
|
||||
output_sexp(const Option<Message>& 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("\n", stdout);
|
||||
}
|
||||
@ -375,7 +369,7 @@ output_json(const Option<Message>& 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<Message>& 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<Message>& msg, const OutputInfo& info, const Options& op
|
||||
return Ok();
|
||||
}
|
||||
|
||||
if (!msg)
|
||||
return Ok();
|
||||
|
||||
mu_println("\t<message>");
|
||||
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<void>
|
||||
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);
|
||||
|
||||
@ -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
|
||||
** 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,7 +232,6 @@ topic_store(const Mu::Store& store, const Options& opts)
|
||||
std::string{prop.description}});
|
||||
}
|
||||
}
|
||||
if (!opts.nocolor)
|
||||
colorify(info, opts);
|
||||
|
||||
std::cout << info << '\n';
|
||||
@ -264,7 +263,6 @@ topic_mu(const Mu::Store& store, const Options& opts)
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.nocolor)
|
||||
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 <topic>'):\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();
|
||||
|
||||
@ -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
|
||||
** 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)};
|
||||
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) {
|
||||
|
||||
@ -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
|
||||
** 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));
|
||||
|
||||
@ -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*/));
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
** 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
** 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)
|
||||
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)
|
||||
|
||||
@ -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
|
||||
** 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)
|
||||
|
||||
71
mu/mu-cmd.cc
71
mu/mu-cmd.cc
@ -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>
|
||||
cmd_scm(const Store& store, const Options& opts)
|
||||
{
|
||||
@ -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-<command> or mu-easy manpages for "
|
||||
"more information");
|
||||
}
|
||||
|
||||
|
||||
using ReadOnlyStoreFunc = std::function<Result<void>(const Store&, const Options&)>;
|
||||
using WritableStoreFunc = std::function<Result<void>(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<typename StoreFunc>
|
||||
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))};
|
||||
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)};
|
||||
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
|
||||
*/
|
||||
|
||||
@ -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
|
||||
** 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
|
||||
|
||||
#endif /*__MU_CMD_H__*/
|
||||
#endif /*MU_CMD_HH__*/
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user