diff options
author | Jo-Philipp Wich <jo@mein.io> | 2022-03-14 08:47:58 +0100 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2022-03-14 15:31:34 +0100 |
commit | 46188077ef727c21513008f4e0c42e8cb211e90e (patch) | |
tree | 47933baeb43ff6ff3a95810c25fc189a6062e7be /main.c | |
parent | 23929951ad4b0ed2bcea793f6bf72d9e4236d3c7 (diff) |
main: rework CLI frontend
- Change command line flags to be align better with those of other
interpreters and with the gcc compiler, e.g. `-D` and `-U` to
define and undefine globals, `-e` to execute script expression etc.
- Pass only excess CLI arguments as `ARGV` to scripts, e.g.
`ucode -e 'print("Hello world")' -- -x -y` would pass only
`[ "-x", "-y" ]` as ARGV contents
- Default to raw mode and introduce flag to enable template mode
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'main.c')
-rw-r--r-- | main.c | 563 |
1 files changed, 381 insertions, 182 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); |