Compare commits

...

1 Commits

Author SHA1 Message Date
Andri Yngvason 6556d893a8 Implement custom option parser 2023-01-01 09:10:31 +00:00
7 changed files with 537 additions and 145 deletions

View File

@ -17,26 +17,40 @@
#pragma once
#include <stdio.h>
#include <getopt.h>
#include <stdbool.h>
struct wv_option {
char short_opt;
const char* long_opt;
const char* schema;
const char* help;
const char* default_;
const char* positional;
bool is_subcommand;
};
struct wv_option_value {
const struct wv_option* option;
char value[256];
};
struct option_parser {
const struct wv_option* options;
char short_opts[128];
struct option long_opts[128];
int n_opts;
struct wv_option_value values[128];
int n_values;
int position;
int endpos;
};
#define OPTION_PARSER_CHECK_ALL_ARGUMENTS 0
#define OPTION_PARSER_STOP_ON_FIRST_NONOPTION 1
void option_parser_init(struct option_parser* self, const struct wv_option* options, unsigned flags);
void option_parser_init(struct option_parser* self,
const struct wv_option* options);
void option_parser_print_options(struct option_parser* self, FILE* stream);
int option_parser_getopt(struct option_parser* self, int argc, char* argv[]);
int option_parser_parse(struct option_parser* self, int argc,
const char* const* argv);
const char* option_parser_get_value(const struct option_parser* self,
const char* name);

View File

@ -19,6 +19,7 @@ c_args = [
'-DAML_UNSTABLE_API=1',
'-Wno-unused-parameter',
'-Wno-missing-field-initializers',
]
git = find_program('git', native: true, required: false)
@ -197,3 +198,7 @@ if scdoc.found()
)
endforeach
endif
if get_option('tests')
subdir('test')
endif

View File

@ -6,3 +6,5 @@ option('man-pages', type: 'feature', value: 'auto',
description: 'Generate and install man pages')
option('systemtap', type: 'boolean', value: false,
description: 'Enable tracing using sdt')
option('tests', type: 'boolean', value: false,
description: 'Build unit tests')

View File

