aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2025-02-25 00:59:31 -0500
committerBruce Hill <bruce@bruce-hill.com>2025-02-25 00:59:31 -0500
commit9a62f8d6a6f8148deaea89e73d866439b588babb (patch)
tree3984626fc4b246a6677939c228527bc285f0e6f6
parent928f3250b3914c88d105d8cf297c6b27a96ed5ad (diff)
Add $Shell.execute()
-rw-r--r--docs/shell.md117
-rw-r--r--environment.c4
-rw-r--r--stdlib/shell.c10
-rw-r--r--stdlib/shell.h2
4 files changed, 127 insertions, 6 deletions
diff --git a/docs/shell.md b/docs/shell.md
new file mode 100644
index 00000000..f453a1d0
--- /dev/null
+++ b/docs/shell.md
@@ -0,0 +1,117 @@
+# Shell Scripting
+
+Tomo comes with a built-in [lang](langs.md) called `Shell` for shell commands.
+This lets you write and invoke shell commands in a more type-safe way.
+
+```tomo
+user_name := ask("What's your name? ")
+_ := $Shell"echo Hello $user_name"
+```
+
+In the above example, there is no risk of code injection, because the
+user-controlled string is automatically escaped when performing interpolation.
+
+## Shell Methods
+
+### `by_line`
+
+**Description:**
+Run a shell command and return an iterator over its output, line-by-line.
+
+**Signature:**
+```tomo
+func by_line(command: Shell -> Void)
+```
+
+**Parameters:**
+
+- `command`: The command to run.
+
+**Returns:**
+An optional iterator over the lines of the command's output. If the command fails
+to run, `none` will be returned.
+
+**Example:**
+```tomo
+i := 1
+for line in $Shell"ping www.example.com":by_line()!:
+ stop if i > 5
+ i += 1
+```
+
+### `execute`
+
+**Description:**
+Execute a shell command without capturing its output and return its exit status.
+
+**Signature:**
+```tomo
+func execute(command: Shell -> Int32?)
+```
+
+**Parameters:**
+
+- `command`: The command to execute.
+
+**Returns:**
+If the command exits normally, return its exit status. Otherwise return `none`.
+
+**Example:**
+```tomo
+>> $Shell"touch file.txt":execute()
+= 0?
+```
+
+---
+
+### `run`
+
+**Description:**
+Run a shell command and return the output text from `stdout`.
+
+**Signature:**
+```tomo
+func run(command: Shell -> Text?)
+```
+
+**Parameters:**
+
+- `command`: The command to run.
+
+**Returns:**
+If the program fails to run (e.g. a non-existent command), return `none`,
+otherwise return the entire standard output of the command as text. **Note:**
+if there is a trailing newline, it will be stripped.
+
+**Example:**
+```tomo
+>> $Shell"seq 5":run()
+= "1$\n2$\n3$\n4$\n5"
+```
+
+---
+
+### `run_bytes`
+
+**Description:**
+Run a shell command and return the output in raw bytes from `stdout`.
+
+**Signature:**
+```tomo
+func run(command: Shell -> [Byte]?)
+```
+
+**Parameters:**
+
+- `command`: The command to run.
+
+**Returns:**
+If the program fails to run (e.g. a non-existent command), return `none`,
+otherwise return the entire standard output of the command as an array of
+bytes.
+
+**Example:**
+```tomo
+>> $Shell"seq 5":run_bytes()
+= [0x31, 0x0A, 0x32, 0x0A, 0x33, 0x0A, 0x34, 0x0A, 0x35, 0x0A]
+```
diff --git a/environment.c b/environment.c
index b785a8e3..2e1c4add 100644
--- a/environment.c
+++ b/environment.c
@@ -380,8 +380,8 @@ env_t *new_compilation_unit(CORD libname)
{"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", "func(command:Shell -> [Byte]?)"},
+ {"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,
diff --git a/stdlib/shell.c b/stdlib/shell.c
index 08898acc..7c54950c 100644
--- a/stdlib/shell.c
+++ b/stdlib/shell.c
@@ -50,7 +50,7 @@ public OptionalArray_t Shell$run_bytes(Shell_t command)
if (len + (size_t)just_read >= capacity)
content = GC_REALLOC(content, (capacity *= 2));
- memcpy(&content[len], chunk, (size_t)just_read);
+ memcpy(content + len, chunk, (size_t)just_read);
len += (size_t)just_read;
} while (just_read == sizeof(chunk));
@@ -75,10 +75,14 @@ public OptionalText_t Shell$run(Shell_t command)
return Text$from_bytes(bytes);
}
-public int32_t Shell$execute(Shell_t command)
+public OptionalInt32_t Shell$execute(Shell_t command)
{
const char *cmd_str = Text$as_c_string(command);
- return system(cmd_str);
+ 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)
diff --git a/stdlib/shell.h b/stdlib/shell.h
index a8f35f65..500bb043 100644
--- a/stdlib/shell.h
+++ b/stdlib/shell.h
@@ -21,7 +21,7 @@ Shell_t Shell$escape_text(Text_t text);
Shell_t Shell$escape_text_array(Array_t texts);
OptionalArray_t Shell$run_bytes(Shell_t command);
OptionalText_t Shell$run(Shell_t command);
-int32_t Shell$execute(Shell_t command);
+OptionalInt32_t Shell$execute(Shell_t command);
#define Shell$hash Text$hash
#define Shell$compare Text$compare