diff options
-rw-r--r-- | main.c | 563 | ||||
-rw-r--r-- | tests/cram/test_basic.t | 97 | ||||
-rwxr-xr-x | tests/custom/run_tests.sh | 5 |
3 files changed, 452 insertions, 213 deletions
@@ -20,7 +20,9 @@ #include <unistd.h> #include <errno.h> #include <ctype.h> +#include <fcntl.h> #include <sys/stat.h> +#include <sys/types.h> #ifdef JSONC #include <json.h> @@ -35,50 +37,80 @@ #include "ucode/source.h" #include "ucode/program.h" +static FILE *stdin_unused; static void print_usage(const char *app) { - printf( - "Usage\n\n" - " # %s [-t] [-l] [-r] [-S] [-R] [-x function [-x ...]] [-e '[prefix=]{\"var\": ...}'] [-E [prefix=]env.json] {-i <file> | -s \"ucode script...\"}\n" - " -h, --help Print this help\n" - " -i file Execute the given ucode script file\n" - " -s \"ucode script...\" Execute the given string as ucode script\n" - " -t Enable VM execution tracing\n" - " -l Do not strip leading block whitespace\n" - " -r Do not trim trailing block newlines\n" - " -S Enable strict mode\n" - " -R Enable raw code mode\n" - " -e Set global variables from given JSON object\n" - " -E Set global variables from given JSON file\n" - " -x Disable given function\n" - " -m Preload given module\n" - " -o Write precompiled byte code to given file\n" - " -O Write precompiled byte code to given file and strip debug information\n", - basename(app)); -} - -static void -register_variable(uc_value_t *scope, const char *key, uc_value_t *val) -{ - char *name = strdup(key); - char *p; + const char *p = strrchr(app, '/'); - if (!name) - return; - - for (p = name; *p; p++) - if (!isalnum(*p) && *p != '_') - *p = '_'; - - ucv_object_add(scope, name, val); - free(name); + printf( + "Usage:\n" + " %1$s -h\n" + " %1$s -e \"expression\"\n" + " %1$s input.uc [input2.uc ...]\n" + " %1$s -c [-s] [-o output.uc] input.uc [input2.uc ...]\n\n" + + "-h\n" + " Help display this help.\n\n" + + "-e \"expression\"\n" + " Execute the given expression as ucode program.\n\n" + + "-t\n" + " Enable VM execution tracing.\n\n" + + "-S\n" + " Enable strict mode.\n\n" + + "-R\n" + " Process source file(s) as raw script code (default).\n\n" + + "-T[flag,flag,...]\n" + " Process the source file(s) as templates, not as raw script code.\n" + " Supported flags: no-lstrip (don't strip leading whitespace before\n" + " block tags), no-rtrim (don't strip trailing newline after block tags).\n\n" + + "-D [name=]value\n" + " Define global variable. If `name` is omitted, a JSON dictionary is\n" + " expected with each property becoming a global variable set to the\n" + " corresponding value. If `name` is specified, it is defined as global\n" + " variable set to `value` parsed as JSON (or the literal `value` string\n" + " if JSON parsing fails).\n\n" + + "-F [name=]path\n" + " Like `-D` but reading the value from the file in `path`. The given\n" + " file must contain a single, well-formed JSON dictionary.\n\n" + + "-U name\n" + " Undefine the given global variable name.\n\n" + + "-l [name=]library\n" + " Preload the given `library`, optionally aliased to `name`.\n\n" + + "-L pattern\n" + " Append given `pattern` to default library search paths. If the pattern\n" + " contains no `*`, it is added twice, once with `/*.so` and once with\n" + " `/*.uc` appended to it.\n\n" + + "-c[flag,flag,...]\n" + " Compile the given source file(s) to bytecode instead of executing them.\n" + " Supported flags: no-interp (omit interpreter line), interp=... (over-\n" + " ride interpreter line with ...)\n\n" + + "-o path\n" + " Output file path when compiling. If omitted, the compiled byte code\n" + " is written to `./uc.out`. Only meaningful in conjunction with `-c`.\n\n" + + "-s\n" + " Omit (strip) debug information when compiling files.\n" + " Only meaningful in conjunction with `-c`.\n\n", + p ? p + 1 : app); } static int -compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip) +compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *interp) { uc_value_t *res = NULL; uc_program_t *program; @@ -95,6 +127,9 @@ compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip) } if (precompile) { + if (interp) + fprintf(precompile, "#!%s\n", interp); + uc_program_write(program, precompile, !strip); fclose(precompile); goto out; @@ -128,41 +163,124 @@ out: } static uc_source_t * -read_stdin(char **ptr) +read_stdin(void) { size_t rlen = 0, tlen = 0; - char buf[128]; + char buf[128], *p = NULL; - if (*ptr) { - fprintf(stderr, "Can read from stdin only once\n"); + if (!stdin_unused) { + fprintf(stderr, "The stdin can only be read once\n"); errno = EINVAL; return NULL; } while (true) { - rlen = fread(buf, 1, sizeof(buf), stdin); + rlen = fread(buf, 1, sizeof(buf), stdin_unused); if (rlen == 0) break; - *ptr = xrealloc(*ptr, tlen + rlen); - memcpy(*ptr + tlen, buf, rlen); + p = xrealloc(p, tlen + rlen); + memcpy(p + tlen, buf, rlen); tlen += rlen; } - return uc_source_new_buffer("[stdin]", *ptr, tlen); + stdin_unused = NULL; + + return uc_source_new_buffer("[stdin]", p, tlen); } -static uc_value_t * -parse_envfile(FILE *fp) +static void +parse_template_modeflags(char *opt, uc_parse_config_t *config) +{ + char *p; + + if (!opt) + return; + + for (p = strtok(opt, ", "); p; p = strtok(NULL, ", ")) { + if (!strcmp(p, "no-lstrip")) + config->lstrip_blocks = false; + else if (!strcmp(p, "no-rtrim")) + config->trim_blocks = false; + else + fprintf(stderr, "Unrecognized -T flag \"%s\", ignoring\n", p); + } +} + +static void +parse_compile_flags(char *opt, char **interp) +{ + char *p, *k, *v; + + if (!opt) + return; + + for (p = strtok(opt, ","); p; p = strtok(NULL, ",")) { + k = p; + v = strchr(p, '='); + + if (v) + *v++ = 0; + + if (!strcmp(k, "no-interp")) { + if (v) + fprintf(stderr, "Compile flag \"%s\" takes no value, ignoring\n", k); + + *interp = NULL; + } + else if (!strcmp(k, "interp")) { + if (!v) + fprintf(stderr, "Compile flag \"%s\" requires a value, ignoring\n", k); + else + *interp = v; + } + else { + fprintf(stderr, "Unrecognized -c flag \"%s\", ignoring\n", k); + } + } +} + +static bool +parse_define_file(char *opt, uc_value_t *globals) { enum json_tokener_error err = json_tokener_continue; + char buf[128], *name = NULL, *p; struct json_tokener *tok; json_object *jso = NULL; - uc_value_t *rv; - char buf[128]; size_t rlen; + FILE *fp; + + p = strchr(opt, '='); + + if (p) { + name = opt; + *p++ = 0; + } + else { + p = opt; + } + + if (!strcmp(p, "-")) { + if (!stdin_unused) { + fprintf(stderr, "The stdin can only be read once\n"); + + return false; + } + + fp = stdin_unused; + stdin_unused = NULL; + } + else + fp = fopen(p, "r"); + + if (!fp) { + fprintf(stderr, "Unable to open definition file \"%s\": %s\n", + p, strerror(errno)); + + return true; + } tok = xjs_new_tokener(); @@ -179,100 +297,209 @@ parse_envfile(FILE *fp) break; } + json_tokener_free(tok); + fclose(fp); + if (err != json_tokener_success || !json_object_is_type(jso, json_type_object)) { json_object_put(jso); - return NULL; + fprintf(stderr, "Invalid definition file \"%s\": %s\n", + p, (err != json_tokener_success) + ? "JSON parse failure" : "Not a valid JSON object"); + + return false; + } + + if (name && *name) { + ucv_object_add(globals, name, ucv_from_json(NULL, jso)); } + else { + json_object_object_foreach(jso, key, val) + ucv_object_add(globals, key, ucv_from_json(NULL, val)); + } + + json_object_put(jso); + + return true; +} + +static bool +parse_define_string(char *opt, uc_value_t *globals) +{ + enum json_tokener_error err; + struct json_tokener *tok; + json_object *jso = NULL; + char *name = NULL, *p; + bool rv = false; + size_t len; + + p = strchr(opt, '='); + + if (p) { + name = opt; + *p++ = 0; + } + else { + p = opt; + } + + len = strlen(p); + tok = xjs_new_tokener(); + + /* NB: the len + 1 here is intentional to pass the terminating \0 byte + * to the json-c parser. This is required to work-around upstream + * issue #681 <https://github.com/json-c/json-c/issues/681> */ + jso = json_tokener_parse_ex(tok, p, len + 1); + + err = json_tokener_get_error(tok); + + /* Treat trailing bytes after a parsed value as error */ + if (err == json_tokener_success && json_tokener_get_parse_end(tok) < len) + err = json_tokener_error_parse_unexpected; json_tokener_free(tok); - rv = ucv_from_json(NULL, jso); + if (err != json_tokener_success) { + json_object_put(jso); + + if (!name || !*name) { + fprintf(stderr, "Invalid -D option value \"%s\": %s\n", + p, json_tokener_error_desc(err)); + + return false; + } + + ucv_object_add(globals, name, ucv_string_new(p)); + + return true; + } + + if (name && *name) { + ucv_object_add(globals, name, ucv_from_json(NULL, jso)); + rv = true; + } + else if (json_object_is_type(jso, json_type_object)) { + json_object_object_foreach(jso, key, val) + ucv_object_add(globals, key, ucv_from_json(NULL, val)); + rv = true; + } + else { + fprintf(stderr, "Invalid -D option value \"%s\": Not a valid JSON object\n", p); + } json_object_put(jso); return rv; } +static void +parse_search_path(char *pattern, uc_value_t *globals) +{ + uc_value_t *rsp = ucv_object_get(globals, "REQUIRE_SEARCH_PATH", NULL); + size_t len; + char *p; + + if (strchr(pattern, '*')) { + ucv_array_push(rsp, ucv_string_new(pattern)); + return; + } + + len = strlen(pattern); + + if (!len) + return; + + while (pattern[len-1] == '/') + pattern[--len] = 0; + + xasprintf(&p, "%s/*.so", pattern); + ucv_array_push(rsp, ucv_string_new(p)); + free(p); + + xasprintf(&p, "%s/*.uc", pattern); + ucv_array_push(rsp, ucv_string_new(p)); + free(p); +} + +static bool +parse_library_load(char *opt, uc_vm_t *vm) +{ + char *name = NULL, *p; + uc_value_t *lib, *ctx; + + p = strchr(opt, '='); + + if (p) { + name = opt; + *p++ = 0; + } + else { + p = opt; + } + + lib = ucv_string_new(p); + ctx = uc_vm_invoke(vm, "require", 1, lib); + ucv_put(lib); + + if (!ctx) + return false; + + ucv_object_add(uc_vm_scope_get(vm), name ? name : p, ctx); + + return true; +} + int main(int argc, char **argv) { - uc_source_t *source = NULL, *envfile = NULL; + char *interp = "/usr/bin/env ucode"; + uc_source_t *source = NULL; FILE *precompile = NULL; - char *stdin = NULL, *c; + char *outfile = NULL; bool strip = false; uc_vm_t vm = { 0 }; - uc_value_t *o, *p; int opt, rv = 0; + uc_value_t *o; + int fd; uc_parse_config_t config = { .strict_declarations = false, .lstrip_blocks = true, - .trim_blocks = true + .trim_blocks = true, + .raw_mode = true }; - if (argc == 1) - { + if (argc == 1) { print_usage(argv[0]); goto out; } + stdin_unused = stdin; + uc_vm_init(&vm, &config); /* load std functions into global scope */ uc_stdlib_load(uc_vm_scope_get(&vm)); - /* register ARGV array */ - o = ucv_array_new_length(&vm, argc); - - for (opt = 0; opt < argc; opt++) - ucv_array_push(o, ucv_string_new(argv[opt])); + /* register ARGV array but populate it later (to allow for -U ARGV) */ + o = ucv_array_new(&vm); - ucv_object_add(uc_vm_scope_get(&vm), "ARGV", o); + ucv_object_add(uc_vm_scope_get(&vm), "ARGV", ucv_get(o)); /* parse options */ - while ((opt = getopt(argc, argv, "hlrtSRe:E:i:s:m:x:o:O:")) != -1) + while ((opt = getopt(argc, argv, "he:tST::RD:F:U:l:L:c::o:s")) != -1) { switch (opt) { case 'h': print_usage(argv[0]); goto out; - case 'i': - if (source) - fprintf(stderr, "Options -i and -s are exclusive\n"); - - if (!strcmp(optarg, "-")) - source = read_stdin(&stdin); - else - source = uc_source_new_file(optarg); - - if (!source) { - fprintf(stderr, "Failed to open %s: %s\n", optarg, strerror(errno)); - rv = 1; - goto out; - } - - break; - - case 'l': - config.lstrip_blocks = false; - break; - - case 'r': - config.trim_blocks = false; + case 'e': + source = uc_source_new_buffer("[-e argument]", xstrdup(optarg), strlen(optarg)); break; - case 's': - if (source) - fprintf(stderr, "Options -i and -s are exclusive\n"); - - c = xstrdup(optarg); - source = uc_source_new_buffer("[-s argument]", c, strlen(c)); - - if (!source) - free(c); - + case 't': + uc_vm_trace_set(&vm, 1); break; case 'S': @@ -283,129 +510,101 @@ main(int argc, char **argv) config.raw_mode = true; break; - case 'e': - c = strchr(optarg, '='); + case 'T': + config.raw_mode = false; + parse_template_modeflags(optarg, &config); + break; - if (c) - *c++ = 0; - else - c = optarg; - - envfile = uc_source_new_buffer("[-e argument]", xstrdup(c), strlen(c)); - /* fallthrough */ - - case 'E': - if (!envfile) { - c = strchr(optarg, '='); - - if (c) - *c++ = 0; - else - c = optarg; - - if (!strcmp(c, "-")) - envfile = read_stdin(&stdin); - else - envfile = uc_source_new_file(c); - - if (!envfile) { - fprintf(stderr, "Failed to open %s: %s\n", c, strerror(errno)); - rv = 1; - goto out; - } + case 'D': + if (!parse_define_string(optarg, uc_vm_scope_get(&vm))) { + rv = 1; + goto out; } - o = parse_envfile(envfile->fp); - - uc_source_put(envfile); - - envfile = NULL; + break; - if (!o) { - fprintf(stderr, "Option -%c must point to a valid JSON object\n", opt); + case 'F': + if (!parse_define_file(optarg, uc_vm_scope_get(&vm))) { rv = 1; goto out; } - if (c > optarg && optarg[0]) { - p = ucv_object_new(&vm); - ucv_object_add(uc_vm_scope_get(&vm), optarg, p); - } - else { - p = uc_vm_scope_get(&vm); - } - - ucv_object_foreach(o, key, val) - register_variable(p, key, ucv_get(val)); - - ucv_put(o); - break; - case 'm': - p = ucv_string_new(optarg); - o = uc_vm_invoke(&vm, "require", 1, p); - - if (o) - register_variable(uc_vm_scope_get(&vm), optarg, o); - - ucv_put(p); - + case 'U': + ucv_object_delete(uc_vm_scope_get(&vm), optarg); break; - case 't': - uc_vm_trace_set(&vm, 1); + case 'L': + parse_search_path(optarg, uc_vm_scope_get(&vm)); break; - case 'x': - o = ucv_object_get(uc_vm_scope_get(&vm), optarg, NULL); + case 'l': + parse_library_load(optarg, &vm); + break; - if (ucv_is_callable(o)) - ucv_object_delete(uc_vm_scope_get(&vm), optarg); - else - fprintf(stderr, "Unknown function %s specified\n", optarg); + case 'c': + outfile = "./uc.out"; + parse_compile_flags(optarg, &interp); + break; + case 's': + strip = true; break; case 'o': - case 'O': - strip = (opt == 'O'); - - if (!strcmp(optarg, "-")) { - precompile = stdout; - } - else { - precompile = fopen(optarg, "wb"); - - if (!precompile) { - fprintf(stderr, "Unable to open output file %s: %s\n", - optarg, strerror(errno)); - - goto out; - } - } - + outfile = optarg; break; } } if (!source && argv[optind] != NULL) { - source = uc_source_new_file(argv[optind]); + if (!strcmp(argv[optind], "-")) + source = read_stdin(); + else + source = uc_source_new_file(argv[optind]); if (!source) { - fprintf(stderr, "Failed to open %s: %s\n", argv[optind], strerror(errno)); + fprintf(stderr, "Failed to open \"%s\": %s\n", argv[optind], strerror(errno)); rv = 1; goto out; } + + optind++; } if (!source) { - fprintf(stderr, "One of -i or -s is required\n"); + fprintf(stderr, "Require either -e expression or source file\n"); rv = 1; goto out; } - rv = compile(&vm, source, precompile, strip); + if (outfile) { + if (!strcmp(outfile, "-")) { + precompile = stdout; + } + else { + fd = open(outfile, O_WRONLY|O_CREAT|O_TRUNC, 0777); + + if (fd == -1) { + fprintf(stderr, "Unable to open output file %s: %s\n", + outfile, strerror(errno)); + + rv = 1; + goto out; + } + + precompile = fdopen(fd, "wb"); + } + } + + /* populate ARGV array */ + for (; optind < argc; optind++) + ucv_array_push(o, ucv_string_new(argv[optind])); + + ucv_put(o); + + rv = compile(&vm, source, precompile, strip, interp); out: uc_source_put(source); diff --git a/tests/cram/test_basic.t b/tests/cram/test_basic.t index d2a3605..b85167f 100644 --- a/tests/cram/test_basic.t +++ b/tests/cram/test_basic.t @@ -10,52 +10,95 @@ setup common environment: check that ucode provides exepected help: $ ucode | sed 's/ucode-san/ucode/' - Usage - - # ucode [-t] [-l] [-r] [-S] [-R] [-x function [-x ...]] [-e '[prefix=]{"var": ...}'] [-E [prefix=]env.json] {-i <file> | -s "ucode script..."} - -h, --help\tPrint this help (esc) - -i file\tExecute the given ucode script file (esc) - -s "ucode script..."\tExecute the given string as ucode script (esc) - -t Enable VM execution tracing - -l Do not strip leading block whitespace - -r Do not trim trailing block newlines - -S Enable strict mode - -R Enable raw code mode - -e Set global variables from given JSON object - -E Set global variables from given JSON file - -x Disable given function - -m Preload given module - -o Write precompiled byte code to given file - -O Write precompiled byte code to given file and strip debug information + Usage: + ucode -h + ucode -e "expression" + ucode input.uc [input2.uc ...] + ucode -c [-s] [-o output.uc] input.uc [input2.uc ...] + + -h + Help display this help. + + -e "expression" + Execute the given expression as ucode program. + + -t + Enable VM execution tracing. + + -S + Enable strict mode. + + -R + Process source file(s) as raw script code (default). + + -T[flag,flag,...] + Process the source file(s) as templates, not as raw script code. + Supported flags: no-lstrip (don't strip leading whitespace before + block tags), no-rtrim (don't strip trailing newline after block tags). + + -D [name=]value + Define global variable. If `name` is omitted, a JSON dictionary is + expected with each property becoming a global variable set to the + corresponding value. If `name` is specified, it is defined as global + variable set to `value` parsed as JSON (or the literal `value` string + if JSON parsing fails). + + -F [name=]path + Like `-D` but reading the value from the file in `path`. The given + file must contain a single, well-formed JSON dictionary. + + -U name + Undefine the given global variable name. + + -l [name=]library + Preload the given `library`, optionally aliased to `name`. + + -L pattern + Append given `pattern` to default library search paths. If the pattern + contains no `*`, it is added twice, once with `/*.so` and once with + `/*.uc` appended to it. + + -c[flag,flag,...] + Compile the given source file(s) to bytecode instead of executing them. + Supported flags: no-interp (omit interpreter line), interp=... (over- + ride interpreter line with ...) + + -o path + Output file path when compiling. If omitted, the compiled byte code + is written to `./uc.out`. Only meaningful in conjunction with `-c`. + + -s + Omit (strip) debug information when compiling files. + Only meaningful in conjunction with `-c`. + check that ucode prints greetings: - $ ucode -s "{% print('hello world') %}" + $ ucode -e "print('hello world')" hello world (no-eol) check that ucode provides proper error messages: - $ ucode -m foo - One of -i or -s is required + $ ucode -l foo + Require either -e expression or source file [1] - $ ucode -m foo -s ' ' + $ ucode -l foo -e ' ' Runtime error: No module named 'foo' could be found [254] - $ touch moo; ucode -m foo -i moo + $ touch moo; ucode -l foo moo Runtime error: No module named 'foo' could be found [254] check that ucode can load fs module: - $ ucode -m fs - One of -i or -s is required + $ ucode -l fs + Require either -e expression or source file [1] - $ ucode -m fs -s ' ' - (no-eol) + $ ucode -l fs -e ' ' - $ touch moo; ucode -m fs -i moo + $ touch moo; ucode -l fs moo diff --git a/tests/custom/run_tests.sh b/tests/custom/run_tests.sh index 2f13c3b..c2839df 100755 --- a/tests/custom/run_tests.sh +++ b/tests/custom/run_tests.sh @@ -93,10 +93,7 @@ run_testcase() { IFS=$' \t\n' - $ucode_bin $args -e '{ - "REQUIRE_SEARCH_PATH": [ "'"$ucode_lib"'/*.so" ], - "TESTFILES_PATH": "'"$dir"'/files" - }' -i - <"$in" >"$dir/res.out" 2>"$dir/res.err" + $ucode_bin -T -L "$ucode_lib/*.so" -D TESTFILES_PATH="$dir/files" $args - <"$in" >"$dir/res.out" 2>"$dir/res.err" ) printf "%d\n" $? > "$dir/res.code" |