diff --git a/include/aws/common/command_line_parser.h b/include/aws/common/command_line_parser.h new file mode 100644 index 000000000..ca3a267e5 --- /dev/null +++ b/include/aws/common/command_line_parser.h @@ -0,0 +1,66 @@ +#ifndef AWS_COMMAND_LINE_PARSER_H +#define AWS_COMMAND_LINE_PARSER_H +/* + * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +#include + +enum aws_cli_options_has_arg { + AWS_CLI_OPTIONS_NO_ARGUMENT = 0, + AWS_CLI_OPTIONS_REQUIRED_ARGUMENT = 1, + AWS_CLI_OPTIONS_OPTIONAL_ARGUMENT = 2, +}; + +struct aws_cli_option { + const char *name; + enum aws_cli_options_has_arg has_arg; + int *flag; + int val; +}; + +/** + * Initialized to 1 (for where the first argument would be). As arguments are parsed, this number is the index + * of the next argument to parse. Reset this to 1 to parse another set of arguments, or to rerun the parser. + */ +AWS_COMMON_API extern int aws_cli_optind; + +/** + * If an option has an argument, when the option is encountered, this will be set to the argument portion. + */ +AWS_COMMON_API extern const char *aws_cli_optarg; + +AWS_EXTERN_C_BEGIN +/** + * A mostly compliant implementation of posix getopt_long(). Parses command-line arguments. argc is the number of + * command line arguments passed in argv. optstring contains the legitimate option characters. The option characters + * coorespond to aws_cli_option::val. If the character is followed by a :, the option requires an argument. If it is + * followed by '::', the argument is optional (not implemented yet). + * + * longopts, is an array of struct aws_cli_option. These are the allowed options for the program. + * The last member of the array must be zero initialized. + * + * If longindex is non-null, it will be set to the index in longopts, for the found option. + * + * Returns option val if it was found, '?' if an option was encountered that was not specified in the option string, + * returns -1 when all arguments that can be parsed have been parsed. + */ +AWS_COMMON_API int aws_cli_getopt_long( + int argc, + char *const argv[], + const char *optstring, + const struct aws_cli_option *longopts, + int *longindex); +AWS_EXTERN_C_END + +#endif /* AWS_COMMAND_LINE_PARSER_H */ diff --git a/source/command_line_parser.c b/source/command_line_parser.c new file mode 100644 index 000000000..26492b7cb --- /dev/null +++ b/source/command_line_parser.c @@ -0,0 +1,120 @@ +/* + * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +#include + +int aws_cli_optind = 1; +int aws_cli_opterr = -1; +int aws_cli_optopt = 0; + +const char *aws_cli_optarg = NULL; + +static const struct aws_cli_option *s_find_option_from_char( + const struct aws_cli_option *longopts, + char search_for, + int *longindex) { + int index = 0; + const struct aws_cli_option *option = &longopts[index]; + + while (option->val != 0 || option->name) { + if (option->val == search_for) { + if (longindex) { + *longindex = index; + } + return option; + } + + option = &longopts[++index]; + } + + return NULL; +} + +static const struct aws_cli_option *s_find_option_from_c_str( + const struct aws_cli_option *longopts, + const char *search_for, + int *longindex) { + int index = 0; + const struct aws_cli_option *option = &longopts[index]; + + while (option->name || option->val != 0) { + if (option->name) { + if (option->name && !strcmp(search_for, option->name)) { + if (longindex) { + *longindex = index; + } + return option; + } + } + + option = &longopts[++index]; + } + + return NULL; +} + +int aws_cli_getopt_long( + int argc, + char *const argv[], + const char *optstring, + const struct aws_cli_option *longopts, + int *longindex) { + aws_cli_optarg = NULL; + + if (aws_cli_optind >= argc) { + return -1; + } + + char first_char = argv[aws_cli_optind][0]; + char second_char = argv[aws_cli_optind][1]; + char *option_start = NULL; + const struct aws_cli_option *option = NULL; + + if (first_char == '-' && second_char != '-') { + option_start = &argv[aws_cli_optind][1]; + option = s_find_option_from_char(longopts, *option_start, longindex); + } else if (first_char == '-' && second_char == '-') { + option_start = &argv[aws_cli_optind][2]; + option = s_find_option_from_c_str(longopts, option_start, longindex); + } else { + return -1; + } + + aws_cli_optind++; + if (option) { + bool has_arg = false; + if (option) { + char *opt_value = memchr(optstring, option->val, strlen(optstring)); + if (!opt_value) { + return '?'; + } + + if (opt_value[1] == ':') { + has_arg = true; + } + } + + if (has_arg) { + if (aws_cli_optind >= argc - 1) { + return '?'; + } + + aws_cli_optarg = argv[aws_cli_optind++]; + } + + return option->val; + } + + return '?'; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 757abe222..bc8f747a0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -264,6 +264,11 @@ add_test_case(uuid_string_parse_malformed) add_test_case(test_environment_functions) +add_test_case(short_argument_parse) +add_test_case(long_argument_parse) +add_test_case(unqualified_argument_parse) +add_test_case(unknown_argument_parse) + generate_test_driver(${CMAKE_PROJECT_NAME}-tests) if (NOT MSVC) diff --git a/tests/command_line_parser_test.c b/tests/command_line_parser_test.c new file mode 100644 index 000000000..b0dad54ee --- /dev/null +++ b/tests/command_line_parser_test.c @@ -0,0 +1,163 @@ +/* + * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +#include +#include + +static int s_test_short_argument_parse_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + struct aws_cli_option options[] = { + {.name = NULL, .has_arg = AWS_CLI_OPTIONS_NO_ARGUMENT, .flag = NULL, .val = 'a'}, + {.name = "beeee", .has_arg = AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, .flag = NULL, .val = 'b'}, + {.name = NULL, .has_arg = AWS_CLI_OPTIONS_OPTIONAL_ARGUMENT, .flag = NULL, .val = 'c'}, + {.name = NULL, .has_arg = 0, .flag = NULL, .val = 0}, + }; + + char *const args[] = { + "prog-name", + "-a", + "-b", + "bval", + "-c", + }; + int argc = 5; + int longindex = 0; + int arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('a', arg); + ASSERT_INT_EQUALS(0, longindex); + ASSERT_INT_EQUALS(2, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('b', arg); + ASSERT_STR_EQUALS("bval", aws_cli_optarg); + ASSERT_INT_EQUALS(1, longindex); + ASSERT_INT_EQUALS(4, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('c', arg); + ASSERT_INT_EQUALS(2, longindex); + ASSERT_INT_EQUALS(-1, aws_cli_getopt_long(argc, args, "ab:c", options, &longindex)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(short_argument_parse, s_test_short_argument_parse_fn) + +static int s_test_long_argument_parse_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + struct aws_cli_option options[] = { + {.name = "aaee", .has_arg = AWS_CLI_OPTIONS_NO_ARGUMENT, .flag = NULL, .val = 'a'}, + {.name = "beeee", .has_arg = AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, .flag = NULL, .val = 'b'}, + {.name = "cceeee", .has_arg = AWS_CLI_OPTIONS_OPTIONAL_ARGUMENT, .flag = NULL, .val = 'c'}, + {.name = NULL, .has_arg = 0, .flag = NULL, .val = 0}, + }; + + char *const args[] = { + "prog-name", + "--aaee", + "--beeee", + "bval", + "-cceeee", + }; + int argc = 5; + int longindex = 0; + int arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('a', arg); + ASSERT_INT_EQUALS(0, longindex); + ASSERT_INT_EQUALS(2, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('b', arg); + ASSERT_STR_EQUALS("bval", aws_cli_optarg); + ASSERT_INT_EQUALS(1, longindex); + ASSERT_INT_EQUALS(4, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('c', arg); + ASSERT_INT_EQUALS(2, longindex); + + ASSERT_INT_EQUALS(-1, aws_cli_getopt_long(argc, args, "ab:c", options, &longindex)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(long_argument_parse, s_test_long_argument_parse_fn) + +static int s_test_unqualified_argument_parse_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + struct aws_cli_option options[] = { + {.name = "aaee", .has_arg = AWS_CLI_OPTIONS_NO_ARGUMENT, .flag = NULL, .val = 'a'}, + {.name = "beeee", .has_arg = AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, .flag = NULL, .val = 'b'}, + {.name = "cceeee", .has_arg = AWS_CLI_OPTIONS_OPTIONAL_ARGUMENT, .flag = NULL, .val = 'c'}, + {.name = NULL, .has_arg = 0, .flag = NULL, .val = 0}, + }; + + char *const args[] = {"prog-name", "-a", "--beeee", "bval", "-c", "operand"}; + int argc = 6; + int longindex = 0; + int arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('a', arg); + ASSERT_INT_EQUALS(0, longindex); + ASSERT_INT_EQUALS(2, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('b', arg); + ASSERT_STR_EQUALS("bval", aws_cli_optarg); + ASSERT_INT_EQUALS(1, longindex); + ASSERT_INT_EQUALS(4, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('c', arg); + ASSERT_INT_EQUALS(2, longindex); + + ASSERT_INT_EQUALS(-1, aws_cli_getopt_long(argc, args, "ab:c", options, &longindex)); + ASSERT_TRUE(aws_cli_optind < argc); + ASSERT_STR_EQUALS("operand", args[aws_cli_optind++]); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(unqualified_argument_parse, s_test_unqualified_argument_parse_fn) + +static int s_test_unknown_argument_parse_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + struct aws_cli_option options[] = { + {.name = "aaee", .has_arg = AWS_CLI_OPTIONS_NO_ARGUMENT, .flag = NULL, .val = 'a'}, + {.name = "beeee", .has_arg = AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, .flag = NULL, .val = 'b'}, + {.name = "cceeee", .has_arg = AWS_CLI_OPTIONS_OPTIONAL_ARGUMENT, .flag = NULL, .val = 'c'}, + {.name = NULL, .has_arg = 0, .flag = NULL, .val = 0}, + }; + + char *const args[] = {"prog-name", "-BOO!", "--beeee", "bval", "-c", "operand"}; + int argc = 6; + int longindex = 0; + int arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('?', arg); + ASSERT_INT_EQUALS(0, longindex); + ASSERT_INT_EQUALS(2, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('b', arg); + ASSERT_STR_EQUALS("bval", aws_cli_optarg); + ASSERT_INT_EQUALS(1, longindex); + ASSERT_INT_EQUALS(4, aws_cli_optind); + arg = aws_cli_getopt_long(argc, args, "ab:c", options, &longindex); + ASSERT_INT_EQUALS('c', arg); + ASSERT_INT_EQUALS(2, longindex); + + ASSERT_INT_EQUALS(-1, aws_cli_getopt_long(argc, args, "ab:c", options, &longindex)); + ASSERT_TRUE(aws_cli_optind < argc); + ASSERT_STR_EQUALS("operand", args[aws_cli_optind++]); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(unknown_argument_parse, s_test_unknown_argument_parse_fn)