aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-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
5 files changed, 285 insertions, 4 deletions
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!$\[]")