feat(cli): add command-line argument parser

This commit is contained in:
2026-06-18 21:02:32 -05:00
parent aa09ef2a83
commit 0c675d6fbb
5 changed files with 144 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ add_library(iZo::izo ALIAS izo)
target_sources(izo
PRIVATE
src/command_line.cpp
src/dialogs.cpp
src/directory_watcher.cpp
src/time.cpp
@@ -89,6 +90,7 @@ if(IZO_BUILD_TESTS)
set_tests_properties(${name} PROPERTIES TIMEOUT 10)
endfunction()
izo_add_test(environment_paths)
izo_add_test(command_line)
izo_add_test(system_time_debug)
izo_add_test(dynamic_library)
izo_add_test(process)

View File

@@ -0,0 +1,39 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace izo {
class CommandLine {
public:
CommandLine(int argc, const char* const argv[]);
CommandLine(int argc, char* argv[]);
// Has reports whether an option was supplied, including --name=value.
bool Has(std::string_view name) const noexcept;
// Get returns the last value supplied as --name value or --name=value.
// A flag without a value and a missing option both return std::nullopt.
std::optional<std::string> Get(std::string_view name) const;
std::string Get(std::string_view name, std::string_view fallback) const;
int GetInt(std::string_view name, int fallback) const;
// Arguments not consumed as option values. argv[0] is never included.
const std::vector<std::string>& Positionals() const noexcept { return positionals_; }
private:
struct Option {
std::string name;
std::optional<std::string> value;
};
void Parse(int argc, const char* const argv[]);
std::vector<Option> options_;
std::vector<std::string> positionals_;
};
} // namespace izo

View File

@@ -1,6 +1,7 @@
#pragma once
#include <izo/Clipboard.hpp>
#include <izo/CommandLine.hpp>
#include <izo/Debug.hpp>
#include <izo/Dialogs.hpp>
#include <izo/DirectoryWatcher.hpp>

68
src/command_line.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include <izo/CommandLine.hpp>
#include <charconv>
#include <utility>
namespace izo {
CommandLine::CommandLine(int argc, const char* const argv[]) { Parse(argc, argv); }
CommandLine::CommandLine(int argc, char* argv[]) {
std::vector<const char*> arguments;
if (argc > 0) arguments.reserve(static_cast<std::size_t>(argc));
for (int index = 0; index < argc; ++index) arguments.push_back(argv ? argv[index] : nullptr);
Parse(argc, arguments.data());
}
void CommandLine::Parse(int argc, const char* const argv[]) {
bool options_ended = false;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv && argv[index] ? argv[index] : "";
if (!options_ended && argument == "--") {
options_ended = true;
continue;
}
if (!options_ended && argument.size() > 2 && argument.compare(0, 2, "--") == 0) {
const auto equals = argument.find('=');
Option option;
option.name = argument.substr(0, equals);
if (equals != std::string::npos) {
option.value = argument.substr(equals + 1);
} else if (index + 1 < argc && argv[index + 1] &&
std::string_view(argv[index + 1]).compare(0, 2, "--") != 0) {
option.value = argv[++index];
}
options_.push_back(std::move(option));
} else {
positionals_.push_back(argument);
}
}
}
bool CommandLine::Has(std::string_view name) const noexcept {
for (const auto& option : options_)
if (option.name == name) return true;
return false;
}
std::optional<std::string> CommandLine::Get(std::string_view name) const {
for (auto option = options_.rbegin(); option != options_.rend(); ++option)
if (option->name == name) return option->value;
return std::nullopt;
}
std::string CommandLine::Get(std::string_view name, std::string_view fallback) const {
const auto value = Get(name);
return value ? *value : std::string(fallback);
}
int CommandLine::GetInt(std::string_view name, int fallback) const {
const auto value = Get(name);
if (!value || value->empty()) return fallback;
int parsed = 0;
const auto result = std::from_chars(value->data(), value->data() + value->size(), parsed);
return result.ec == std::errc{} && result.ptr == value->data() + value->size()
? parsed : fallback;
}
} // namespace izo

34
tests/command_line.cpp Normal file
View File

@@ -0,0 +1,34 @@
#include "test_support.hpp"
#include <izo/CommandLine.hpp>
int main() {
const char* arguments[] = {
"program", "--verbose", "--config", "settings.json", "input.txt",
"--threads=8", "--threads", "12", "--invalid", "many", "--empty=",
"--", "--literal", "output.txt",
};
izo::CommandLine args(static_cast<int>(sizeof(arguments) / sizeof(arguments[0])), arguments);
CHECK(args.Has("--verbose"));
CHECK(!args.Has("--missing"));
CHECK(!args.Get("--verbose"));
CHECK(args.Get("--config") == "settings.json");
CHECK(args.Get("--missing", "fallback") == "fallback");
CHECK(args.GetInt("--threads", 4) == 12);
CHECK(args.GetInt("--invalid", 4) == 4);
CHECK(args.GetInt("--empty", 4) == 4);
CHECK(args.Get("--empty") == "");
CHECK(args.Positionals().size() == 3);
CHECK(args.Positionals()[0] == "input.txt");
CHECK(args.Positionals()[1] == "--literal");
CHECK(args.Positionals()[2] == "output.txt");
const char* negative[] = {"program", "--count", "-2"};
CHECK(izo::CommandLine(3, negative).GetInt("--count", 0) == -2);
char program[] = "program";
char flag[] = "--flag";
char* mutable_arguments[] = {program, flag};
CHECK(izo::CommandLine(2, mutable_arguments).Has("--flag"));
}