Skip to content

Commit 08c0d97

Browse files
committed
Improve CLI/logging behavior: show usage on no args, add configurable log levels, and silence tests by default
1 parent 457e3ca commit 08c0d97

8 files changed

Lines changed: 355 additions & 7 deletions

File tree

include/cli.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ struct Options
1313
bool version = false;
1414
bool recursive = false;
1515
bool hashes = false;
16+
bool quiet = false;
1617
bool output_set = false;
1718
bool extract_set = false;
19+
bool log_level_set = false;
1820
std::string output;
1921
std::string extract;
22+
std::string log_level;
2023
std::vector<std::string> dump;
2124
std::vector<std::string> plugins;
2225
std::vector<std::string> pe;

include/manacommons/color.h

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ namespace utils
4040
{
4141

4242
enum Color { RED, GREEN, YELLOW, RESET };
43+
enum class LogLevel { OFF = 0, ERROR = 1, WARNING = 2, INFO = 3, DEBUG = 4 };
4344

4445
/**
4546
* @brief Set the font in the terminal to the specified color.
@@ -67,8 +68,48 @@ DECLSPEC_MANACOMMONS std::ostream& print_colored_text(const std::string& text,
6768
const std::string& prefix = "",
6869
const std::string& suffix = "");
6970

70-
#define PRINT_ERROR utils::print_colored_text("!", utils::RED, std::cerr, "[", "] Error: ")
71-
#define PRINT_WARNING utils::print_colored_text("*", utils::YELLOW, std::cerr, "[", "] Warning: ")
71+
/**
72+
* @brief Set the global logging level.
73+
*/
74+
DECLSPEC_MANACOMMONS void set_log_level(LogLevel level);
75+
76+
/**
77+
* @brief Retrieve the global logging level.
78+
*/
79+
DECLSPEC_MANACOMMONS LogLevel get_log_level();
80+
81+
/**
82+
* @brief Returns whether a message with the given level should be emitted.
83+
*/
84+
DECLSPEC_MANACOMMONS bool should_log(LogLevel level);
85+
86+
/**
87+
* @brief Parse a logging level from text.
88+
*
89+
* Accepted values are: off, error, warning, info, debug.
90+
*/
91+
DECLSPEC_MANACOMMONS bool parse_log_level(const std::string& value, LogLevel& level);
92+
93+
/**
94+
* @brief Set log level from text.
95+
*
96+
* @return Whether parsing succeeded.
97+
*/
98+
DECLSPEC_MANACOMMONS bool set_log_level_from_string(const std::string& value);
99+
100+
/**
101+
* @brief Convert log level to text.
102+
*/
103+
DECLSPEC_MANACOMMONS const char* log_level_to_string(LogLevel level);
104+
105+
/**
106+
* @brief Streams used by logging macros.
107+
*/
108+
DECLSPEC_MANACOMMONS std::ostream& error_stream();
109+
DECLSPEC_MANACOMMONS std::ostream& warning_stream();
110+
111+
#define PRINT_ERROR utils::error_stream()
112+
#define PRINT_WARNING utils::warning_stream()
72113

73114

74115
#define LOG_CAP 100

manacommons/color.cpp

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,44 @@
1717

1818
#include "manacommons/color.h"
1919

20+
#include <algorithm>
21+
#include <atomic>
22+
#include <cctype>
23+
2024
namespace utils
2125
{
2226

27+
namespace {
28+
29+
class NullBuffer : public std::streambuf
30+
{
31+
public:
32+
int overflow(int c) override { return c; }
33+
};
34+
35+
std::ostream& null_stream()
36+
{
37+
static NullBuffer buffer;
38+
static std::ostream sink(&buffer);
39+
return sink;
40+
}
41+
42+
std::atomic<LogLevel>& global_log_level()
43+
{
44+
static std::atomic<LogLevel> level(LogLevel::WARNING);
45+
return level;
46+
}
47+
48+
std::string lowercase(std::string value)
49+
{
50+
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
51+
return static_cast<char>(std::tolower(c));
52+
});
53+
return value;
54+
}
55+
56+
} // namespace
57+
2358
#ifdef _WIN32
2459