@ -20,7 +20,6 @@
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <getopt.h>
#include <assert.h>
#include <inttypes.h>
#include <errno.h>
@ -1059,7 +1058,7 @@ static void on_nvnc_client_new(struct nvnc_client* client)
self->nr_clients);
}
void parse_keyboard_option(struct wayvnc* self, char* arg)
void parse_keyboard_option(struct wayvnc* self, const char* arg)
{
// Find optional variant, separated by -
char* index = strchr(arg, '-');
@ -1242,6 +1241,7 @@ int main(int argc, char* argv[])
const char* output_name = NULL;
const char* seat_name = NULL;
const char* socket_path = NULL;
const char* keyboard_options = NULL;
bool overlay_cursor = false;
bool show_performance = false;
@ -1253,6 +1253,8 @@ int main(int argc, char* argv[])
int log_level = NVNC_LOG_WARNING;
static const struct wv_option opts[] = {
{ .positional = "address" },
{ .positional = "port" },
{ 'C', "config", "<path>",
"Select a config file." },
{ 'g', "gpu", NULL,
@ -1268,7 +1270,8 @@ int main(int argc, char* argv[])
{ 'r', "render-cursor", NULL,
"Enable overlay cursor rendering." },
{ 'f', "max-fps", "<fps>",
"Set rate limit (default 30)." },
"Set rate limit (default 30).",
.default_ = "30" },
{ 'p', "performance", NULL,
"Show performance counters." },
{ 'u', "unix-socket", NULL,
@ -1280,84 +1283,53 @@ int main(int argc, char* argv[])
{ 'v', "verbose", NULL,
"Be more verbose. Same as setting --log-level=info" },
{ 'L', "log-level", "<level>",
"Set log level. The levels are: error, warning, info, debug trace and quiet." },
"Set log level. The levels are: error, warning, info, debug trace and quiet.",
.default_ = "warning" },
{ 'h', "help", NULL,
"Get help (this text)." },
{ '\0', NULL, NULL, NULL }
};
struct option_parser option_parser;
option_parser_init(&option_parser, opts,
OPTION_PARSER_CHECK_ALL_ARGUMENTS);
option_parser_init(&option_parser, opts);
while (1) {
int c = option_parser_getopt(&option_parser, argc, argv);
if (c < 0)
break;
if (option_parser_parse(&option_parser, argc,
(const char* const*)argv) < 0)
return wayvnc_usage(&option_parser, stderr, 1);
switch (c) {
case 'C':
cfg_file = optarg;
break;
case 'g':
enable_gpu_features = true;
break;
case 'o':
output_name = optarg;
break;
case 'k':
parse_keyboard_option(&self, optarg);
break;
case 's':
seat_name = optarg;
break;
case 'S':
socket_path = optarg;
break;
case 'r':
overlay_cursor = true;
break;
case 'f':
max_rate = atoi(optarg);
break;
case 'p':
show_performance = true;
break;
case 'u':
use_unix_socket = true;
break;
case 'd':
disable_input = true;
break;
case 'v':
log_level = NVNC_LOG_INFO;
break;
case 'L':
log_level = log_level_from_string(optarg);
if (log_level < 0) {
fprintf(stderr, "Invalid log level: %s\n",
optarg);
return wayvnc_usage(&option_parser, stderr, 1);
}
break;
case 'V':
return show_version();
case 'h':
return wayvnc_usage(&option_parser, stdout, 0);
default:
return wayvnc_usage(&option_parser, stderr, 1);
}
if (option_parser_get_value(&option_parser, "version")) {
return show_version();
}
if (option_parser_get_value(&option_parser, "help")) {
return wayvnc_usage(&option_parser, stdout, 0);
}
cfg_file = option_parser_get_value(&option_parser, "config");
enable_gpu_features = !!option_parser_get_value(&option_parser, "gpu");
output_name = option_parser_get_value(&option_parser, "output");
seat_name = option_parser_get_value(&option_parser, "seat");
socket_path = option_parser_get_value(&option_parser, "socket");
overlay_cursor = !!option_parser_get_value(&option_parser, "render-cursor");
show_performance = !!option_parser_get_value(&option_parser, "performance");
use_unix_socket = !!option_parser_get_value(&option_parser, "unix");
disable_input = !!option_parser_get_value(&option_parser, "disable-input");
log_level = option_parser_get_value(&option_parser, "verbose")
? NVNC_LOG_INFO : NVNC_LOG_WARNING;
log_level = log_level_from_string(
option_parser_get_value(&option_parser, "log-level"));
max_rate = atoi(option_parser_get_value(&option_parser, "max-fps"));
keyboard_options = option_parser_get_value(&option_parser, "keyboard");
if (keyboard_options)
parse_keyboard_option(&self, keyboard_options);
nvnc_set_log_level(log_level);
int n_args = argc - optind;
if (n_args >= 1)
address = argv[optind];
if (n_args >= 2)
port = atoi(argv[optind + 1]);
address = option_parser_get_value(&option_parser, "address");
const char* port_str = option_parser_get_value(&option_parser, "port");
if (port_str)
port = atoi(port_str);
if (seat_name && disable_input) {
nvnc_log(NVNC_LOG_ERROR, "seat and disable-input are conflicting options");

View File

@ -15,53 +15,30 @@
*/
#include "option-parser.h"
#include "strlcpy.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
static int count_options(const struct wv_option* opts)
{
int n = 0;
while (opts[n].short_opt || opts[n].long_opt)
while (opts[n].short_opt || opts[n].long_opt || opts[n].positional)
n++;
return n;
}
void option_parser_init(struct option_parser* self,
const struct wv_option* options,
unsigned flags)
const struct wv_option* options)
{
memset(self, 0, sizeof(*self));
self->options = options;
self->n_opts = count_options(options);
int short_opt_index = 0;
int long_opt_index = 0;
if (flags && OPTION_PARSER_STOP_ON_FIRST_NONOPTION)
self->short_opts[short_opt_index++] = '+';
for (int i = 0; i < self->n_opts; ++i) {
assert(options[i].short_opt); // TODO: Make this optional?
self->short_opts[short_opt_index++] = options[i].short_opt;
if (options[i].schema)
self->short_opts[short_opt_index++] = ':';
if (!options[i].long_opt)
continue;
const struct wv_option* src_opt = &options[i];
struct option* dst_opt = &self->long_opts[long_opt_index++];
dst_opt->val = src_opt->short_opt;
dst_opt->name = src_opt->long_opt;
dst_opt->has_arg = src_opt->schema ?
required_argument : no_argument;
}
}
static int get_left_col_width(const struct wv_option* opts, int n)
@ -129,7 +106,8 @@ static void reflow_text(char* dst, const char* src, int width)
static void format_option(const struct wv_option* opt, int left_col_width,
FILE* stream)
{
assert(opt->help);
if (!opt->help)
return;
int n_chars = fprintf(stream, " ");
if (opt->short_opt)
@ -171,7 +149,223 @@ void option_parser_print_options(struct option_parser* self, FILE* stream)
}
}
int option_parser_getopt(struct option_parser* self, int argc, char* argv[])
static const struct wv_option* find_long_option(
const struct option_parser* self, const char* name)
{
return getopt_long(argc, argv, self->short_opts, self->long_opts, NULL);
for (int i = 0; i < self->n_opts; ++i) {
if (!self->options[i].long_opt)
continue;
if (strcmp(self->options[i].long_opt, name) == 0)
return &self->options[i];
}
return NULL;
}
static const struct wv_option* find_short_option(
const struct option_parser* self, char name)
{
for (int i = 0; i < self->n_opts; ++i)
if (self->options[i].short_opt == name)
return &self->options[i];
return NULL;
}
static const struct wv_option* find_positional_option(
struct option_parser* self, int position)
{
int current_pos = 0;
for (int i = 0; i < self->n_opts; ++i) {
if (!self->options[i].positional)
continue;
if (current_pos == position)
return &self->options[i];
current_pos += 1;
}
return NULL;
}
static int append_value(struct option_parser* self,
const struct wv_option* option, const char* value)
{
if ((size_t)self->n_values >= ARRAY_SIZE(self->values)) {
fprintf(stderr, "ERROR: Too many arguments!\n");
return -1;
}
struct wv_option_value* dst = &self->values[self->n_values++];
dst->option = option;
strlcpy(dst->value, value, sizeof(dst->value));
return 0;
}
static int parse_long_arg(struct option_parser* self, int argc,
const char* const* argv, int index)
{
int count = 1;
char name[256];
strlcpy(name, argv[index] + 2, sizeof(name));
char* eq = strchr(name, '=');
if (eq)
*eq = '\0';
const struct wv_option* opt = find_long_option(self, name);
if (!opt) {
fprintf(stderr, "ERROR: Unknown option: \"%s\"\n", name);
return -1;
}
const char* value = "1";
if (opt->schema) {
if (eq) {
value = eq + 1;
} else {
if (index + 1 >= argc) {
fprintf(stderr, "ERROR: An argument is required for the \"%s\" option\n",
opt->long_opt);
return -1;
}
value = argv[index + 1];
count += 1;
}
}
if (append_value(self, opt, value) < 0)
return -1;
return count;
}
static int parse_short_args(struct option_parser* self, char argc,
const char* const* argv, int index)
{
int count = 1;
int len = strlen(argv[index]);
for (int i = 1; i < len; ++i) {
char name = argv[index][i];
const struct wv_option* opt = find_short_option(self, name);
if (!opt) {
fprintf(stderr, "ERROR: Unknown option: \"%c\"\n", name);
return -1;
}
const char* value = "1";
if (opt->schema) {
const char* tail = argv[index] + i + 1;
if (tail[0] == '=') {
value = tail + 1;
} else if (tail[0]) {
value = tail;
} else {
if (index + 1 >= argc) {
fprintf(stderr, "ERROR: An argument is required for the \"%c\" option\n",
opt->short_opt);
return -1;
}
value = argv[index + 1];
count += 1;
}
}
if (append_value(self, opt, value) < 0)
return -1;
if (opt->schema)
break;
}
return count;
}
static int parse_positional_arg(struct option_parser* self, char argc,
const char* const* argv, int i)
{
const struct wv_option* opt = find_positional_option(self, self->position);
if (!opt)
return 1;
if (append_value(self, opt, argv[i]) < 0)
return -1;
self->position += 1;
return opt->is_subcommand ? 0 : 1;
}
int option_parser_parse(struct option_parser* self, int argc,
const char* const* argv)
{
int i = 1;
while (i < argc) {
if (argv[i][0] == '-') {
if (argv[i][1] == '-') {
if (argv[i][2] == '\0')
return 0;
int rc = parse_long_arg(self, argc, argv, i);
if (rc < 0)
return -1;
i += rc;
} else {
int rc = parse_short_args(self, argc, argv, i);
if (rc < 0)
return -1;
i += rc;
}
} else {
int rc = parse_positional_arg(self, argc, argv, i);
if (rc < 0)
return -1;
if (rc == 0)
break;
i += rc;
}
}
self->endpos = i;
return 0;
}
const char* option_parser_get_value(const struct option_parser* self,
const char* name)
{
const struct wv_option* opt;
bool is_short = name[0] && !name[1];
for (int i = 0; i < self->n_values; ++i) {
const struct wv_option_value* value = &self->values[i];
opt = value->option;
if (is_short) {
if (opt->short_opt && opt->short_opt == *name)
return value->value;
} else {
if (opt->long_opt && strcmp(opt->long_opt, name) == 0)
return value->value;
}
if (opt->positional && strcmp(opt->positional, name) == 0)
return value->value;
}
if (is_short) {
opt = find_short_option(self, name[0]);
if (opt)
return opt->default_;
} else {
opt = find_long_option(self, name);
if (opt)
return opt->default_;
}
// TODO: Add positional option?
return NULL;
}

View File

@ -20,7 +20,6 @@
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <getopt.h>
#include <assert.h>
#include <inttypes.h>
#include <errno.h>
@ -71,6 +70,8 @@ int main(int argc, char* argv[])
unsigned flags = 0;
static const struct wv_option opts[] = {
{ .positional = "command",
.is_subcommand = true },
{ 'S', "socket", "<path>",
"Control socket path." },
{ 'w', "wait", NULL,
@ -85,46 +86,36 @@ int main(int argc, char* argv[])
"Be more verbose." },
{ 'h', "help", NULL,
"Get help (this text)." },
{ '\0', NULL, NULL, NULL }
{ }
};
struct option_parser option_parser;
option_parser_init(&option_parser, opts,
OPTION_PARSER_STOP_ON_FIRST_NONOPTION);
while (1) {
int c = option_parser_getopt(&option_parser, argc, argv);
if (c < 0)
break;
switch (c) {
case 'S':
socket_path = optarg;
break;
case 'w':
flags |= CTL_CLIENT_SOCKET_WAIT;
break;
case 'r':
flags |= CTL_CLIENT_RECONNECT;
break;
case 'j':
flags |= CTL_CLIENT_PRINT_JSON;
break;
case 'v':
verbose = true;
break;
case 'V':
return show_version();
case 'h':
return wayvncctl_usage(stdout, &option_parser, 0);
default:
return wayvncctl_usage(stderr, &option_parser, 1);
}
}
argc -= optind;
if (argc <= 0)
option_parser_init(&option_parser, opts);
if (option_parser_parse(&option_parser, argc,
(const char* const*)argv) < 0)
return wayvncctl_usage(stderr, &option_parser, 1);
argv = &argv[optind];
if (option_parser_get_value(&option_parser, "help"))
return wayvncctl_usage(stdout, &option_parser, 0);
if (option_parser_get_value(&option_parser, "version"))
return show_version();
socket_path = option_parser_get_value(&option_parser, "socket");
flags |= option_parser_get_value(&option_parser, "wait")
? CTL_CLIENT_SOCKET_WAIT : 0;
flags |= option_parser_get_value(&option_parser, "reconnect")
? CTL_CLIENT_RECONNECT : 0;
flags |= option_parser_get_value(&option_parser, "json")
? CTL_CLIENT_PRINT_JSON : 0;
verbose = !!option_parser_get_value(&option_parser, "verbose");
// No command; nothing to do...
if (!option_parser_get_value(&option_parser, "command"))
return 0;
argc -= option_parser.endpos;
argv += option_parser.endpos;
ctl_client_debug_log(verbose);

View File

@ -0,0 +1,214 @@
#include "tst.h"
#include "option-parser.h"
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
static const struct wv_option options[] = {
{ .positional = "first" },
{ .positional = "second" },
{ .positional = "third" },
{ .positional = "command", .is_subcommand = true },
{ 'a', "option-a", NULL, "Description of a" },
{ 'b', "option-b", NULL, "Description of b" },
{ 'v', "value-option", "value", "Description of v" },
{ },
};
static int test_simple(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = {
"executable",
"-a",
"-b",
"pos 1",
"pos 2",
};
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_STR_EQ("pos 1", option_parser_get_value(&parser, "first"));
ASSERT_STR_EQ("pos 2", option_parser_get_value(&parser, "second"));
ASSERT_FALSE(option_parser_get_value(&parser, "third"));
ASSERT_TRUE(option_parser_get_value(&parser, "a"));
ASSERT_TRUE(option_parser_get_value(&parser, "option-b"));
ASSERT_FALSE(option_parser_get_value(&parser, "value-option"));
return 0;
}
static int test_short_value_option_with_space(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-v", "value" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_STR_EQ("value", option_parser_get_value(&parser, "value-option"));
return 0;
}
static int test_short_value_option_without_space(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-vvalue" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_STR_EQ("value", option_parser_get_value(&parser, "value-option"));
return 0;
}
static int test_short_value_option_with_eq(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-v=value" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_STR_EQ("value", option_parser_get_value(&parser, "value-option"));
return 0;
}
static int test_long_value_option_with_space(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "--value-option", "value" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_STR_EQ("value", option_parser_get_value(&parser, "value-option"));
return 0;
}
static int test_long_value_option_without_space(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "--value-option=value" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_STR_EQ("value", option_parser_get_value(&parser, "value-option"));
return 0;
}
static int test_multi_short_option(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-ab" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_TRUE(option_parser_get_value(&parser, "a"));
ASSERT_TRUE(option_parser_get_value(&parser, "b"));
return 0;
}
static int test_multi_short_option_with_value(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-abvthe-value" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_TRUE(option_parser_get_value(&parser, "a"));
ASSERT_TRUE(option_parser_get_value(&parser, "b"));
ASSERT_STR_EQ("the-value", option_parser_get_value(&parser, "v"));
return 0;
}
static int test_stop(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "exec", "-a", "--", "-b"};
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_TRUE(option_parser_get_value(&parser, "a"));
ASSERT_FALSE(option_parser_get_value(&parser, "b"));
return 0;
}
static int test_unknown_short_option(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-x" };
ASSERT_INT_EQ(-1, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
return 0;
}
static int test_unknown_long_option(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "--an-unknown-long-option" };
ASSERT_INT_EQ(-1, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
return 0;
}
static int test_missing_short_value(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-v" };
ASSERT_INT_EQ(-1, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
return 0;
}
static int test_missing_long_value(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "--value-option" };
ASSERT_INT_EQ(-1, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
return 0;
}
static int test_subcommand_without_arguments(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-ab", "first", "second", "third",
"do-stuff" };
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_INT_EQ(5, parser.endpos);
ASSERT_STR_EQ("do-stuff", option_parser_get_value(&parser, "command"));
return 0;
}
static int test_subcommand_with_arguments(void)
{
struct option_parser parser;
option_parser_init(&parser, options);
const char* argv[] = { "executable", "-ab", "first", "second", "third",
"do-stuff", "--some-option", "another-argument"};
ASSERT_INT_EQ(0, option_parser_parse(&parser, ARRAY_SIZE(argv), argv));
ASSERT_INT_EQ(5, parser.endpos);
ASSERT_STR_EQ("do-stuff", option_parser_get_value(&parser, "command"));
return 0;
}
int main()
{
int r = 0;
RUN_TEST(test_simple);
RUN_TEST(test_short_value_option_with_space);
RUN_TEST(test_short_value_option_without_space);
RUN_TEST(test_short_value_option_with_eq);
RUN_TEST(test_long_value_option_with_space);
RUN_TEST(test_long_value_option_without_space);
RUN_TEST(test_multi_short_option);
RUN_TEST(test_multi_short_option_with_value);
RUN_TEST(test_stop);
RUN_TEST(test_unknown_short_option);
RUN_TEST(test_unknown_long_option);
RUN_TEST(test_missing_short_value);
RUN_TEST(test_missing_long_value);
RUN_TEST(test_subcommand_without_arguments);
RUN_TEST(test_subcommand_with_arguments);
return r;
}