diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 7ab1b4d..e11816b 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -4,7 +4,7 @@ name: CMake on multiple platforms on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] pull_request: branches: [ "main" ] diff --git a/CMakeLists.txt b/CMakeLists.txt index f80b9c6..76a8726 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.26) set(CMAKE_COMPILE_WARNING_AS_ERROR ON) project(akoc LANGUAGES C - VERSION 0.1.0 + VERSION 1.0.0 DESCRIPTION "A C implementation of the Ako config language" HOMEPAGE_URL "https://github.com/Tuyuji/AkoC" ) @@ -18,7 +18,10 @@ add_library(akoc src/ako.c src/mem/dyn_string.c src/lex/parser.c) -target_include_directories(akoc PUBLIC include) +target_include_directories(akoc PUBLIC + $ + $ +) #Test project(akotest C) @@ -30,4 +33,48 @@ enable_testing() add_test(NAME akotest COMMAND $ -) \ No newline at end of file +) + +# A bunch of small cli tools +add_subdirectory(utils) + +include(GNUInstallDirs) + +install(TARGETS akoc + EXPORT akocTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install(DIRECTORY include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h" +) + +install(EXPORT akocTargets + FILE akocTargets.cmake + NAMESPACE akoc:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/akoc +) + +include(CMakePackageConfigHelpers) + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/akocConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/akocConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/akoc +) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/akocConfigVersion.cmake" + VERSION ${akoc_VERSION} + COMPATIBILITY SameMajorVersion +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/akocConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/akocConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/akoc +) diff --git a/README.md b/README.md index 01605a1..a5e2db0 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,18 @@ int main() return 1; } + //You can get elements like this: ako_elem_t* win = ako_elem_table_get(root, "window"); ako_elem_t* size = ako_elem_table_get(win, "size"); ako_int w = ako_elem_get_int(ako_elem_array_get(size, 0)); ako_int h = ako_elem_get_int(ako_elem_array_get(size, 1)); + //Or like this, you should make sure ako_elem_get isn't null as + //ako_elem_get_int will assert if you pass it a NULL ptr. + w = ako_elem_get_int(ako_elem_get(root, "window.size.0")); + h = ako_elem_get_int(ako_elem_get(root, "window.size.1")); + printf("Window size: %d x %d\n", w, h); ako_elem_table_add(win, "title", ako_elem_create_string("Hello, world")); diff --git a/cmake/akocConfig.cmake.in b/cmake/akocConfig.cmake.in new file mode 100644 index 0000000..0f9f27e --- /dev/null +++ b/cmake/akocConfig.cmake.in @@ -0,0 +1,9 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# Include the exported targets +include("${CMAKE_CURRENT_LIST_DIR}/akocTargets.cmake") + +# Check that all required components are found +check_required_components(akoc) \ No newline at end of file diff --git a/include/ako/ako.h b/include/ako/ako.h index e742e88..69c44e4 100644 --- a/include/ako/ako.h +++ b/include/ako/ako.h @@ -4,6 +4,7 @@ #define AKO_VMAJOR 0 #define AKO_VMINOR 1 #define AKO_VPATCH 0 +#define AKO_VERSION_STR "0.1.0" #include diff --git a/include/ako/ako.hpp b/include/ako/ako.hpp index ceaa6a2..4d05852 100644 --- a/include/ako/ako.hpp +++ b/include/ako/ako.hpp @@ -2,7 +2,8 @@ // SPDX-License-Identifier: MIT #pragma once -extern "C" { +extern "C" +{ #include "ako.h" #include "elem.h" #include "types.h" diff --git a/include/ako/elem.h b/include/ako/elem.h index bed2026..cbecd0c 100644 --- a/include/ako/elem.h +++ b/include/ako/elem.h @@ -72,4 +72,10 @@ ako_elem_t* ako_elem_create_string(const char* str); ako_elem_t* ako_elem_create_shorttype(const char* str); ako_elem_t* ako_elem_create_bool(bool value); ako_elem_t* ako_elem_create_error(const char* error); -ako_elem_t* ako_elem_create_errorf(const char* fmt, ...); \ No newline at end of file +ako_elem_t* ako_elem_create_errorf(const char* fmt, ...); + +// Accepts a path in ako format: e.g "song.artist" +// Returns the found element or NULL if not found +// If an error occurs, it will return an error element +// If ignore_error is true, it will return NULL if an error occurs +ako_elem_t* ako_elem_get(ako_elem_t* root, const char* path); \ No newline at end of file diff --git a/src/ako.c b/src/ako.c index 0b1f706..8d636aa 100644 --- a/src/ako.c +++ b/src/ako.c @@ -48,11 +48,23 @@ void ako_free_string(const char* str) ako_elem_t* ako_parse(const char* source) { - static char* err = NULL; - dyn_array_t tokens = ako_tokenize(source, &err); + if (source == NULL || strlen(source) == 0) + { + return NULL; + } + + ako_elem_t* err = NULL; + dyn_array_t tokens = ako_tokenize(source, &err, false); + if (err != NULL) + { + // uh oh + return err; + } + if (tokens.size == 0) { - return ako_elem_create_error(err); + // no tokens + return NULL; } /*for(size_t i = 0; i < tokens.size; i++) diff --git a/src/elem.c b/src/elem.c index 2d77b89..e43ac7a 100644 --- a/src/elem.c +++ b/src/elem.c @@ -6,6 +6,7 @@ #include #include "ako/ako.h" +#include "lex/token.h" #include "private.h" #define IS_TABLE_OR_ARRAY(type) (type == AT_TABLE || type == AT_ARRAY) @@ -410,3 +411,106 @@ ako_elem_t* ako_elem_create_errorf(const char* fmt, ...) elem->str = str; return elem; } + +ako_elem_t* ako_elem_get(ako_elem_t* root, const char* path) +{ + // When tokenizatio this will be our error checking + // once done were using this for our current table + ako_elem_t* elem; + + dyn_array_t tokens = ako_tokenize(path, &elem, true); + if (elem != NULL) + { + ako_elem_destroy(elem); + return NULL; + } + + // We only support a small subset of the tokens + // We only support the following tokens: AKO_TT_IDENT AKO_TT_STRING AKO_TT_DOT AKO_TT_INT + elem = root; + for (size_t i = 0; i < tokens.size; ++i) + { + token_t* token = dyn_array_get(&tokens, i); + if (token->type != AKO_TT_IDENT && token->type != AKO_TT_STRING && token->type != AKO_TT_DOT && + token->type != AKO_TT_INT) + { + // no clue what do with what ever token this is + goto giveup; + } + + bool is_end = false; + if (i == tokens.size - 1) + { + is_end = true; + } + + if (is_end) + { + if (ako_elem_get_type(elem) == AT_ARRAY) + { + // if its an array we only support int + if (token->type != AKO_TT_INT) + { + goto giveup; + } + elem = ako_elem_array_get(elem, token->value_int); + goto valid; + } + else + { + // Only support string or ident at the end + if (!(token->type == AKO_TT_STRING || token->type == AKO_TT_IDENT)) + { + goto giveup; + } + + elem = ako_elem_table_get(elem, token->value_string); + goto valid; + } + + goto giveup; + } + + if (ako_elem_get_type(elem) == AT_ARRAY) + { + if (token->type != AKO_TT_INT) + { + goto giveup; + } + + elem = ako_elem_array_get(elem, token->value_int); + } + else + { + if (!(token->type == AKO_TT_STRING || token->type == AKO_TT_IDENT)) + { + // Unsupported + goto giveup; + } + + elem = ako_elem_table_get(elem, token->value_string); + } + + if (elem == NULL) + { + goto giveup; + } + + // ensure our next token is a dot + i++; + token = dyn_array_get(&tokens, i); + if (token->type != AKO_TT_DOT) + { + // Incorrect + goto giveup; + } + } + +giveup: + ako_free_tokens(&tokens); + return NULL; + +valid: + ako_free_tokens(&tokens); + return elem; +} diff --git a/src/lex/token.h b/src/lex/token.h index b4ecc1c..bd893b9 100644 --- a/src/lex/token.h +++ b/src/lex/token.h @@ -65,6 +65,8 @@ typedef struct size_t location_format(const location_t* loc, char* output, size_t output_size); +typedef struct ako_elem ako_elem_t; + // Returns an array of Token_t, please destroy the returned array when finished :) -dyn_array_t ako_tokenize(const char* source, char** err); +dyn_array_t ako_tokenize(const char* source, ako_elem_t** err, bool ignore_floats); void ako_free_tokens(dyn_array_t* tokens); diff --git a/src/lex/tokenizer.c b/src/lex/tokenizer.c index 4383fce..6cc6eba 100644 --- a/src/lex/tokenizer.c +++ b/src/lex/tokenizer.c @@ -8,6 +8,7 @@ #include #include "ako/ako.h" +#include "ako/elem.h" #include "token.h" #if defined(_MSC_VER) @@ -23,6 +24,7 @@ typedef struct state const char* source; size_t source_len; size_t index; + bool ignore_floats; location_t meta; location_t current_loc; @@ -109,6 +111,11 @@ static size_t count_number(state_t* state) while (has_value(state, offset)) { char c = peek(state, offset); + if (state->ignore_floats && c == '.') + { + // Ignoring floats and found a dot, were done :) + break; + } if (isdigit(c) || c == '.') { // good number! @@ -177,7 +184,7 @@ size_t location_format(const location_t* loc, char* output, size_t output_size) return written; } -static bool parse_digit(state_t* state, char** err) +static bool parse_digit(state_t* state, ako_elem_t** err) { *err = NULL; token_t token; @@ -215,7 +222,7 @@ static bool parse_digit(state_t* state, char** err) if (errno != 0 || strlen(numerr) > 0) { // Failed - sprintf(*err, "Failed to parse number at %zu:%zu", state->meta.line, state->meta.column); + *err = ako_elem_create_errorf("Failed to parse number at %zu:%zu", state->meta.line, state->meta.column); dyn_array_destroy(&state->tokens); return false; } @@ -224,15 +231,17 @@ static bool parse_digit(state_t* state, char** err) return true; } -dyn_array_t ako_tokenize(const char* source, char** err) +dyn_array_t ako_tokenize(const char* source, ako_elem_t** err, bool ignore_floats) { static dyn_array_t empty_array = {0}; + *err = NULL; state_t* state = alloca(sizeof(state_t)); memset(state, 0, sizeof(state_t)); state->tokens = dyn_array_create(sizeof(token_t)); state->source = source; state->source_len = strlen(source); + state->ignore_floats = ignore_floats; state->current_loc.line = 1; state->current_loc.column = 1; token_t token; @@ -241,7 +250,7 @@ dyn_array_t ako_tokenize(const char* source, char** err) while (has_value(state, 0)) { char c = peek(state, 0); - if (c == ' ' || c == '\n') + if (c == ' ' || c == '\n' || c == '\t') { consume(state); continue; @@ -360,8 +369,8 @@ dyn_array_t ako_tokenize(const char* source, char** err) if (!parse_digit(state, err)) { // Failed to parse number and we had an X before, this isn't valid - sprintf(*err, "Failed to parse vector at %zu:%zu", vector_delimiter.line, - vector_delimiter.column); + *err = ako_elem_create_errorf("Failed to parse vector at %zu:%zu", vector_delimiter.line, + vector_delimiter.column); dyn_array_destroy(&state->tokens); return empty_array; } @@ -375,16 +384,11 @@ dyn_array_t ako_tokenize(const char* source, char** err) continue; } } - else + else if (*err != NULL && ako_elem_is_error(*err)) { // if theres an error set then we failed to parse in a bad way - if (*err != NULL) - { - // Failed to parse number - sprintf(*err, "Failed to parse number at %zu:%zu", state->meta.line, state->meta.column); - dyn_array_destroy(&state->tokens); - return empty_array; - } + dyn_array_destroy(&state->tokens); + return empty_array; } if (c == '"') @@ -430,6 +434,11 @@ dyn_array_t ako_tokenize(const char* source, char** err) add_token(state, token); continue; } + + // If were here then we have a bad character + *err = ako_elem_create_errorf("Unknown character %c at %zu:%zu", c, state->meta.line, state->meta.column); + dyn_array_destroy(&state->tokens); + return empty_array; } return state->tokens; diff --git a/test/main.c b/test/main.c index 46d106e..f81dffe 100644 --- a/test/main.c +++ b/test/main.c @@ -111,7 +111,7 @@ int basic_value_first() ako_elem_destroy(egg); - //now try: thing+, not really a VT but its the same idea + // now try: thing+, not really a VT but its the same idea egg = ako_parse("thing+"); ASSERT_ELEM(egg); thing = ako_elem_table_get(egg, "thing"); @@ -253,6 +253,40 @@ int basic_serialise() return 0; }*/ +// +const char* sample_ako = +"song [\n" +" name \"The MMORPG ADDICTS ANTHEM\"\n" +" artists [[\n" +" [\n" +" name \"TENKOMORI\"\n" +" links [[\n" +" \"a\" \"b\" \"c\"\n" +" ]]\n" +" ]\n" +" [\n" +" name \"Hatsune Miku\"\n" +" links [[\n" +" \"e\" \"f\" \"g\"\n" +" ]]\n" +" ]\n" +" ]]\n" +"]\n"; + +int util_get() +{ + ako_elem_t* egg = ako_parse(sample_ako); + ASSERT_ELEM(egg); + + ASSERT_ELEM_STR(ako_elem_get(egg, "song.artists.0.links.1"), "b"); + ASSERT_ELEM_STR(ako_elem_get(egg, "song.artists.1.links.1"), "f"); + + ASSERT_ELEM_STR(ako_elem_get(egg, "song.artists.0.name"), "TENKOMORI"); + + ako_elem_destroy(egg); + return 0; +} + static test_t tests[] = { {"Basic parsing", &basic_parse}, {"Basic value first parsing", &basic_value_first}, @@ -267,6 +301,8 @@ static test_t tests[] = { // Unicode // {"Unicode parsing", &unicode_parse}, + // Utils + {"Utility Get", &util_get}, {NULL, NULL} // Null terminator }; diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..295b402 --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,7 @@ +# cli tools + +if(NOT WIN32) + add_executable(akocli + akocli.c) + target_link_libraries(akocli PUBLIC akoc) +endif () \ No newline at end of file diff --git a/utils/akocli.c b/utils/akocli.c new file mode 100644 index 0000000..04b0d02 --- /dev/null +++ b/utils/akocli.c @@ -0,0 +1,212 @@ +// Copyright (c) 2025 Tuyuji, Reece Hagan +// SPDX-License-Identifier: MIT +#include "ako/ako.h" + +#include +#include +#include +#include +#include + +void print_help() +{ + printf("Usage: akocli [OPTIONS]\n"); + printf("\nOptions:\n"); + printf("\t-h, --help Show this help message and exit\n"); + printf("\t-v, --version Show version information and exit\n"); + printf("\t-i Input file\n"); + printf("\t-t, --validate Validate the input file\n"); +} + +char* read_stdin_all() +{ + size_t buffer_size = 4096; + size_t len = 0; + char* buffer = malloc(buffer_size); + if (buffer == NULL) + { + return NULL; + } + + size_t i = 0; + while ((i = fread(buffer + len, 1, buffer_size - len, stdin)) > 0) + { + len += i; + if (len == buffer_size) + { + buffer_size += 4096; + char* new_buffer = realloc(buffer, buffer_size); + if (new_buffer == NULL) + { + free(buffer); + return NULL; + } + buffer = new_buffer; + } + } + + buffer[len] = '\0'; + return buffer; +} + +static struct +{ + char* source; + ako_elem_t* result; +} state; + +void free_state() +{ + if (state.source != NULL) + { + free(state.source); + state.source = NULL; + } + + if (state.result != NULL) + { + ako_elem_destroy(state.result); + state.result = NULL; + } +} + +//Examples: +//akocli --help +//akocli +int main(int argc, char **argv) +{ + memset(&state, 0, sizeof(state)); + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + print_help(); + return 0; + } + } + + const char* input_file = NULL; + const char* query_str = NULL; + bool validate = false; + + static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"version", no_argument, 0, 'v'}, + {"input", required_argument, 0, 'i'}, + {"validate", no_argument, 0, 't'}, + {"query", required_argument, 0, 'q'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "hvti:q:", long_options, NULL)) != -1) + { + switch (opt) + { + case 'h': + print_help(); + return 0; + case 'v': + printf("akocli version " AKO_VERSION_STR "\n"); + return 0; + case 'i': + input_file = optarg; + break; + case 't': + validate = true; + break; + case 'q': + query_str = optarg; + break; + default: + print_help(); + return 1; + } + } + + bool read_from_stdin = false; + + if (!isatty(STDIN_FILENO)) + { + read_from_stdin = true; + } + + if (input_file != NULL && strcmp(input_file, "-") == 0) + { + read_from_stdin = true; + } + + if (read_from_stdin) + { + state.source = read_stdin_all(); + } + else + { + if (input_file == NULL) + { + printf("No input file specified\n"); + return 1; + } + + //input_file has to be a file path + FILE* file = fopen(input_file, "r"); + if (file == NULL) + { + perror("fopen"); + return 1; + } + fseek(file, 0, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0, SEEK_SET); + state.source = malloc(file_size + 1); + if (state.source == NULL) + { + perror("malloc"); + fclose(file); + return 1; + } + size_t ignored = fread((char*)state.source, 1, file_size, file); + (void)ignored; + ((char*)state.source)[file_size] = '\0'; + fclose(file); + } + + state.result = ako_parse(state.source); + if (validate) + { + if (state.result == NULL || ako_elem_is_error(state.result)) + { + printf("Failed to parse: %s\n", ako_elem_get_string(state.result)); + free_state(); + return 1; + } + printf("Parsed successfully\n"); + free_state(); + return 0; + } + + if (ako_elem_is_error(state.result)) + { + printf("Failed to parse: %s\n", ako_elem_get_string(state.result)); + free_state(); + return 1; + } + + if (query_str != NULL) + { + ako_elem_t* elem = ako_elem_get(state.result, query_str); + if (elem == NULL) + { + free_state(); + return 1; + } + + const char* str = ako_serialize(elem, NULL, ASF_FORMAT); + printf("%s\n", str); + ako_free_string(str); + } + + //idk + free_state(); + return 0; +} \ No newline at end of file