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
* 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)}}}

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
** 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();

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
** 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));

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
** 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);

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
** 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();

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
** 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) {

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
** 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));

View File

@ -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*/));
}

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

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)
continue; // skip whitespace-only lines
do_quit = server.invoke(line) ? false : true;
if (!server.invoke(line))
do_quit = true;
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
** 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)

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
** 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)

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>
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
*/

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
** 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__*/

View File

@ -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();
}