2560
void set_color(Color c)
@@ -108,8 +143,98 @@ std::ostream& print_colored_text(const std::string& text,
108143
return sink << suffix;
109144
}
110145

146+
void set_log_level(LogLevel level)
147+
{
148+
global_log_level().store(level, std::memory_order_relaxed);
149+
}
150+
151+
LogLevel get_log_level()
152+
{
153+
return global_log_level().load(std::memory_order_relaxed);
154+
}
155+
156+
bool should_log(LogLevel level)
157+
{
158+
return static_cast<int>(get_log_level()) >= static_cast<int>(level);
159+
}
160+
161+
bool parse_log_level(const std::string& value, LogLevel& level)
162+
{
163+
const std::string normalized = lowercase(value);
164+
if (normalized == "off") {
165+
level = LogLevel::OFF;
166+
return true;
167+
}
168+
if (normalized == "error") {
169+
level = LogLevel::ERROR;
170+
return true;
171+
}
172+
if (normalized == "warning") {
173+
level = LogLevel::WARNING;
174+
return true;
175+
}
176+
if (normalized == "info") {
177+
level = LogLevel::INFO;
178+
return true;
179+
}
180+
if (normalized == "debug") {
181+
level = LogLevel::DEBUG;
182+
return true;
183+
}
184+
return false;
185+
}
186+
187+
bool set_log_level_from_string(const std::string& value)
188+
{
189+
LogLevel level;
190+
if (!parse_log_level(value, level)) {
191+
return false;
192+
}
193+
set_log_level(level);
194+
return true;
195+
}
196+
197+
const char* log_level_to_string(LogLevel level)
198+
{
199+
switch (level)
200+
{
201+
case LogLevel::OFF:
202+
return "off";
203+
case LogLevel::ERROR:
204+
return "error";
205+
case LogLevel::WARNING:
206+
return "warning";
207+
case LogLevel::INFO:
208+
return "info";
209+
case LogLevel::DEBUG:
210+
return "debug";
211+
default:
212+
return "warning";
213+
}
214+
}
215+
216+
std::ostream& error_stream()
217+
{
218+
if (!should_log(LogLevel::ERROR)) {
219+
return null_stream();
220+
}
221+
return print_colored_text("!", RED, std::cerr, "[", "] Error: ");
222+
}
223+
224+
std::ostream& warning_stream()
225+
{
226+
if (!should_log(LogLevel::WARNING)) {
227+
return null_stream();
228+
}
229+
return print_colored_text("*", YELLOW, std::cerr, "[", "] Warning: ");
230+
}
231+
111232
bool is_log_cap_reached()
112233
{
234+
if (!should_log(LogLevel::WARNING)) {
235+
return true;
236+
}
237+
113238
static unsigned int log_count = 0;
114239
if (++log_count < LOG_CAP) {
115240
return false;

src/cli.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ std::vector<std::string> reorder_args_for_cli11(int argc, char** argv)
4343
positionals.reserve(static_cast<size_t>(argc));
4444

4545
auto is_long_with_value = [](const std::string& name) {
46-
return name == "--output" || name == "--dump" || name == "--extract" || name == "--plugins";
46+
return name == "--output" || name == "--dump" || name == "--extract" || name == "--plugins" || name == "--log-level";
4747
};
4848

4949
bool end_of_options = false;
@@ -131,6 +131,7 @@ bool parse_args(Options& opts, int argc, char**argv, const HelpPrinter& help_pri
131131
->required()
132132
->type_name("FILE");
133133
app.add_flag("-r,--recursive", opts.recursive, "Scan all files in a directory (subdirectories will be ignored).");
134+
app.add_flag("-q,--quiet", opts.quiet, "Only display errors.");
134135
auto* output_opt = app.add_option("-o,--output", opts.output, "The output format. May be 'raw' (default) or 'json'.");
135136
auto* dump_opt = app.add_option("-d,--dump", opts.dump,
136137
"Dump PE information. Available choices are any combination of: "
@@ -146,6 +147,18 @@ bool parse_args(Options& opts, int argc, char**argv, const HelpPrinter& help_pri
146147
"Analyze the binary with additional plugins. (may slow down the analysis!)");
147148
plugins_opt->expected(1);
148149
plugins_opt->multi_option_policy(CLI::MultiOptionPolicy::TakeAll);
150+
auto* log_level_opt = app.add_option("--log-level", opts.log_level,
151+
"Set log verbosity. Accepted values: off, error, warning, info, debug.");
152+
153+
if (argc <= 1)
154+
{
155+
if (help_printer) {
156+
help_printer(app, argv[0]);
157+
} else {
158+
print_help_default(app, argv[0]);
159+
}
160+
return false;
161+
}
149162

150163
try
151164
{
@@ -175,9 +188,23 @@ bool parse_args(Options& opts, int argc, char**argv, const HelpPrinter& help_pri
175188

176189
opts.output_set = output_opt->count() > 0;
177190
opts.extract_set = extract_opt->count() > 0;
191+
opts.log_level_set = log_level_opt->count() > 0;
178192
opts.dump = split_comma_values(opts.dump);
179193
opts.plugins = split_comma_values(opts.plugins);
180194

195+
if (opts.quiet && !opts.log_level_set) {
196+
opts.log_level = "error";
197+
opts.log_level_set = true;
198+
}
199+
if (opts.log_level_set) {
200+
utils::LogLevel level = utils::LogLevel::WARNING;
201+
if (!utils::parse_log_level(opts.log_level, level)) {
202+
PRINT_ERROR << "Invalid log level \"" << opts.log_level
203+
<< "\". Expected one of: off, error, warning, info, debug." << std::endl;
204+
return false;
205+
}
206+
}
207+
181208
if (opts.version)
182209
{
183210
std::stringstream ss;

src/main.cpp

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <set>
2424
#include <sstream>
2525
#include <utility>
26+
#include <optional>
2627

2728
#include <filesystem>
2829

@@ -54,6 +55,44 @@
5455

5556
namespace bfs = std::filesystem;
5657

58+
namespace {
59+
60+
void apply_early_log_level_from_argv(int argc, char** argv)
61+
{
62+
std::optional<utils::LogLevel> selected;
63+
for (int i = 1; i < argc; ++i)
64+
{
65+
const std::string arg = argv[i];
66+
if (arg == "-q" || arg == "--quiet")
67+
{
68+
selected = utils::LogLevel::ERROR;
69+
continue;
70+
}
71+
72+
std::string value;
73+
if (arg.rfind("--log-level=", 0) == 0) {
74+
value = arg.substr(std::string("--log-level=").size());
75+
}
76+
else if (arg == "--log-level" && i + 1 < argc) {
77+
value = argv[++i];
78+
}
79+
80+
if (!value.empty())
81+
{
82+
utils::LogLevel level = utils::LogLevel::WARNING;
83+
if (utils::parse_log_level(value, level)) {
84+
selected = level;
85+
}
86+
}
87+
}
88+
89+
if (selected) {
90+
utils::set_log_level(*selected);
91+
}
92+
}
93+
94+
} // namespace
95+
5796
/**
5897
* @brief Prints the help message of the program.
5998
*
@@ -476,19 +515,23 @@ int main(int argc, char** argv)
476515
std::vector<std::string> selected_plugins, selected_categories;
477516

478517
mana::paths::initialize(argv[0]);
518+
apply_early_log_level_from_argv(argc, argv);
479519
const bfs::path config_dir(mana::paths::config_dir());
480520
const bfs::path plugin_dir(mana::paths::plugin_dir());
481521

482522
// Initialize Yara and load plugins.
483523
yara::Yara::initialize();
484524
plugin::PluginManager::get_instance().load_all(plugin_dir.string());
485525

486-
// Load the configuration
487-
config conf = parse_config((config_dir / "manalyze.conf").string());
488-
489526
if (!parse_args(opts, argc, argv, print_help, validate_args)) {
490527
return -1;
491528
}
529+
if (opts.log_level_set) {
530+
utils::set_log_level_from_string(opts.log_level);
531+
}
532+
533+
// Load the configuration
534+
config conf = parse_config((config_dir / "manalyze.conf").string());
492535

493536
// Get all the paths now and make them absolute before changing the working directory
494537
std::set<std::string> targets = get_input_files(opts);

0 commit comments

Comments
 (0)