aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2025-03-17 19:29:28 -0400
committerBruce Hill <bruce@bruce-hill.com>2025-03-17 19:29:28 -0400
commit0bba31912665a82f848642e6b4247071a3ee177a (patch)
treeae5c3ac7501a0841c9a858d6559a0dfb7db69035
parent94993c5f113b27083e586c7620eb896fe750c6d1 (diff)
Big overhaul:
- Clean up environment code using type strings instead of manually defining types - Add Commands module - Move Shell lang into an example module that uses Commands module - Fix some bugs with chained library dependencies
-rw-r--r--.gitignore7
-rw-r--r--Makefile4
-rw-r--r--compile.c6
-rw-r--r--environment.c108
-rw-r--r--examples/commands/README.md5
-rw-r--r--examples/commands/commands.c177
-rw-r--r--examples/commands/commands.tm63
-rw-r--r--examples/shell/shell.tm35
-rw-r--r--examples/tomo-install/tomo-install.tm9
-rw-r--r--parse.c19
-rw-r--r--parse.h1
-rw-r--r--stdlib/README.md1
-rw-r--r--stdlib/shell.c157
-rw-r--r--stdlib/shell.h35
-rw-r--r--stdlib/tomo.h1
-rw-r--r--tomo.c7
16 files changed, 367 insertions, 268 deletions
diff --git a/.gitignore b/.gitignore
index 140d7bf5..1a2f8c86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,9 @@
/tags
/test/*
!/test/*.tm
-/examples/*
+/examples
!/examples/*.tm
-!/examples/*.ini
+!/examples/*/*.tm
+!/examples/*/*.ini
+!/examples/*/*.md
+/junk
diff --git a/Makefile b/Makefile
index e0774c84..f7305c67 100644
--- a/Makefile
+++ b/Makefile
@@ -32,7 +32,7 @@ CFLAGS_PLACEHOLDER="$$(echo -e '\033[2m<flags...>\033[m')"
LDLIBS=-lgc -lcord -lm -lunistring -lgmp -ldl
BUILTIN_OBJS=stdlib/siphash.o stdlib/arrays.o stdlib/bools.o stdlib/bytes.o stdlib/nums.o stdlib/integers.o \
stdlib/pointers.o stdlib/memory.o stdlib/text.o stdlib/threads.o stdlib/c_strings.o stdlib/tables.o \
- stdlib/types.o stdlib/util.o stdlib/files.o stdlib/shell.o stdlib/paths.o stdlib/rng.o \
+ stdlib/types.o stdlib/util.o stdlib/files.o stdlib/paths.o stdlib/rng.o \
stdlib/optionals.o stdlib/patterns.o stdlib/metamethods.o stdlib/functiontype.o stdlib/stdlib.o \
stdlib/structs.o stdlib/enums.o stdlib/moments.o stdlib/mutexeddata.o
TESTS=$(patsubst %.tm,%.tm.testresult,$(wildcard test/*.tm))
@@ -72,7 +72,7 @@ clean:
pandoc --lua-filter=.pandoc/bold-code.lua -s $< -t man -o $@
examples:
- ./tomo -IL examples/vectors examples/base64 examples/log examples/ini examples/game examples/http examples/threads examples/tomodeps examples/tomo-install examples/wrap
+ ./tomo -IL examples/vectors examples/commands examples/shell examples/base64 examples/log examples/ini examples/game examples/http examples/threads examples/tomodeps examples/tomo-install examples/wrap
./tomo examples/learnxiny.tm
install: tomo libtomo.so tomo.1
diff --git a/compile.c b/compile.c
index b476e80e..b2bb9aa5 100644
--- a/compile.c
+++ b/compile.c
@@ -524,8 +524,6 @@ CORD compile_type(type_t *t)
return "Text_t";
else if (streq(text->lang, "Pattern"))
return "Pattern_t";
- else if (streq(text->lang, "Shell"))
- return "Shell_t";
else
return CORD_all(namespace_prefix(text->env, text->env->namespace->parent), text->lang, "$$type");
}
@@ -2660,7 +2658,7 @@ CORD compile(env_t *env, ast_t *ast)
CORD lang_constructor;
if (!lang || streq(lang, "Text"))
lang_constructor = "Text";
- else if (streq(lang, "Pattern") || streq(lang, "Shell"))
+ else if (streq(lang, "Pattern"))
lang_constructor = lang;
else
lang_constructor = CORD_all(namespace_prefix(Match(text_t, TextType)->env, Match(text_t, TextType)->env->namespace->parent), lang);
@@ -3849,8 +3847,6 @@ CORD compile_type_info(type_t *t)
return "&Text$info";
else if (streq(text->lang, "Pattern"))
return "&Pattern$info";
- else if (streq(text->lang, "Shell"))
- return "&Shell$info";
return CORD_all("(&", namespace_prefix(text->env, text->env->namespace->parent), text->lang, "$$info)");
}
case StructType: {
diff --git a/environment.c b/environment.c
index 4ffa1dd9..afa9524b 100644
--- a/environment.c
+++ b/environment.c
@@ -5,6 +5,7 @@
#include "cordhelpers.h"
#include "environment.h"
+#include "parse.h"
#include "stdlib/datatypes.h"
#include "stdlib/tables.h"
#include "stdlib/text.h"
@@ -17,51 +18,60 @@ type_t *RNG_TYPE = NULL;
public type_t *PATH_TYPE = NULL;
public type_t *THREAD_TYPE = NULL;
+static type_t *declare_type(env_t *env, const char *def_str)
+{
+ ast_t *ast = parse(def_str);
+ if (!ast) errx(1, "Couldn't not parse struct def: %s", def_str);
+ if (ast->tag != Block) errx(1, "Couldn't not parse struct def: %s", def_str);
+ ast_list_t *statements = Match(ast, Block)->statements;
+ if (statements == NULL || statements->next) errx(1, "Couldn't not parse struct def: %s", def_str);
+ switch (statements->ast->tag) {
+ case StructDef: {
+ auto def = Match(statements->ast, StructDef);
+ prebind_statement(env, statements->ast);
+ bind_statement(env, statements->ast);
+ return Table$str_get(*env->types, def->name);
+ }
+ case EnumDef: {
+ auto def = Match(statements->ast, EnumDef);
+ prebind_statement(env, statements->ast);
+ bind_statement(env, statements->ast);
+ return Table$str_get(*env->types, def->name);
+ }
+ default: errx(1, "Not a type definition: %s", def_str);
+ }
+}
+
+static type_t *bind_type(env_t *env, const char *name, type_t *type)
+{
+ if (Table$str_get(*env->types, name))
+ errx(1, "Duplicate binding for type: %s", name);
+ Table$str_set(env->types, name, type);
+ return type;
+}
+
env_t *new_compilation_unit(CORD libname)
{
env_t *env = new(env_t);
env->code = new(compilation_unit_t);
env->types = new(Table_t);
env->globals = new(Table_t);
- env->locals = new(Table_t, .fallback=env->globals);
+ env->locals = env->globals;
env->imports = new(Table_t);
- if (!TEXT_TYPE)
- TEXT_TYPE = Type(TextType, .lang="Text", .env=namespace_env(env, "Text"));
+ TEXT_TYPE = bind_type(env, "Text", Type(TextType, .lang="Text", .env=namespace_env(env, "Text")));
+ (void)bind_type(env, "Int", Type(BigIntType));
+ (void)bind_type(env, "Int32", Type(IntType, .bits=TYPE_IBITS32));
+ (void)bind_type(env, "Memory", Type(MemoryType));
+ MATCH_TYPE = declare_type(env, "struct Match(text:Text, index:Int, captures:[Text])");
+ PATH_TYPE = declare_type(env, "struct Path(type:Int32, components:[Text])");
+ THREAD_TYPE = declare_type(env, "struct Thread(; opaque)");
+ RNG_TYPE = declare_type(env, "struct RNG(state:@Memory)");
typedef struct {
const char *name, *code, *type_str;
} ns_entry_t;
- {
- env_t *match_env = namespace_env(env, "Match");
- MATCH_TYPE = Type(
- StructType, .name="Match", .env=match_env,
- .fields=new(arg_t, .name="text", .type=TEXT_TYPE,
- .next=new(arg_t, .name="index", .type=INT_TYPE,
- .next=new(arg_t, .name="captures", .type=Type(ArrayType, .item_type=TEXT_TYPE)))));
- }
-
- {
- env_t *path_env = namespace_env(env, "Path");
- PATH_TYPE = Type(
- StructType, .name="Path", .env=path_env,
- .fields=new(arg_t, .name="type", .type=Type(IntType, .bits=TYPE_IBITS32),
- .next=new(arg_t, .name="components", .type=Type(ArrayType, .item_type=TEXT_TYPE))));
- }
-
- {
- env_t *thread_env = namespace_env(env, "Thread");
- THREAD_TYPE = Type(StructType, .name="Thread", .env=thread_env, .opaque=true);
- }
-
- {
- env_t *rng_env = namespace_env(env, "RNG");
- RNG_TYPE = Type(
- StructType, .name="RNG", .env=rng_env,
- .fields=new(arg_t, .name="state", .type=Type(PointerType, .pointed=Type(MemoryType))));
- }
-
struct {
const char *name;
type_t *type;
@@ -353,15 +363,6 @@ env_t *new_compilation_unit(CORD libname)
{"num32", "RNG$num32", "func(rng:RNG, min=Num32(0.0), max=Num32(1.0) -> Num32)"},
{"set_seed", "RNG$set_seed", "func(rng:RNG, seed:[Byte])"},
)},
- {"Shell", Type(TextType, .lang="Shell", .env=namespace_env(env, "Shell")), "Shell_t", "Shell$info", TypedArray(ns_entry_t,
- {"by_line", "Shell$by_line", "func(command:Shell -> func(->Text?)?)"},
- {"escape_int", "Int$value_as_text", "func(i:Int -> Shell)"},
- {"escape_text", "Shell$escape_text", "func(text:Text -> Shell)"},
- {"escape_text_array", "Shell$escape_text_array", "func(texts:[Text] -> Shell)"},
- {"execute", "Shell$execute", "func(command:Shell -> Int32?)"},
- {"run_bytes", "Shell$run_bytes", "func(command:Shell -> [Byte]?)"},
- {"run", "Shell$run", "func(command:Shell -> Text?)"},
- )},
{"Text", TEXT_TYPE, "Text_t", "Text$info", TypedArray(ns_entry_t,
{"as_c_string", "Text$as_c_string", "func(text:Text -> CString)"},
{"at", "Text$cluster", "func(text:Text, index:Int -> Text)"},
@@ -543,12 +544,6 @@ env_t *new_compilation_unit(CORD libname)
{"Path$escape_text", "func(text:Text -> Path)"},
{"Path$escape_path", "func(path:Path -> Path)"},
{"Int$value_as_text", "func(i:Int -> Path)"});
- ADD_CONSTRUCTORS("Shell",
- {"Shell$escape_text", "func(text:Text -> Shell)"},
- {"Shell$escape_path", "func(path:Path -> Shell)"},
- {"Shell$escape_text_array", "func(texts:[Text] -> Shell)"},
- {"Shell$escape_path_array", "func(paths:[Path] -> Shell)"},
- {"Int$value_as_text", "func(i:Int -> Shell)"});
ADD_CONSTRUCTORS("CString", {"Text$as_c_string", "func(text:Text -> CString)"});
ADD_CONSTRUCTORS("Moment",
{"Moment$now", "func(-> Moment)"},
@@ -558,11 +553,6 @@ env_t *new_compilation_unit(CORD libname)
ADD_CONSTRUCTORS("Thread", {"Thread$new", "func(fn:func() -> Thread)"});
#undef ADD_CONSTRUCTORS
- set_binding(namespace_env(env, "Shell"), "from_text",
- Type(FunctionType, .args=new(arg_t, .name="text", .type=TEXT_TYPE),
- .ret=Type(TextType, .lang="Shell", .env=namespace_env(env, "Shell"))),
- "(Shell_t)");
-
set_binding(namespace_env(env, "Path"), "from_text",
Type(FunctionType, .args=new(arg_t, .name="text", .type=TEXT_TYPE),
.ret=PATH_TYPE),
@@ -576,6 +566,8 @@ env_t *new_compilation_unit(CORD libname)
struct {
const char *name, *code, *type_str;
} global_vars[] = {
+ {"USE_COLOR", "USE_COLOR", "Bool"},
+ {"random", "default_rng", "RNG"},
{"say", "say", "func(text:Text, newline=yes)"},
{"print", "say", "func(text:Text, newline=yes)"},
{"ask", "ask", "func(prompt:Text, bold=yes, force_tty=yes -> Text)"},
@@ -583,8 +575,6 @@ env_t *new_compilation_unit(CORD libname)
{"fail", "fail_text", "func(message:Text -> Abort)"},
{"sleep", "sleep_num", "func(seconds:Num)"},
{"now", "Moment$now", "func(->Moment)"},
- {"random", "default_rng", "RNG"},
- {"USE_COLOR", "USE_COLOR", "Bool"},
};
for (size_t i = 0; i < sizeof(global_vars)/sizeof(global_vars[0]); i++) {
@@ -604,10 +594,12 @@ CORD namespace_prefix(env_t *env, namespace_t *ns)
CORD prefix = CORD_EMPTY;
for (; ns; ns = ns->parent)
prefix = CORD_all(ns->name, "$", prefix);
- if (env->libname)
- prefix = CORD_all("_$", env->libname, "$", prefix);
- else
- prefix = CORD_all("_$", prefix);
+ if (env->locals != env->globals) {
+ if (env->libname)
+ prefix = CORD_all("_$", env->libname, "$", prefix);
+ else
+ prefix = CORD_all("_$", prefix);
+ }
return prefix;
}
diff --git a/examples/commands/README.md b/examples/commands/README.md
new file mode 100644
index 00000000..040f4bd5
--- /dev/null
+++ b/examples/commands/README.md
@@ -0,0 +1,5 @@
+# Commands
+
+This module provides a way to run executable programs and get their output. You
+can also feed in text to the programs' `stdin`. Think of it as `popen()` on
+steroids.
diff --git a/examples/commands/commands.c b/examples/commands/commands.c
new file mode 100644
index 00000000..f07b5c32
--- /dev/null
+++ b/examples/commands/commands.c
@@ -0,0 +1,177 @@
+#pragma once
+
+#include <errno.h>
+#include <gc.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <spawn.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/param.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define READ_END 0
+#define WRITE_END 1
+public int run_command(Text_t exe, Array_t arg_array, Table_t env_table,
+ Array_t input_bytes, Array_t *output_bytes, Array_t *error_bytes)
+{
+ pthread_testcancel();
+
+ struct sigaction sa = { .sa_handler = SIG_IGN }, oldint, oldquit;
+ sigaction(SIGINT, &sa, &oldint);
+ sigaction(SIGQUIT, &sa, &oldquit);
+ sigaddset(&sa.sa_mask, SIGCHLD);
+ sigset_t old, reset;
+ sigprocmask(SIG_BLOCK, &sa.sa_mask, &old);
+ sigemptyset(&reset);
+ if (oldint.sa_handler != SIG_IGN) sigaddset(&reset, SIGINT);
+ if (oldquit.sa_handler != SIG_IGN) sigaddset(&reset, SIGQUIT);
+ posix_spawnattr_t attr;
+ posix_spawnattr_init(&attr);
+ posix_spawnattr_setsigmask(&attr, &old);
+ posix_spawnattr_setsigdefault(&attr, &reset);
+ posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGDEF|POSIX_SPAWN_SETSIGMASK);
+
+ int child_inpipe[2], child_outpipe[2], child_errpipe[2];
+ pipe(child_inpipe);
+ pipe(child_outpipe);
+ pipe(child_errpipe);
+
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_adddup2(&actions, child_inpipe[READ_END], STDIN_FILENO);
+ posix_spawn_file_actions_addclose(&actions, child_inpipe[WRITE_END]);
+ posix_spawn_file_actions_adddup2(&actions, child_outpipe[WRITE_END], STDOUT_FILENO);
+ posix_spawn_file_actions_addclose(&actions, child_outpipe[READ_END]);
+ posix_spawn_file_actions_adddup2(&actions, child_errpipe[WRITE_END], STDERR_FILENO);
+ posix_spawn_file_actions_addclose(&actions, child_errpipe[READ_END]);
+
+ const char *exe_str = Text$as_c_string(exe);
+
+ Array_t arg_strs = {};
+ Array$insert_value(&arg_strs, exe_str, I(0), sizeof(char*));
+ for (int64_t i = 0; i < arg_array.length; i++)
+ Array$insert_value(&arg_strs, Text$as_c_string(*(Text_t*)(arg_array.data + i*arg_array.stride)), I(0), sizeof(char*));
+ Array$insert_value(&arg_strs, NULL, I(0), sizeof(char*));
+ char **args = arg_strs.data;
+
+ extern char **environ;
+ char **env = environ;
+ if (env_table.entries.length > 0) {
+ Array_t env_array = {}; // Array of const char*
+ for (char **e = environ; *e; e++)
+ Array$insert(&env_array, e, I(0), sizeof(char*));
+
+ for (int64_t i = 0; i < env_table.entries.length; i++) {
+ struct { Text_t key, value; } *entry = env_table.entries.data + env_table.entries.stride*i;
+ const char *env_entry = heap_strf("%k=%k", &entry->key, &entry->value);
+ Array$insert(&env_array, &env_entry, I(0), sizeof(char*));
+ }
+ Array$insert_value(&env_array, NULL, I(0), sizeof(char*));
+ assert(env_array.stride == sizeof(char*));
+ env = env_array.data;
+ }
+
+ pid_t pid;
+ int ret = exe_str[0] == '/' ?
+ posix_spawn(&pid, exe_str, &actions, &attr, args, env)
+ : posix_spawnp(&pid, exe_str, &actions, &attr, args, env);
+ if (ret != 0)
+ return -1;
+
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&actions);
+
+ close(child_inpipe[READ_END]);
+ close(child_outpipe[WRITE_END]);
+ close(child_errpipe[WRITE_END]);
+
+ struct pollfd pollfds[3] = {
+ {.fd=child_inpipe[WRITE_END], .events=POLLOUT},
+ {.fd=child_outpipe[WRITE_END], .events=POLLIN},
+ {.fd=child_errpipe[WRITE_END], .events=POLLIN},
+ };
+
+ if (input_bytes.length > 0 && input_bytes.stride != 1)
+ Array$compact(&input_bytes, sizeof(char));
+ if (output_bytes)
+ *output_bytes = (Array_t){.atomic=1, .stride=1, .length=0};
+ if (error_bytes)
+ *error_bytes = (Array_t){.atomic=1, .stride=1, .length=0};
+
+ for (;;) {
+ (void)poll(pollfds, sizeof(pollfds)/sizeof(pollfds[0]), -1); // Wait for data or readiness
+ bool did_something = false;
+ if (pollfds[0].revents) {
+ if (input_bytes.length > 0) {
+ ssize_t written = write(child_inpipe[WRITE_END], input_bytes.data, (size_t)input_bytes.length);
+ if (written > 0) {
+ input_bytes.data += written;
+ input_bytes.length -= (int64_t)written;
+ did_something = true;
+ } else if (written < 0) {
+ close(child_inpipe[WRITE_END]);
+ pollfds[0].events = 0;
+ }
+ }
+ if (input_bytes.length <= 0) {
+ close(child_inpipe[WRITE_END]);
+ pollfds[0].events = 0;
+ }
+ }
+ char buf[256];
+ if (pollfds[1].revents) {
+ ssize_t n = read(child_outpipe[READ_END], buf, sizeof(buf));
+ did_something = did_something || (n > 0);
+ if (n <= 0) {
+ close(child_outpipe[READ_END]);
+ pollfds[1].events = 0;
+ } else if (n > 0 && output_bytes) {
+ if (output_bytes->free < n) {
+ output_bytes->data = GC_REALLOC(output_bytes->data, (size_t)(output_bytes->length + n));
+ output_bytes->free = 0;
+ }
+ memcpy(output_bytes->data + output_bytes->length, buf, (size_t)n);
+ output_bytes->length += n;
+ }
+ }
+ if (pollfds[2].revents) {
+ ssize_t n = read(child_errpipe[READ_END], buf, sizeof(buf));
+ did_something = did_something || (n > 0);
+ if (n <= 0) {
+ close(child_errpipe[READ_END]);
+ pollfds[2].events = 0;
+ } else if (n > 0 && error_bytes) {
+ if (error_bytes->free < n) {
+ error_bytes->data = GC_REALLOC(error_bytes->data, (size_t)(error_bytes->length + n));
+ error_bytes->free = 0;
+ }
+ memcpy(error_bytes->data + error_bytes->length, buf, (size_t)n);
+ error_bytes->length += n;
+ }
+ }
+ if (!did_something) break;
+ }
+
+ int status = 0;
+ if (ret == 0) {
+ while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {
+ if (WIFEXITED(status) || WIFSIGNALED(status))
+ break;
+ else if (WIFSTOPPED(status))
+ kill(pid, SIGCONT);
+ }
+ }
+
+ sigaction(SIGINT, &oldint, NULL);
+ sigaction(SIGQUIT, &oldquit, NULL);
+ sigprocmask(SIG_SETMASK, &old, NULL);
+
+ if (ret) errno = ret;
+ return status;
+}
+
+#undef READ_END
+#undef WRITE_END
diff --git a/examples/commands/commands.tm b/examples/commands/commands.tm
new file mode 100644
index 00000000..940dc3d9
--- /dev/null
+++ b/examples/commands/commands.tm
@@ -0,0 +1,63 @@
+# Functions for running system commands
+
+use ./commands.c
+
+extern run_command:func(exe:Text, args:[Text], env:{Text,Text}, input:[Byte], output:&[Byte], error:&[Byte] -> Int32)
+
+enum ExitType(Exited(status:Int32), Signaled(signal:Int32), Failed)
+
+struct ProgramResult(stdout:[Byte], stderr:[Byte], exit_type:ExitType):
+ func or_fail(r:ProgramResult -> ProgramResult):
+ when r.exit_type is Exited(status):
+ if status == 0:
+ return r
+ else: fail("Program failed: $r")
+ fail("Program failed: $r")
+
+ func output_text(r:ProgramResult, trim_newline=yes -> Text?):
+ when r.exit_type is Exited(status):
+ if status == 0:
+ if text := Text.from_bytes(r.stdout):
+ if trim_newline:
+ text = text:trim($/{1 nl}/, trim_left=no, trim_right=yes)
+ return text
+ else: return none
+ return none
+
+ func error_text(r:ProgramResult -> Text?):
+ when r.exit_type is Exited(status):
+ if status == 0:
+ return Text.from_bytes(r.stderr)
+ else: return none
+ return none
+
+struct Command(command:Text, args=[:Text], env={:Text,Text}):
+ func run(command:Command, input="", input_bytes=[:Byte] -> ProgramResult):
+ if input.length > 0:
+ (&input_bytes):insert_all(input:bytes())
+
+ stdout := [:Byte]
+ stderr := [:Byte]
+ status := run_command(command.command, command.args, command.env, input_bytes, &stdout, &stderr)
+
+ if inline C : Bool { WIFEXITED(_$status) }:
+ return ProgramResult(stdout, stderr, ExitType.Exited(inline C : Int32 { WEXITSTATUS(_$status) }))
+
+ if inline C : Bool { WIFSIGNALED(_$status) }:
+ return ProgramResult(stdout, stderr, ExitType.Signaled(inline C : Int32 { WTERMSIG(_$status) }))
+
+ return ProgramResult(stdout, stderr, ExitType.Failed)
+
+ func get_output(command:Command, input="", trim_newline=yes -> Text?):
+ return command:run(input=input):output_text(trim_newline=trim_newline)
+
+ func get_output_bytes(command:Command, input="", input_bytes=[:Byte] -> [Byte]?):
+ result := command:run(input=input, input_bytes=input_bytes)
+ when result.exit_type is Exited(status):
+ if status == 0: return result.stdout
+ return none
+ else: return none
+
+func main(command:Text, args:[Text], input=""):
+ cmd := Command(command, args)
+ say(cmd:get_output(input=input)!)
diff --git a/examples/shell/shell.tm b/examples/shell/shell.tm
new file mode 100644
index 00000000..cf5fcd2e
--- /dev/null
+++ b/examples/shell/shell.tm
@@ -0,0 +1,35 @@
+use commands
+
+lang Shell:
+ convert(text:Text -> Shell):
+ return Shell.from_text("'" ++ text:replace($/'/, `'"'"'`) ++ "'")
+
+ convert(texts:[Text] -> Shell):
+ return Shell.from_text(" ":join([Shell(t).text for t in texts]))
+
+ convert(path:Path -> Shell):
+ return Shell(Text(path:expand_home()))
+
+ convert(paths:[Path] -> Shell):
+ return Shell.from_text(" ":join([Shell(Text(p)).text for p in paths]))
+
+ convert(n:Int -> Shell): return Shell.from_text(Text(n))
+ convert(n:Int64 -> Shell): return Shell.from_text(Text(n))
+ convert(n:Int32 -> Shell): return Shell.from_text(Text(n))
+ convert(n:Int16 -> Shell): return Shell.from_text(Text(n))
+ convert(n:Int8 -> Shell): return Shell.from_text(Text(n))
+ convert(n:Num -> Shell): return Shell.from_text(Text(n))
+ convert(n:Num32 -> Shell): return Shell.from_text(Text(n))
+
+ func command(shell:Shell -> Command):
+ return Command("sh", ["-c", shell.text])
+
+ func run(shell:Shell, input="", input_bytes=[:Byte] -> ProgramResult):
+ return shell:command():run(input=input, input_bytes=input_bytes)
+
+ func get_output(shell:Shell, input="", trim_newline=yes -> Text?):
+ return shell:command():get_output(input=input, trim_newline=trim_newline)
+
+ func get_output_bytes(shell:Shell, input="", input_bytes=[:Byte] -> [Byte]?):
+ return shell:command():get_output_bytes(input=input, input_bytes=input_bytes)
+
diff --git a/examples/tomo-install/tomo-install.tm b/examples/tomo-install/tomo-install.tm
index 43e23028..0205c380 100644
--- a/examples/tomo-install/tomo-install.tm
+++ b/examples/tomo-install/tomo-install.tm
@@ -1,3 +1,4 @@
+use shell
_USAGE := "
tomo-install file.tm...
@@ -33,7 +34,7 @@ func main(paths:[Path]):
for url in urls:
original_url := url
url_without_protocol := url:trim($|http{0-1 s}://|, trim_right=no)
- hash := $(echo -n @url_without_protocol | sha256sum):run()!:slice(to=32)
+ hash := $Shell@(echo -n @url_without_protocol | sha256sum):get_output()!:slice(to=32)
if (~/.local/share/tomo/installed/$hash):is_directory():
say("Already installed: $url")
skip
@@ -61,14 +62,14 @@ func main(paths:[Path]):
echo @original_url > ~/.local/share/tomo/installed/@hash/source.url
tomo -L ~/.local/share/tomo/installed/@hash
ln -f -s ../installed/@hash/lib@hash.so ~/.local/share/tomo/lib/lib@hash.so
- `:run()!)
+ `:get_output()!)
if alias:
- say($(
+ say($Shell(
set -exuo pipefail
ln -f -s @hash ~/.local/share/tomo/installed/@alias
ln -f -s lib@hash.so ~/.local/share/tomo/lib/lib@alias.so
- ):run()!)
+ ):get_output()!)
say("$\[1]Installed $url!$\[]")
diff --git a/parse.c b/parse.c
index 757c8cb3..5110e98e 100644
--- a/parse.c
+++ b/parse.c
@@ -1377,8 +1377,6 @@ PARSER(parse_text) {
if (!lang && (open_quote == '/' || open_quote == '|'))
lang = "Pattern";
- else if (!lang && open_quote == '(')
- lang = "Shell";
} else {
return NULL;
}
@@ -2604,4 +2602,21 @@ type_ast_t *parse_type_str(const char *str) {
return ast;
}
+ast_t *parse(const char *str) {
+ file_t *file = spoof_file("<string>", str);
+ parse_ctx_t ctx = {
+ .file=file,
+ .on_err=NULL,
+ };
+
+ const char *pos = file->text;
+ whitespace(&pos);
+ ast_t *ast = parse_file_body(&ctx, pos);
+ pos = ast->end;
+ whitespace(&pos);
+ if (pos < file->text + file->len && *pos != '\0')
+ parser_err(&ctx, pos, pos + strlen(pos), "I couldn't parse this part of the file");
+ return ast;
+}
+
// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0
diff --git a/parse.h b/parse.h
index 8fe71257..95b2ff95 100644
--- a/parse.h
+++ b/parse.h
@@ -8,5 +8,6 @@
type_ast_t *parse_type_str(const char *str);
ast_t *parse_file(const char *path, jmp_buf *on_err);
+ast_t *parse(const char *str);
// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0
diff --git a/stdlib/README.md b/stdlib/README.md
index c3814114..ba47dd0a 100644
--- a/stdlib/README.md
+++ b/stdlib/README.md
@@ -29,7 +29,6 @@ some common functionality.
- Paths: [header](stdlib/paths.h), [implementation](stdlib/paths.c)
- Patterns: [header](stdlib/patterns.h), [implementation](stdlib/patterns.c)
- Pointers: [header](stdlib/pointers.h), [implementation](stdlib/pointers.c)
-- Shell: [header](stdlib/shell.h), [implementation](stdlib/shell.c)
- Tables: [header](stdlib/tables.h), [implementation](stdlib/tables.c)
- Text: [header](stdlib/text.h), [implementation](stdlib/text.c)
- Threads: [header](stdlib/threads.h), [implementation](stdlib/threads.c)
diff --git a/stdlib/shell.c b/stdlib/shell.c
deleted file mode 100644
index 30dc7c6f..00000000
--- a/stdlib/shell.c
+++ /dev/null
@@ -1,157 +0,0 @@
-// A lang for Shell Command Language
-#include <errno.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <unistr.h>
-
-#include "arrays.h"
-#include "integers.h"
-#include "paths.h"
-#include "patterns.h"
-#include "shell.h"
-#include "text.h"
-#include "types.h"
-#include "util.h"
-
-public Shell_t Shell$escape_text(Text_t text)
-{
- return Texts(Text("'"), Text$replace(text, Text("'"), Text("'\"'\"'"), Text(""), false), Text("'"));
-}
-
-public Shell_t Shell$escape_path(Path_t path)
-{
- return Shell$escape_text(Path$as_text(&path, false, &Path$info));
-}
-
-public Shell_t Shell$escape_text_array(Array_t texts)
-{
- Array_t all_escaped = {};
- for (int64_t i = 0; i < texts.length; i++) {
- Text_t raw = *(Text_t*)(texts.data + i*texts.stride);
- Text_t escaped = Shell$escape_text(raw);
- Array$insert(&all_escaped, &escaped, I(0), sizeof(Text_t));
- }
- return Text$join(Text(" "), all_escaped);
-}
-
-public Shell_t Shell$escape_path_array(Array_t paths)
-{
- Array_t all_escaped = {};
- for (int64_t i = 0; i < paths.length; i++) {
- Path_t path = *(Path_t*)(paths.data + i*paths.stride);
- Text_t escaped = Shell$escape_path(path);
- Array$insert(&all_escaped, &escaped, I(0), sizeof(Text_t));
- }
- return Text$join(Text(" "), all_escaped);
-}
-
-public OptionalArray_t Shell$run_bytes(Shell_t command)
-{
- const char *cmd_str = Text$as_c_string(command);
- FILE *prog = popen(cmd_str, "r");
- if (!prog)
- return NONE_ARRAY;
-
- size_t capacity = 256, len = 0;
- char *content = GC_MALLOC_ATOMIC(capacity);
- char chunk[256];
- size_t just_read;
- do {
- just_read = fread(chunk, 1, sizeof(chunk), prog);
- if (just_read == 0) {
- if (errno == EAGAIN || errno == EINTR)
- continue;
- break;
- }
-
- if (len + (size_t)just_read >= capacity)
- content = GC_REALLOC(content, (capacity *= 2));
-
- memcpy(content + len, chunk, (size_t)just_read);
- len += (size_t)just_read;
- } while (just_read == sizeof(chunk));
-
- int status = pclose(prog);
- if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
- return NONE_ARRAY;
-
- return (Array_t){.data=content, .atomic=1, .stride=1, .length=len};
-}
-
-public OptionalText_t Shell$run(Shell_t command)
-{
- Array_t bytes = Shell$run_bytes(command);
- if (bytes.length < 0)
- return NONE_TEXT;
-
- if (bytes.length > 0 && *(char*)(bytes.data + (bytes.length-1)*bytes.stride) == '\n') {
- --bytes.length;
- if (bytes.length > 0 && *(char*)(bytes.data + (bytes.length-1)*bytes.stride) == '\r')
- --bytes.length;
- }
- return Text$from_bytes(bytes);
-}
-
-public OptionalInt32_t Shell$execute(Shell_t command)
-{
- const char *cmd_str = Text$as_c_string(command);
- int status = system(cmd_str);
- if (WIFEXITED(status))
- return (OptionalInt32_t){.i=WEXITSTATUS(status)};
- else
- return (OptionalInt32_t){.is_none=true};
-}
-
-static void _line_reader_cleanup(FILE **f)
-{
- if (f && *f) {
- pclose(*f);
- *f = NULL;
- }
-}
-
-static Text_t _next_line(FILE **f)
-{
- if (!f || !*f) return NONE_TEXT;
-
- char *line = NULL;
- size_t size = 0;
- ssize_t len = getline(&line, &size, *f);
- if (len <= 0) {
- _line_reader_cleanup(f);
- return NONE_TEXT;
- }
-
- while (len > 0 && (line[len-1] == '\r' || line[len-1] == '\n'))
- --len;
-
- if (u8_check((uint8_t*)line, (size_t)len) != NULL)
- fail("Invalid UTF8!");
-
- Text_t line_text = Text$format("%.*s", len, line);
- free(line);
- return line_text;
-}
-
-public OptionalClosure_t Shell$by_line(Shell_t command)
-{
- const char *cmd_str = Text$as_c_string(command);
- FILE *prog = popen(cmd_str, "r");
- if (!prog)
- return NONE_CLOSURE;
-
- FILE **wrapper = GC_MALLOC(sizeof(FILE*));
- *wrapper = prog;
- GC_register_finalizer(wrapper, (void*)_line_reader_cleanup, NULL, NULL, NULL);
- return (Closure_t){.fn=(void*)_next_line, .userdata=wrapper};
-}
-
-public const TypeInfo_t Shell$info = {
- .size=sizeof(Shell_t),
- .align=__alignof__(Shell_t),
- .tag=TextInfo,
- .TextInfo={.lang="Shell"},
- .metamethods=Text$metamethods,
-};
-
-// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0
diff --git a/stdlib/shell.h b/stdlib/shell.h
deleted file mode 100644
index 517a3609..00000000
--- a/stdlib/shell.h
+++ /dev/null
@@ -1,35 +0,0 @@
-#pragma once
-
-// A lang for Shell Command Language
-
-#include <stdbool.h>
-#include <stdint.h>
-
-#include "arrays.h"
-#include "datatypes.h"
-#include "optionals.h"
-#include "text.h"
-#include "types.h"
-
-#define Shell_t Text_t
-#define OptionalShell_t Text_t
-#define Shell(text) ((Shell_t)Text(text))
-#define Shells(...) ((Shell_t)Texts(__VA_ARGS__))
-
-OptionalClosure_t Shell$by_line(Shell_t command);
-Shell_t Shell$escape_text(Text_t text);
-Shell_t Shell$escape_path(Path_t path);
-Shell_t Shell$escape_text_array(Array_t texts);
-Shell_t Shell$escape_path_array(Array_t paths);
-OptionalArray_t Shell$run_bytes(Shell_t command);
-OptionalText_t Shell$run(Shell_t command);
-OptionalInt32_t Shell$execute(Shell_t command);
-
-#define Shell$hash Text$hash
-#define Shell$compare Text$compare
-#define Shell$equal Text$equal
-
-extern const TypeInfo_t Shell$info;
-
-// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0
-
diff --git a/stdlib/tomo.h b/stdlib/tomo.h
index d3887af4..61dba404 100644
--- a/stdlib/tomo.h
+++ b/stdlib/tomo.h
@@ -25,7 +25,6 @@
#include "patterns.h"
#include "pointers.h"
#include "rng.h"
-#include "shell.h"
#include "siphash.h"
#include "structs.h"
#include "tables.h"
diff --git a/tomo.c b/tomo.c
index 279a7c79..07c431a4 100644
--- a/tomo.c
+++ b/tomo.c
@@ -21,7 +21,6 @@
#include "stdlib/optionals.h"
#include "stdlib/patterns.h"
#include "stdlib/paths.h"
-#include "stdlib/shell.h"
#include "stdlib/text.h"
#include "typecheck.h"
#include "types.h"
@@ -490,6 +489,12 @@ void build_file_dependency_graph(Path_t path, Table_t *to_compile, Table_t *to_l
case USE_MODULE: {
Text_t lib = Text$format("'%s/.local/share/tomo/installed/%s/lib%s.so'", getenv("HOME"), use->path, use->path);
Table$set(to_link, &lib, ((Bool_t[1]){1}), Table$info(&Text$info, &Bool$info));
+
+ Array_t children = Path$glob(Path$from_str(heap_strf("%s/.local/share/tomo/installed/%s/*.tm", getenv("HOME"), use->path)));
+ for (int64_t i = 0; i < children.length; i++) {
+ Path_t *child = (Path_t*)(children.data + i*children.stride);
+ build_file_dependency_graph(*child, to_compile, to_link);
+ }
break;
}
case USE_SHARED_OBJECT: {